From fd2a3912d24eb7653b5a21d5c6432b03076ca9d3 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Thu, 6 Jul 2023 10:55:03 -0500 Subject: [PATCH 1/4] Add Auth Project management examples to docs --- ...101339_sirosen_manage_projects_example.rst | 5 + docs/examples/auth_manage_projects/index.rst | 68 ++++++++ .../list_and_create_projects.py | 80 +++++++++ .../auth_manage_projects/list_projects.py | 40 +++++ .../auth_manage_projects/manage_projects.py | 157 ++++++++++++++++++ docs/examples/index.rst | 1 + 6 files changed, 351 insertions(+) create mode 100644 changelog.d/20230706_101339_sirosen_manage_projects_example.rst create mode 100644 docs/examples/auth_manage_projects/index.rst create mode 100644 docs/examples/auth_manage_projects/list_and_create_projects.py create mode 100644 docs/examples/auth_manage_projects/list_projects.py create mode 100644 docs/examples/auth_manage_projects/manage_projects.py diff --git a/changelog.d/20230706_101339_sirosen_manage_projects_example.rst b/changelog.d/20230706_101339_sirosen_manage_projects_example.rst new file mode 100644 index 000000000..f7f9b82d9 --- /dev/null +++ b/changelog.d/20230706_101339_sirosen_manage_projects_example.rst @@ -0,0 +1,5 @@ +Documentation +~~~~~~~~~~~~~ + +- New scripts in the example gallery demonstrate usage of the Globus Auth + Developer APIs to List, Create, Delete, and Update Projects. (:pr:`NUMBER`) diff --git a/docs/examples/auth_manage_projects/index.rst b/docs/examples/auth_manage_projects/index.rst new file mode 100644 index 000000000..560827209 --- /dev/null +++ b/docs/examples/auth_manage_projects/index.rst @@ -0,0 +1,68 @@ +Manage Globus Auth Projects +=========================== + +.. note:: + + The following scripts, when run, may leave tokens in a JSON file in + your home directory. Be sure to delete these tokens after use. + +List Projects via the Auth API +------------------------------ + +The following is a very small and simple script using the Globus Auth Developer +APIs. + +It uses the tutorial client ID from the :ref:`tutorial `. +For simplicity, the script will prompt for login on each use. + +.. literalinclude:: list_projects.py + :caption: ``list_projects.py`` [:download:`download `] + :language: python + + +List and Create Projects via the Auth API +----------------------------------------- + +The next example builds upon the earlier example by offering a pair of +features, List and Create. + +Argument parsing allows for an action to be selected, which is then executed by +calling the appropriate function. + +.. literalinclude:: list_and_create_projects.py + :caption: ``list_and_create_projects.py`` [:download:`download `] + :language: python + + +List, Create, Update, and Delete Projects via the Auth API +---------------------------------------------------------- + +.. warning:: + + The following script has destructive capabilities. + + On update, the current user is *always* set as the only admin and + contact for projects. This means that other admins are implicitly removed. + + Deleting projects may be harmful to your production applications. + Only delete with care. + +The following example expands upon the former by adding update and delete +functionality. +For updates, the script only supports updating the ``display_name`` of a project. + +Because Delete and Update require authentication under a session policy, the +login code grows here to include a storage adapter (with data kept in +``~/.sdk-manage-projects.json``). If a policy failure is encountered, the code +will prompt the user to login again to satisfy the policy and then reexecute +the desired activity. + +It also changes the handling of the session error to immediately and +automatically retry execution of the operation. + +As a result, this example is significantly more complex, but it still follows +the same basic pattern as above. + +.. literalinclude:: manage_projects.py + :caption: ``manage_projects.py`` [:download:`download `] + :language: python diff --git a/docs/examples/auth_manage_projects/list_and_create_projects.py b/docs/examples/auth_manage_projects/list_and_create_projects.py new file mode 100644 index 000000000..e3dd06882 --- /dev/null +++ b/docs/examples/auth_manage_projects/list_and_create_projects.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python + +import argparse +import os + +import globus_sdk +from globus_sdk.tokenstorage import SimpleJSONFileAdapter + +MY_FILE_ADAPTER = SimpleJSONFileAdapter( + os.path.expanduser("~/.sdk-manage-projects.json") +) + +SCOPES = [globus_sdk.AuthClient.scopes.manage_projects, "openid", "email"] +RESOURCE_SERVER = globus_sdk.AuthClient.resource_server + +# tutorial client ID +# we recommend replacing this with your own client for any production use-cases +CLIENT_ID = "61338d24-54d5-408f-a10d-66c06b59f6d2" + +NATIVE_CLIENT = globus_sdk.NativeAppAuthClient(CLIENT_ID) + + +def do_login_flow(): + NATIVE_CLIENT.oauth2_start_flow(requested_scopes=SCOPES) + authorize_url = NATIVE_CLIENT.oauth2_get_authorize_url() + print(f"Please go to this URL and login:\n\n{authorize_url}\n") + auth_code = input("Please enter the code here: ").strip() + tokens = NATIVE_CLIENT.oauth2_exchange_code_for_tokens(auth_code) + return tokens.by_resource_server[RESOURCE_SERVER] + + +def get_auth_client(): + tokens = do_login_flow() + return globus_sdk.AuthClient( + authorizer=globus_sdk.AccessTokenAuthorizer(tokens["access_token"]) + ) + + +def create_project(args): + auth_client = get_auth_client() + userinfo = auth_client.oauth2_userinfo() + print( + auth_client.create_project( + args.name, + contact_email=userinfo["email"], + admin_ids=userinfo["sub"], + ) + ) + + +def list_projects(args): + auth_client = get_auth_client() + for project in auth_client.get_projects(): + print(f"name: {project['display_name']}") + print(f"id: {project['id']}") + print() + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("action", choices=["create", "list"]) + parser.add_argument("-n", "--name", help="Project name for create") + args = parser.parse_args() + + execute(parser, args) + + +def execute(parser, args): + if args.action == "create": + if args.name is None: + parser.error("create requires --name") + create_project(args) + elif args.action == "list": + list_projects(args) + else: + raise NotImplementedError() + + +if __name__ == "__main__": + main() diff --git a/docs/examples/auth_manage_projects/list_projects.py b/docs/examples/auth_manage_projects/list_projects.py new file mode 100644 index 000000000..2a075f022 --- /dev/null +++ b/docs/examples/auth_manage_projects/list_projects.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python + +import globus_sdk + +SCOPES = [globus_sdk.AuthClient.scopes.manage_projects, "openid", "email"] +RESOURCE_SERVER = globus_sdk.AuthClient.resource_server + +# tutorial client ID +# we recommend replacing this with your own client for any production use-cases +CLIENT_ID = "61338d24-54d5-408f-a10d-66c06b59f6d2" + +NATIVE_CLIENT = globus_sdk.NativeAppAuthClient(CLIENT_ID) + + +def do_login_flow(): + NATIVE_CLIENT.oauth2_start_flow(requested_scopes=SCOPES) + authorize_url = NATIVE_CLIENT.oauth2_get_authorize_url() + print(f"Please go to this URL and login:\n\n{authorize_url}\n") + auth_code = input("Please enter the code here: ").strip() + tokens = NATIVE_CLIENT.oauth2_exchange_code_for_tokens(auth_code) + return tokens.by_resource_server[RESOURCE_SERVER] + + +def get_auth_client(): + tokens = do_login_flow() + return globus_sdk.AuthClient( + authorizer=globus_sdk.AccessTokenAuthorizer(tokens["access_token"]) + ) + + +def main(): + auth_client = get_auth_client() + for project in auth_client.get_projects(): + print(f"name: {project['display_name']}") + print(f"id: {project['id']}") + print() + + +if __name__ == "__main__": + main() diff --git a/docs/examples/auth_manage_projects/manage_projects.py b/docs/examples/auth_manage_projects/manage_projects.py new file mode 100644 index 000000000..1d33cdaf3 --- /dev/null +++ b/docs/examples/auth_manage_projects/manage_projects.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python + +import argparse +import os + +import globus_sdk +from globus_sdk.tokenstorage import SimpleJSONFileAdapter + +MY_FILE_ADAPTER = SimpleJSONFileAdapter( + os.path.expanduser("~/.sdk-manage-projects.json") +) + +SCOPES = [globus_sdk.AuthClient.scopes.manage_projects, "openid", "email"] +RESOURCE_SERVER = globus_sdk.AuthClient.resource_server + +# tutorial client ID +# we recommend replacing this with your own client for any production use-cases +CLIENT_ID = "61338d24-54d5-408f-a10d-66c06b59f6d2" + +NATIVE_CLIENT = globus_sdk.NativeAppAuthClient(CLIENT_ID) + + +def do_login_flow(*, session_params: dict | None = None): + NATIVE_CLIENT.oauth2_start_flow(requested_scopes=SCOPES) + # special note! + # this works because oauth2_get_authorize_url supports session error data + # as parameters to build the authorization URL + # you could do this manually with the following supported parameters: + # - session_required_identities + # - session_required_single_domain + # - session_required_policies + authorize_url = NATIVE_CLIENT.oauth2_get_authorize_url(**session_params) + print(f"Please go to this URL and login:\n\n{authorize_url}\n") + auth_code = input("Please enter the code here: ").strip() + tokens = NATIVE_CLIENT.oauth2_exchange_code_for_tokens(auth_code) + return tokens + + +def get_tokens(): + if not MY_FILE_ADAPTER.file_exists(): + # do a login flow, getting back initial tokens + response = do_login_flow() + # now store the tokens and pull out the correct token + MY_FILE_ADAPTER.store(response) + tokens = response.by_resource_server[RESOURCE_SERVER] + else: + # otherwise, we already did login; load the tokens from that file + tokens = MY_FILE_ADAPTER.get_token_data(RESOURCE_SERVER) + + return tokens + + +def get_auth_client(): + tokens = get_tokens() + return globus_sdk.AuthClient( + authorizer=globus_sdk.AccessTokenAuthorizer(tokens["access_token"]) + ) + + +def create_project(args): + auth_client = get_auth_client() + userinfo = auth_client.oauth2_userinfo() + print( + auth_client.create_project( + args.name, + contact_email=userinfo["email"], + admin_ids=userinfo["sub"], + ) + ) + + +def update_project(args): + auth_client = get_auth_client(reauth=args.reauth) + userinfo = auth_client.oauth2_userinfo() + kwargs = {"admin_ids": userinfo["sub"]} + if args.name is not None: + kwargs["display_name"] = args.name + print(auth_client.update_project(args.project_id, **kwargs)) + + +def delete_project(args): + auth_client = get_auth_client() + print(auth_client.delete_project(args.project_id)) + + +def list_projects(args): + auth_client = get_auth_client() + for project in auth_client.get_projects(): + print(f"name: {project['display_name']}") + print(f"id: {project['id']}") + print() + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("action", choices=["create", "update", "delete", "list"]) + parser.add_argument("-p", "--project-id", help="Project ID for delete") + parser.add_argument("-n", "--name", help="Project name for create") + args = parser.parse_args() + + try: + execute(parser, args) + except globus_sdk.GlobusAPIError as err: + if err.info.authorization_parameters: + err_params = err.info.authorization_parameters + session_params = {} + if err_params.session_required_identities: + print("session required identities detected") + session_params[ + "session_required_identities" + ] = err_params.session_required_identities + if err_params.session_required_single_domain: + print("session required single domain detected") + session_params[ + "session_required_single_domain" + ] = err_params.session_required_single_domain + if err_params.session_required_policies: + print("session required policies detected") + session_params[ + "session_required_policies" + ] = err_params.session_required_policies + print(session_params) + print(err_params) + response = do_login_flow(session_params=session_params) + # now store the tokens + MY_FILE_ADAPTER.store(response) + print( + "Reauthenticated successfully to satisfy " + "session requirements. Will now try again.\n" + ) + + # try the action again + execute(parser, args) + raise + + +def execute(parser, args): + if args.action == "create": + if args.name is None: + parser.error("create requires --name") + create_project(args) + elif args.action == "update": + if args.project_id is None: + raise ValueError("update requires --project-id") + update_project(args) + elif args.action == "delete": + if args.project_id is None: + parser.error("delete requires --project-id") + delete_project(args) + elif args.action == "list": + list_projects(args) + else: + raise NotImplementedError() + + +if __name__ == "__main__": + main() diff --git a/docs/examples/index.rst b/docs/examples/index.rst index 7c1fdeec9..b942ad86f 100644 --- a/docs/examples/index.rst +++ b/docs/examples/index.rst @@ -7,6 +7,7 @@ Each of these pages contains an example of a piece of SDK functionality. .. toctree:: minimal_transfer_script/index + auth_manage_projects/index group_listing authorization native_app From f218e67c4ec57ccfbc51325ea59bdf90e711ba33 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Thu, 6 Jul 2023 10:56:41 -0500 Subject: [PATCH 2/4] In Update Project example, do not set admin_ids Due to a bug, admin_ids or group_admin_ids must be supplied when performing an update. As a result, the prior version of this script sets the current user as an admin and this necessitates warning text. Remove that setting to avoid having a warning which is based around a bug. This means that the example cannot be considered ready to release until the bug is resolved. --- docs/examples/auth_manage_projects/index.rst | 3 --- docs/examples/auth_manage_projects/manage_projects.py | 3 +-- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/docs/examples/auth_manage_projects/index.rst b/docs/examples/auth_manage_projects/index.rst index 560827209..ad12f55cd 100644 --- a/docs/examples/auth_manage_projects/index.rst +++ b/docs/examples/auth_manage_projects/index.rst @@ -41,9 +41,6 @@ List, Create, Update, and Delete Projects via the Auth API The following script has destructive capabilities. - On update, the current user is *always* set as the only admin and - contact for projects. This means that other admins are implicitly removed. - Deleting projects may be harmful to your production applications. Only delete with care. diff --git a/docs/examples/auth_manage_projects/manage_projects.py b/docs/examples/auth_manage_projects/manage_projects.py index 1d33cdaf3..3fffe73eb 100644 --- a/docs/examples/auth_manage_projects/manage_projects.py +++ b/docs/examples/auth_manage_projects/manage_projects.py @@ -71,8 +71,7 @@ def create_project(args): def update_project(args): auth_client = get_auth_client(reauth=args.reauth) - userinfo = auth_client.oauth2_userinfo() - kwargs = {"admin_ids": userinfo["sub"]} + kwargs = {} if args.name is not None: kwargs["display_name"] = args.name print(auth_client.update_project(args.project_id, **kwargs)) From def86c7045a384f7257e2a85814ade934ce6286f Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Tue, 18 Jul 2023 15:26:25 -0500 Subject: [PATCH 3/4] Remove "update" from manage-projects example This is blocked on a bug in the way that updates are handled vis-a-vis admin_ids. Rather than holding back the whole doc, simply drop the `update` functionality. It can be restored in the future. --- docs/examples/auth_manage_projects/index.rst | 15 +++++---------- .../auth_manage_projects/manage_projects.py | 14 +------------- 2 files changed, 6 insertions(+), 23 deletions(-) diff --git a/docs/examples/auth_manage_projects/index.rst b/docs/examples/auth_manage_projects/index.rst index ad12f55cd..9cdae6056 100644 --- a/docs/examples/auth_manage_projects/index.rst +++ b/docs/examples/auth_manage_projects/index.rst @@ -34,8 +34,8 @@ calling the appropriate function. :language: python -List, Create, Update, and Delete Projects via the Auth API ----------------------------------------------------------- +List, Create, and Delete Projects via the Auth API +-------------------------------------------------- .. warning:: @@ -44,19 +44,14 @@ List, Create, Update, and Delete Projects via the Auth API Deleting projects may be harmful to your production applications. Only delete with care. -The following example expands upon the former by adding update and delete -functionality. -For updates, the script only supports updating the ``display_name`` of a project. +The following example expands upon the former by adding delete functionality. -Because Delete and Update require authentication under a session policy, the -login code grows here to include a storage adapter (with data kept in +Because Delete requires authentication under a session policy, the login code +grows here to include a storage adapter (with data kept in ``~/.sdk-manage-projects.json``). If a policy failure is encountered, the code will prompt the user to login again to satisfy the policy and then reexecute the desired activity. -It also changes the handling of the session error to immediately and -automatically retry execution of the operation. - As a result, this example is significantly more complex, but it still follows the same basic pattern as above. diff --git a/docs/examples/auth_manage_projects/manage_projects.py b/docs/examples/auth_manage_projects/manage_projects.py index 3fffe73eb..de0ef764b 100644 --- a/docs/examples/auth_manage_projects/manage_projects.py +++ b/docs/examples/auth_manage_projects/manage_projects.py @@ -69,14 +69,6 @@ def create_project(args): ) -def update_project(args): - auth_client = get_auth_client(reauth=args.reauth) - kwargs = {} - if args.name is not None: - kwargs["display_name"] = args.name - print(auth_client.update_project(args.project_id, **kwargs)) - - def delete_project(args): auth_client = get_auth_client() print(auth_client.delete_project(args.project_id)) @@ -92,7 +84,7 @@ def list_projects(args): def main(): parser = argparse.ArgumentParser() - parser.add_argument("action", choices=["create", "update", "delete", "list"]) + parser.add_argument("action", choices=["create", "delete", "list"]) parser.add_argument("-p", "--project-id", help="Project ID for delete") parser.add_argument("-n", "--name", help="Project name for create") args = parser.parse_args() @@ -138,10 +130,6 @@ def execute(parser, args): if args.name is None: parser.error("create requires --name") create_project(args) - elif args.action == "update": - if args.project_id is None: - raise ValueError("update requires --project-id") - update_project(args) elif args.action == "delete": if args.project_id is None: parser.error("delete requires --project-id") From ebf2f66bfb72d78bb9bca20843c5be4cbe2a81f0 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Tue, 18 Jul 2023 19:53:59 -0500 Subject: [PATCH 4/4] Remove args from list_projects example functions --- .../examples/auth_manage_projects/list_and_create_projects.py | 4 ++-- docs/examples/auth_manage_projects/manage_projects.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/examples/auth_manage_projects/list_and_create_projects.py b/docs/examples/auth_manage_projects/list_and_create_projects.py index e3dd06882..50d44eda1 100644 --- a/docs/examples/auth_manage_projects/list_and_create_projects.py +++ b/docs/examples/auth_manage_projects/list_and_create_projects.py @@ -48,7 +48,7 @@ def create_project(args): ) -def list_projects(args): +def list_projects(): auth_client = get_auth_client() for project in auth_client.get_projects(): print(f"name: {project['display_name']}") @@ -71,7 +71,7 @@ def execute(parser, args): parser.error("create requires --name") create_project(args) elif args.action == "list": - list_projects(args) + list_projects() else: raise NotImplementedError() diff --git a/docs/examples/auth_manage_projects/manage_projects.py b/docs/examples/auth_manage_projects/manage_projects.py index de0ef764b..8ba516dbf 100644 --- a/docs/examples/auth_manage_projects/manage_projects.py +++ b/docs/examples/auth_manage_projects/manage_projects.py @@ -74,7 +74,7 @@ def delete_project(args): print(auth_client.delete_project(args.project_id)) -def list_projects(args): +def list_projects(): auth_client = get_auth_client() for project in auth_client.get_projects(): print(f"name: {project['display_name']}") @@ -135,7 +135,7 @@ def execute(parser, args): parser.error("delete requires --project-id") delete_project(args) elif args.action == "list": - list_projects(args) + list_projects() else: raise NotImplementedError()