diff --git a/src/globus_sdk/_testing/data/auth/create_child_client.py b/src/globus_sdk/_testing/data/auth/create_child_client.py new file mode 100644 index 000000000..7277e3e08 --- /dev/null +++ b/src/globus_sdk/_testing/data/auth/create_child_client.py @@ -0,0 +1,110 @@ +import typing as t +import uuid + +from responses.matchers import json_params_matcher + +from globus_sdk._testing.models import RegisteredResponse, ResponseSet + +_COMMON_RESPONSE_RECORD = { + "fqdns": [], + "links": {"privacy_policy": None, "terms_and_conditions": None}, + "parent_client": None, + "preselect_idp": None, + "prompt_for_named_grant": True, + "redirect_uris": [], + "required_idp": None, + "scopes": [], + "userinfo_from_effective_identity": True, +} + +PUBLIC_CLIENT_RESPONSE_RECORD = { + "client_type": "public_installed_client", + "grant_types": ["authorization_code", "refresh_token"], + **_COMMON_RESPONSE_RECORD, +} + +PRIVATE_CLIENT_RESPONSE_RECORD = { + "client_type": "hybrid_confidential_client_resource_server", + "grant_types": [ + "authorization_code", + "client_credentials", + "refresh_token", + "urn:globus:auth:grant_type:dependent_token", + ], + **_COMMON_RESPONSE_RECORD, +} + +PUBLIC_CLIENT_REQUEST_ARGS = { + "name": "FOO", + "public_client": True, + "publicly_visible": True, +} + +PUBLIC_CLIENT_REQUEST_BODY = { + "name": PUBLIC_CLIENT_REQUEST_ARGS["name"], + "public_client": PUBLIC_CLIENT_REQUEST_ARGS["public_client"], + "visibility": "public" + if PUBLIC_CLIENT_REQUEST_ARGS["publicly_visible"] + else "private", +} + + +def register_response( + args: t.Mapping[str, t.Any], +) -> RegisteredResponse: + # Some name of args to create_client() have differenlty named fields. + body_fields: t.Dict[str, t.Any] = {} + for arg_name in args: + if arg_name == "publicly_visible": + body_fields["visibility"] = "public" if args[arg_name] else "private" + elif arg_name == "terms_and_conditions" or arg_name == "privacy_policy": + body_fields["links"] = { + arg_name: args[arg_name], + **body_fields.get("links", {}), + } + else: + body_fields[arg_name] = args[arg_name] + + # Default to a public client response unless arg says otherwise + client_response_record = ( + PUBLIC_CLIENT_RESPONSE_RECORD + if {**PUBLIC_CLIENT_REQUEST_ARGS, **args}["public_client"] is True + else PRIVATE_CLIENT_RESPONSE_RECORD + ) + + return RegisteredResponse( + service="auth", + method="POST", + path="/v2/api/clients", + json={"client": {**client_response_record, **body_fields}}, + metadata={ + # Test functions use 'args' to form request + "args": {**PUBLIC_CLIENT_REQUEST_ARGS, **args}, + # Test functions use 'response' to verify response + "response": body_fields, + }, + match=[ + json_params_matcher( + {"client": {**PUBLIC_CLIENT_REQUEST_BODY, **body_fields}} + ) + ], + ) + + +RESPONSES = ResponseSet( + default=register_response({}), + name=register_response({"name": str(uuid.uuid4()).replace("-", "")}), + public_client=register_response({"public_client": True}), + private_client=register_response({"public_client": False}), + publicly_visible=register_response({"publicly_visible": True}), + not_publicly_visible=register_response({"publicly_visible": False}), + redirect_uris=register_response({"redirect_uris": ["https://foo.com"]}), + links=register_response( + { + "terms_and_conditions": "https://foo.org", + "privacy_policy": "https://boo.org", + } + ), + required_idp=register_response({"required_idp": str(uuid.uuid1())}), + preselect_idp=register_response({"preselect_idp": str(uuid.uuid1())}), +) diff --git a/src/globus_sdk/_testing/data/auth/create_client.py b/src/globus_sdk/_testing/data/auth/create_client.py new file mode 100644 index 000000000..574730676 --- /dev/null +++ b/src/globus_sdk/_testing/data/auth/create_client.py @@ -0,0 +1,115 @@ +import typing as t +import uuid + +from responses.matchers import json_params_matcher + +from globus_sdk._testing.models import RegisteredResponse, ResponseSet + +_COMMON_RESPONSE_RECORD = { + "fqdns": [], + "links": {"privacy_policy": None, "terms_and_conditions": None}, + "parent_client": None, + "preselect_idp": None, + "prompt_for_named_grant": True, + "redirect_uris": [], + "required_idp": None, + "scopes": [], + "userinfo_from_effective_identity": True, +} + +PUBLIC_CLIENT_RESPONSE_RECORD = { + "client_type": "public_installed_client", + "grant_types": ["authorization_code", "refresh_token"], + **_COMMON_RESPONSE_RECORD, +} + +PRIVATE_CLIENT_RESPONSE_RECORD = { + "client_type": "hybrid_confidential_client_resource_server", + "grant_types": [ + "authorization_code", + "client_credentials", + "refresh_token", + "urn:globus:auth:grant_type:dependent_token", + ], + **_COMMON_RESPONSE_RECORD, +} + +PUBLIC_CLIENT_REQUEST_ARGS = { + "name": "FOO", + "public_client": True, + "project_id": str(uuid.uuid1()), + "publicly_visible": True, +} + +PUBLIC_CLIENT_REQUEST_BODY = { + "name": PUBLIC_CLIENT_REQUEST_ARGS["name"], + "public_client": PUBLIC_CLIENT_REQUEST_ARGS["public_client"], + "project": PUBLIC_CLIENT_REQUEST_ARGS["project_id"], + "visibility": "public" + if PUBLIC_CLIENT_REQUEST_ARGS["publicly_visible"] + else "private", +} + + +def register_response( + args: t.Mapping[str, t.Any], +) -> RegisteredResponse: + # Some name of args to create_client() have differenlty named fields. + body_fields: t.Dict[str, t.Any] = {} + for arg_name in args: + if arg_name == "project_id": + body_fields["project"] = args[arg_name] + elif arg_name == "publicly_visible": + body_fields["visibility"] = "public" if args[arg_name] else "private" + elif arg_name == "terms_and_conditions" or arg_name == "privacy_policy": + body_fields["links"] = { + arg_name: args[arg_name], + **body_fields.get("links", {}), + } + else: + body_fields[arg_name] = args[arg_name] + + # Default to a public client response unless arg says otherwise + client_response_record = ( + PUBLIC_CLIENT_RESPONSE_RECORD + if {**PUBLIC_CLIENT_REQUEST_ARGS, **args}["public_client"] is True + else PRIVATE_CLIENT_RESPONSE_RECORD + ) + + return RegisteredResponse( + service="auth", + method="POST", + path="/v2/api/clients", + json={"client": {**client_response_record, **body_fields}}, + metadata={ + # Test functions use 'args' to form request + "args": {**PUBLIC_CLIENT_REQUEST_ARGS, **args}, + # Test functions use 'response' to verify response + "response": body_fields, + }, + match=[ + json_params_matcher( + {"client": {**PUBLIC_CLIENT_REQUEST_BODY, **body_fields}} + ) + ], + ) + + +RESPONSES = ResponseSet( + default=register_response({}), + name=register_response({"name": str(uuid.uuid4()).replace("-", "")}), + public_client=register_response({"public_client": True}), + private_client=register_response({"public_client": False}), + project_id=register_response({"project_id": str(uuid.uuid1())}), + publicly_visible=register_response({"publicly_visible": True}), + not_publicly_visible=register_response({"publicly_visible": False}), + redirect_uris=register_response({"redirect_uris": ["https://foo.com"]}), + links=register_response( + { + "terms_and_conditions": "https://foo.org", + "privacy_policy": "https://boo.org", + } + ), + required_idp=register_response({"required_idp": str(uuid.uuid1())}), + preselect_idp=register_response({"preselect_idp": str(uuid.uuid1())}), +) diff --git a/src/globus_sdk/_testing/data/auth/create_client_credential.py b/src/globus_sdk/_testing/data/auth/create_client_credential.py new file mode 100644 index 000000000..19f43fd5b --- /dev/null +++ b/src/globus_sdk/_testing/data/auth/create_client_credential.py @@ -0,0 +1,43 @@ +import uuid + +from globus_sdk._testing.models import RegisteredResponse, ResponseSet + +NEW_CREDENTIAL_NAME = str(uuid.uuid4()).replace("-", "") + +CREDENTIAL = { + "name": "foo", + "id": str(uuid.uuid1()), + "created": "2023-10-21T22:46:15.845937+00:00", + "client": str(uuid.uuid1()), + "secret": "abc123", +} + + +RESPONSES = ResponseSet( + default=RegisteredResponse( + service="auth", + method="POST", + path=f"/v2/api/clients/{CREDENTIAL['client']}/credentials", + json={"credential": CREDENTIAL}, + metadata={ + "credential_id": CREDENTIAL["id"], + "client_id": CREDENTIAL["client"], + "name": CREDENTIAL["name"], + }, + ), + name=RegisteredResponse( + service="auth", + method="POST", + path=f"/v2/api/clients/{CREDENTIAL['client']}/credentials", + json={ + "credential": { + **CREDENTIAL, + "name": NEW_CREDENTIAL_NAME, + } + }, + metadata={ + "name": NEW_CREDENTIAL_NAME, + "client_id": CREDENTIAL["client"], + }, + ), +) diff --git a/src/globus_sdk/_testing/data/auth/create_native_app_instance.py b/src/globus_sdk/_testing/data/auth/create_native_app_instance.py new file mode 100644 index 000000000..cc9637ac2 --- /dev/null +++ b/src/globus_sdk/_testing/data/auth/create_native_app_instance.py @@ -0,0 +1,86 @@ +import typing as t +import uuid + +from responses.matchers import json_params_matcher + +from globus_sdk._testing.models import RegisteredResponse, ResponseSet + +APP_REQUEST_ARGS = { + "template_id": str(uuid.uuid1()), + "name": str(uuid.uuid1()).replace("-", ""), +} + + +def make_app_request_body(request_args: t.Dict[str, t.Any]) -> t.Dict[str, t.Any]: + request_body = request_args.copy() + request_body["template_id"] = str(request_args["template_id"]) + return request_body + + +def make_app_response_body(request_args: t.Mapping[str, t.Any]) -> t.Dict[str, t.Any]: + return { + "client": { + "fqdns": [], + "name": request_args["name"], + "id": "e634cc2a-d528-494e-8dda-92ec54a883c9", + "public_client": False, + "scopes": [], + "required_idp": None, + "grant_types": [ + "authorization_code", + "client_credentials", + "refresh_token", + ], + "userinfo_from_effective_identity": True, + "client_type": "confidential_client", + "prompt_for_named_grant": False, + "links": {"privacy_policy": None, "terms_and_conditions": None}, + "visibility": "private", + "preselect_idp": None, + "parent_client": str(request_args["template_id"]), + "project": None, + "redirect_uris": [], + }, + "included": { + "client_credential": { + "name": "Auto-created at client creation", + "id": "b4840855-2de8-4035-b1b4-4e7c8f518943", + "client": "e634cc2a-d528-494e-8dda-92ec54a883c9", + "secret": "cgK1HG9Y0DcZw79YlQEJpZCF4CMxIbaFf5sohWxjcfY=", + } + }, + } + + +def register_response( + args: t.Mapping[str, t.Any], +) -> RegisteredResponse: + request_args = {**APP_REQUEST_ARGS, **args} + request_body = make_app_request_body(request_args) + response_body = make_app_response_body(request_args) + + return RegisteredResponse( + service="auth", + method="POST", + path="/v2/api/clients", + json={"client": response_body}, + metadata={ + # Test functions use 'args' to form request + "args": request_args, + # Test functions use 'response' to verify response + "response": response_body, + }, + match=[ + json_params_matcher( + {"client": request_body}, + ) + ], + ) + + +RESPONSES = ResponseSet( + default=register_response({}), + template_id_str=register_response({"template_id": str(uuid.uuid1())}), + template_id_uuid=register_response({"template_id": uuid.uuid1()}), + name=register_response({"name": str(uuid.uuid1()).replace("-", "")}), +) diff --git a/src/globus_sdk/_testing/data/auth/create_policy.py b/src/globus_sdk/_testing/data/auth/create_policy.py new file mode 100644 index 000000000..ac2bcdf63 --- /dev/null +++ b/src/globus_sdk/_testing/data/auth/create_policy.py @@ -0,0 +1,100 @@ +import typing as t +import uuid + +from responses.matchers import json_params_matcher + +from globus_sdk._testing.models import RegisteredResponse, ResponseSet + +POLICY_REQUEST_ARGS = { + "project_id": str(uuid.uuid1()), + "high_assurance": False, + "authentication_assurance_timeout": 35, + "display_name": "Policy of Foo", + "description": "Controls access to Foo", +} + + +def make_request_body(request_args: t.Dict[str, t.Any]) -> t.Dict[str, t.Any]: + request_body = request_args.copy() + request_body["project_id"] = str(request_args["project_id"]) + + for domain_constraints in [ + "domain_constraints_include", + "domain_constraints_exclude", + ]: + if request_args.get(domain_constraints, None) is not None: + request_body[domain_constraints] = request_args[domain_constraints] + + return request_body + + +def make_response_body(request_args: t.Dict[str, t.Any]) -> t.Dict[str, t.Any]: + response_body = request_args.copy() + response_body["project_id"] = str(request_args["project_id"]) + response_body["id"] = str(uuid.uuid1()) + + for domain_constraints in [ + "domain_constraints_include", + "domain_constraints_exclude", + ]: + response_body[domain_constraints] = request_args.get(domain_constraints, None) + + return response_body + + +def register_response( + args: t.Mapping[str, t.Any], +) -> RegisteredResponse: + request_args = {**POLICY_REQUEST_ARGS, **args} + request_body = make_request_body(request_args) + response_body = make_response_body(request_args) + + return RegisteredResponse( + service="auth", + method="POST", + path="/v2/api/policies", + json={"policy": response_body}, + metadata={ + # Test functions use 'args' to form request + "args": request_args, + # Test functions use 'response' to verify response + "response": response_body, + }, + match=[json_params_matcher({"policy": request_body})], + ) + + +RESPONSES = ResponseSet( + default=register_response({}), + project_id_str=register_response({"project_id": str(uuid.uuid1())}), + project_id_uuid=register_response({"project_id": uuid.uuid1()}), + high_assurance=register_response({"high_assurance": True}), + not_high_assurance=register_response({"high_assurance": False}), + authentication_assurance_timeout=register_response( + {"authentication_assurance_timeout": 23} + ), + display_name=register_response( + {"display_name": str(uuid.uuid4()).replace("-", "")} + ), + description=register_response({"description": str(uuid.uuid4()).replace("-", "")}), + domain_constraints_include=register_response( + { + "domain_constraints_include": ["globus.org", "uchicago.edu"], + }, + ), + empty_domain_constraints_include=register_response( + { + "domain_constraints_include": [], + }, + ), + domain_constraints_exclude=register_response( + { + "domain_constraints_exclude": ["globus.org", "uchicago.edu"], + }, + ), + empty_domain_constraints_exclude=register_response( + { + "domain_constraints_exclude": [], + }, + ), +) diff --git a/src/globus_sdk/_testing/data/auth/create_scope.py b/src/globus_sdk/_testing/data/auth/create_scope.py new file mode 100644 index 000000000..be2bb4645 --- /dev/null +++ b/src/globus_sdk/_testing/data/auth/create_scope.py @@ -0,0 +1,111 @@ +import typing as t +import uuid + +from responses.matchers import json_params_matcher + +from globus_sdk import AuthClient +from globus_sdk._testing.models import RegisteredResponse, ResponseSet + +SCOPE_REQUEST_ARGS = { + "client_id": str(uuid.uuid1()), + "name": "Client manage scope", + "description": "Manage configuration of this client", + "scope_suffix": "manage", +} + + +def make_request_body(request_args: t.Mapping[str, t.Any]) -> t.Dict[str, t.Any]: + request_body = { + "name": request_args["name"], + "description": request_args["description"], + "scope_suffix": request_args["scope_suffix"], + "advertised": request_args.get("advertised", False), + "allows_refresh_token": request_args.get("allows_refresh_token", True), + } + + if request_args.get("required_domains", None) is not None: + request_body["required_domains"] = request_args["required_domains"] + + if request_args.get("dependent_scopes", None) is not None: + request_body["dependent_scopes"] = [ + { + "scope": str(ds.scope), + "optional": ds.optional, + "requires_refresh_token": ds.requires_refresh_token, + } + for ds in request_args["dependent_scopes"] + ] + + return request_body + + +def make_response_body(request_args: t.Mapping[str, t.Any]) -> t.Dict[str, t.Any]: + return { + "scope_string": f"https://auth.globus.org/scopes/{request_args['client_id']}/{request_args['scope_suffix']}", # noqa: E501 + "allows_refresh_token": request_args.get("allows_refresh_token", True), + "id": str(uuid.uuid1()), + "advertised": request_args.get("advertised", False), + "required_domains": request_args.get("required_domains", []), + "name": request_args["name"], + "description": request_args["description"], + "client": str(request_args["client_id"]), + "dependent_scopes": [ + { + "scope": str(ds.scope), + "optional": ds.optional, + "requires_refresh_token": ds.requires_refresh_token, + } + for ds in request_args.get("dependent_scopes", []) + ], + } + + +def register_response( + args: t.Mapping[str, t.Any], +) -> RegisteredResponse: + request_args = {**SCOPE_REQUEST_ARGS, **args} + request_body = make_request_body(request_args) + response_body = make_response_body(request_args) + + return RegisteredResponse( + service="auth", + method="POST", + path=f"/v2/api/clients/{request_args['client_id']}/scopes", + json={"scope": response_body}, + metadata={ + # Test functions use 'args' to form request + "args": request_args, + # Test functions use 'response' to verify response + "response": response_body, + }, + match=[json_params_matcher({"scope": request_body})], + ) + + +RESPONSES = ResponseSet( + default=register_response({}), + client_id_str=register_response({"client_id": str(uuid.uuid1())}), + client_id_uuid=register_response({"client_id": uuid.uuid1()}), + name=register_response({"name": str(uuid.uuid4()).replace("-", "")}), + description=register_response({"description": str(uuid.uuid4()).replace("-", "")}), + scope_suffix=register_response( + {"scope_suffix": str(uuid.uuid4()).replace("-", "")} + ), + advertised=register_response({"advertised": True}), + not_advertised=register_response({"advertised": False}), + allows_refresh_token=register_response({"allows_refresh_token": True}), + disallows_refresh_token=register_response({"allows_refresh_token": False}), + no_required_domains=register_response({"required_domains": []}), + required_domains=register_response( + {"required_domains": ["globus.org", "uchicago.edu"]} + ), + no_dependent_scopes=register_response({"dependent_scopes": []}), + dependent_scopes=register_response( + { + "dependent_scopes": [ + AuthClient.DependentScope(str(uuid.uuid1()), True, True), + AuthClient.DependentScope(uuid.uuid1(), False, False), + ], + } + ), +) diff --git a/src/globus_sdk/_testing/data/auth/delete_client.py b/src/globus_sdk/_testing/data/auth/delete_client.py new file mode 100644 index 000000000..6fb521d3d --- /dev/null +++ b/src/globus_sdk/_testing/data/auth/delete_client.py @@ -0,0 +1,34 @@ +import uuid + +from globus_sdk._testing.models import RegisteredResponse, ResponseSet + +CLIENT = { + "required_idp": None, + "name": "Great client of FOO", + "redirect_uris": [], + "links": {"privacy_policy": None, "terms_and_conditions": None}, + "scopes": [], + "grant_types": ["authorization_code", "client_credentials", "refresh_token"], + "id": str(uuid.uuid1()), + "prompt_for_named_grant": False, + "fqdns": ["globus.org"], + "project": "da84e531-1afb-43cb-8c87-135ab580516a", + "client_type": "client_identity", + "visibility": "private", + "parent_client": None, + "userinfo_from_effective_identity": True, + "preselect_idp": None, + "public_client": False, +} # type: ignore [var-annotated] + +RESPONSES = ResponseSet( + default=RegisteredResponse( + service="auth", + method="DELETE", + path=f"/v2/api/clients/{CLIENT['id']}", + json={"client": CLIENT}, + metadata={ + "client_id": CLIENT["id"], + }, + ), +) diff --git a/src/globus_sdk/_testing/data/auth/delete_client_credential.py b/src/globus_sdk/_testing/data/auth/delete_client_credential.py new file mode 100644 index 000000000..953739e96 --- /dev/null +++ b/src/globus_sdk/_testing/data/auth/delete_client_credential.py @@ -0,0 +1,24 @@ +import uuid + +from globus_sdk._testing.models import RegisteredResponse, ResponseSet + +CREDENTIAL = { + "name": "foo", + "id": str(uuid.uuid1()), + "created": "2023-10-21T22:46:15.845937+00:00", + "client": "7dee4432-0297-4989-ad23-a2b672a52b12", + "secret": None, +} + +RESPONSES = ResponseSet( + default=RegisteredResponse( + service="auth", + method="DELETE", + path=(f"/v2/api/clients/{CREDENTIAL['client']}/credentials/{CREDENTIAL['id']}"), + json={"credential": CREDENTIAL}, + metadata={ + "credential_id": CREDENTIAL["id"], + "client_id": CREDENTIAL["client"], + }, + ), +) diff --git a/src/globus_sdk/_testing/data/auth/delete_policy.py b/src/globus_sdk/_testing/data/auth/delete_policy.py new file mode 100644 index 000000000..c4bc6ad14 --- /dev/null +++ b/src/globus_sdk/_testing/data/auth/delete_policy.py @@ -0,0 +1,26 @@ +import uuid + +from globus_sdk._testing.models import RegisteredResponse, ResponseSet + +POLICY = { + "high_assurance": False, + "domain_constraints_include": ["greenlight.org"], + "display_name": "GreenLight domain Only Policy", + "description": "Only allow access from @greenlight.org", + "id": str(uuid.uuid1()), + "domain_constraints_exclude": None, + "project_id": "da84e531-1afb-43cb-8c87-135ab580516a", + "authentication_assurance_timeout": 35, +} + +RESPONSES = ResponseSet( + default=RegisteredResponse( + service="auth", + method="DELETE", + path=f"/v2/api/policies/{POLICY['id']}", + json={"policy": POLICY}, + metadata={ + "policy_id": POLICY["id"], + }, + ) +) diff --git a/src/globus_sdk/_testing/data/auth/delete_scope.py b/src/globus_sdk/_testing/data/auth/delete_scope.py new file mode 100644 index 000000000..67aaee1da --- /dev/null +++ b/src/globus_sdk/_testing/data/auth/delete_scope.py @@ -0,0 +1,27 @@ +import uuid + +from globus_sdk._testing.models import RegisteredResponse, ResponseSet + +SCOPE = { + "scope_string": "https://auth.globus.org/scopes/3f33d83f-ec0a-4190-887d-0622e7c4ee9a/manager", # noqa: E501 + "allows_refresh_token": False, + "id": str(uuid.uuid1()), + "advertised": False, + "required_domains": [], + "name": "Client manage scope", + "description": "Manage configuration of this client", + "client": "3f33d83f-ec0a-4190-887d-0622e7c4ee9a", + "dependent_scopes": [], +} + +RESPONSES = ResponseSet( + default=RegisteredResponse( + service="auth", + method="DELETE", + path=f"/v2/api/scopes/{SCOPE['id']}", + json={"scope": SCOPE}, + metadata={ + "scope_id": SCOPE["id"], + }, + ), +) diff --git a/src/globus_sdk/_testing/data/auth/get_client.py b/src/globus_sdk/_testing/data/auth/get_client.py new file mode 100644 index 000000000..581e80bd0 --- /dev/null +++ b/src/globus_sdk/_testing/data/auth/get_client.py @@ -0,0 +1,44 @@ +import uuid + +from globus_sdk._testing.models import RegisteredResponse, ResponseSet + +FQDN = "globus.org" + +CLIENT = { + "required_idp": None, + "name": "Great client of FOO", + "redirect_uris": [], + "links": {"privacy_policy": None, "terms_and_conditions": None}, + "scopes": [], + "grant_types": ["authorization_code", "client_credentials", "refresh_token"], + "id": str(uuid.uuid1()), + "prompt_for_named_grant": False, + "fqdns": [FQDN], + "project": "da84e531-1afb-43cb-8c87-135ab580516a", + "client_type": "client_identity", + "visibility": "private", + "parent_client": None, + "userinfo_from_effective_identity": True, + "preselect_idp": None, + "public_client": False, +} # type: ignore [var-annotated] + +RESPONSES = ResponseSet( + default=RegisteredResponse( + service="auth", + path=f"/v2/api/clients/{CLIENT['id']}", + json={"client": CLIENT}, + metadata={ + "client_id": CLIENT["id"], + }, + ), + fqdn=RegisteredResponse( + service="auth", + path="/v2/api/clients", + json={"client": CLIENT}, + metadata={ + "client_id": CLIENT["id"], + "fqdn": FQDN, + }, + ), +) diff --git a/src/globus_sdk/_testing/data/auth/get_client_credentials.py b/src/globus_sdk/_testing/data/auth/get_client_credentials.py new file mode 100644 index 000000000..a86bffb49 --- /dev/null +++ b/src/globus_sdk/_testing/data/auth/get_client_credentials.py @@ -0,0 +1,23 @@ +import uuid + +from globus_sdk._testing.models import RegisteredResponse, ResponseSet + +CREDENTIAL = { + "name": "foo", + "id": str(uuid.uuid1()), + "created": "2023-10-21T22:46:15.845937+00:00", + "client": "7dee4432-0297-4989-ad23-a2b672a52b12", + "secret": None, +} + +RESPONSES = ResponseSet( + default=RegisteredResponse( + service="auth", + path=f"/v2/api/clients/{CREDENTIAL['client']}/credentials", + json={"credentials": [CREDENTIAL]}, + metadata={ + "credential_id": CREDENTIAL["id"], + "client_id": CREDENTIAL["client"], + }, + ), +) diff --git a/src/globus_sdk/_testing/data/auth/get_clients.py b/src/globus_sdk/_testing/data/auth/get_clients.py new file mode 100644 index 000000000..3cdb0e46c --- /dev/null +++ b/src/globus_sdk/_testing/data/auth/get_clients.py @@ -0,0 +1,52 @@ +import uuid + +from globus_sdk._testing.models import RegisteredResponse, ResponseSet + +FOO_CLIENT = { + "required_idp": None, + "name": "Great client of FOO", + "redirect_uris": [], + "links": {"privacy_policy": None, "terms_and_conditions": None}, + "scopes": [], + "grant_types": ["authorization_code", "client_credentials", "refresh_token"], + "id": str(uuid.uuid1()), + "prompt_for_named_grant": False, + "fqdns": ["foo.net"], + "project": "da84e531-1afb-43cb-8c87-135ab580516a", + "client_type": "client_identity", + "visibility": "private", + "parent_client": None, + "userinfo_from_effective_identity": True, + "preselect_idp": None, + "public_client": False, +} # type: ignore [var-annotated] + +BAR_CLIENT = { + "required_idp": None, + "name": "Lessor client of BAR", + "redirect_uris": [], + "links": {"privacy_policy": None, "terms_and_conditions": None}, + "scopes": [], + "grant_types": ["authorization_code", "client_credentials", "refresh_token"], + "id": str(uuid.uuid1()), + "prompt_for_named_grant": False, + "fqdns": ["bar.org"], + "project": "da84e531-1afb-43cb-8c87-135ab580516a", + "client_type": "client_identity", + "visibility": "private", + "parent_client": None, + "userinfo_from_effective_identity": True, + "preselect_idp": None, + "public_client": False, +} # type: ignore [var-annotated] + +RESPONSES = ResponseSet( + default=RegisteredResponse( + service="auth", + path="/v2/api/clients", + json={"clients": [BAR_CLIENT, FOO_CLIENT]}, + metadata={ + "client_ids": [FOO_CLIENT["id"], BAR_CLIENT["id"]], + }, + ), +) diff --git a/src/globus_sdk/_testing/data/auth/get_policies.py b/src/globus_sdk/_testing/data/auth/get_policies.py new file mode 100644 index 000000000..a8b47bdb1 --- /dev/null +++ b/src/globus_sdk/_testing/data/auth/get_policies.py @@ -0,0 +1,36 @@ +import uuid + +from globus_sdk._testing.models import RegisteredResponse, ResponseSet + +GREEN_LIGHT_POLICY = { + "high_assurance": False, + "domain_constraints_include": ["greenlight.org"], + "display_name": "GreenLight domain Only Policy", + "description": "Only allow access from @greenlight.org", + "id": str(uuid.uuid1()), + "domain_constraints_exclude": None, + "project_id": "da84e531-1afb-43cb-8c87-135ab580516a", + "authentication_assurance_timeout": 35, +} + +RED_LIGHT_POLICY = { + "high_assurance": True, + "domain_constraints_include": None, + "display_name": "No RedLight domain Policy", + "description": "Disallow access from @redlight.org", + "id": str(uuid.uuid1()), + "domain_constraints_exclude": ["redlight.org"], + "project_id": "da84e531-1afb-43cb-8c87-135ab580516a", + "authentication_assurance_timeout": 35, +} + +RESPONSES = ResponseSet( + default=RegisteredResponse( + service="auth", + path="/v2/api/policies", + json={"policies": [GREEN_LIGHT_POLICY, RED_LIGHT_POLICY]}, + metadata={ + "policy_ids": [GREEN_LIGHT_POLICY["id"], RED_LIGHT_POLICY["id"]], + }, + ) +) diff --git a/src/globus_sdk/_testing/data/auth/get_policy.py b/src/globus_sdk/_testing/data/auth/get_policy.py new file mode 100644 index 000000000..f9c0a5b5b --- /dev/null +++ b/src/globus_sdk/_testing/data/auth/get_policy.py @@ -0,0 +1,25 @@ +import uuid + +from globus_sdk._testing.models import RegisteredResponse, ResponseSet + +POLICY = { + "high_assurance": False, + "domain_constraints_include": ["greenlight.org"], + "display_name": "GreenLight domain Only Policy", + "description": "Only allow access from @greenlight.org", + "id": str(uuid.uuid1()), + "domain_constraints_exclude": None, + "project_id": "da84e531-1afb-43cb-8c87-135ab580516a", + "authentication_assurance_timeout": 35, +} + +RESPONSES = ResponseSet( + default=RegisteredResponse( + service="auth", + path=f"/v2/api/policies/{POLICY['id']}", + json={"policy": POLICY}, + metadata={ + "policy_id": POLICY["id"], + }, + ) +) diff --git a/src/globus_sdk/_testing/data/auth/get_project.py b/src/globus_sdk/_testing/data/auth/get_project.py new file mode 100644 index 000000000..0306d9baa --- /dev/null +++ b/src/globus_sdk/_testing/data/auth/get_project.py @@ -0,0 +1,53 @@ +import uuid + +from globus_sdk._testing.models import RegisteredResponse, ResponseSet + +GUARDIANS_IDP_ID = str(uuid.uuid1()) +STAR_LORD = { + "identity_provider": GUARDIANS_IDP_ID, + "identity_type": "login", + "organization": "Guardians of the Galaxy", + "status": "used", + "id": str(uuid.uuid1()), + "name": "Star Lord", + "username": "star.lord@guardians.galaxy", + # I thought it would be funny if he didn't get 'star.lord' + # because someone else got it first + "email": "star.lord2@guardians.galaxy", +} +ROCKET_RACCOON = { + "identity_provider": GUARDIANS_IDP_ID, + "identity_type": "login", + "organization": "Guardians of the Galaxy", + "status": "used", + "id": str(uuid.uuid1()), + "name": "Rocket", + "username": "rocket@guardians.galaxy", + # and think about it, who else would try to lay claim + # to that email address? + "email": "star.lord@guardians.galaxy", +} + +GUARDIANS_PROJECT = { + "admin_ids": [ROCKET_RACCOON["id"]], + "contact_email": "support@guardians.galaxy", + "display_name": "Guardians of the Galaxy Portal", + "admin_group_ids": None, + "id": str(uuid.uuid1()), + "project_name": "Guardians of the Galaxy Portal", + "admins": { + "identities": [STAR_LORD, ROCKET_RACCOON], + "groups": [], + }, +} + +RESPONSES = ResponseSet( + default=RegisteredResponse( + service="auth", + path=f"/v2/api/projects/{GUARDIANS_PROJECT['id']}", + json={"project": GUARDIANS_PROJECT}, + metadata={ + "project_id": GUARDIANS_PROJECT["id"], + }, + ) +) diff --git a/src/globus_sdk/_testing/data/auth/get_scope.py b/src/globus_sdk/_testing/data/auth/get_scope.py new file mode 100644 index 000000000..e7bd40b2b --- /dev/null +++ b/src/globus_sdk/_testing/data/auth/get_scope.py @@ -0,0 +1,26 @@ +import uuid + +from globus_sdk._testing.models import RegisteredResponse, ResponseSet + +SCOPE = { + "scope_string": "https://auth.globus.org/scopes/3f33d83f-ec0a-4190-887d-0622e7c4ee9a/manager", # noqa: E501 + "allows_refresh_token": False, + "id": str(uuid.uuid1()), + "advertised": False, + "required_domains": [], + "name": "Client manage scope", + "description": "Manage configuration of this client", + "client": "3f33d83f-ec0a-4190-887d-0622e7c4ee9a", + "dependent_scopes": [], +} + +RESPONSES = ResponseSet( + default=RegisteredResponse( + service="auth", + path=f"/v2/api/scopes/{SCOPE['id']}", + json={"scope": SCOPE}, + metadata={ + "scope_id": SCOPE["id"], + }, + ), +) diff --git a/src/globus_sdk/_testing/data/auth/get_scopes.py b/src/globus_sdk/_testing/data/auth/get_scopes.py new file mode 100644 index 000000000..0b4e195a8 --- /dev/null +++ b/src/globus_sdk/_testing/data/auth/get_scopes.py @@ -0,0 +1,58 @@ +import uuid + +from responses.matchers import query_param_matcher + +from globus_sdk._testing.models import RegisteredResponse, ResponseSet + +SCOPE1 = { + "scope_string": "https://auth.globus.org/scopes/3f33d83f-ec0a-4190-887d-0622e7c4ee9a/manage", # noqa: E501 + "allows_refresh_token": False, + "id": str(uuid.uuid1()), + "advertised": False, + "required_domains": [], + "name": "Client manage scope", + "description": "Manage configuration of this client", + "client": "3f33d83f-ec0a-4190-887d-0622e7c4ee9a", + "dependent_scopes": [], +} + +SCOPE2 = { + "scope_string": "https://auth.globus.org/scopes/dfc9a6d3-3373-4a6d-b0a1-b7026d1559d6/view", # noqa: E501 + "allows_refresh_token": False, + "id": str(uuid.uuid1()), + "advertised": False, + "required_domains": [], + "name": "Client view scope", + "description": "View configuration of this client", + "client": "dfc9a6d3-3373-4a6d-b0a1-b7026d1559d6", + "dependent_scopes": [], +} + +RESPONSES = ResponseSet( + default=RegisteredResponse( + service="auth", + path="/v2/api/scopes", + json={"scopes": [SCOPE1, SCOPE2]}, + metadata={ + "scope_ids": [SCOPE1["id"], SCOPE2["id"]], + }, + ), + id=RegisteredResponse( + service="auth", + path="/v2/api/scopes", + json={"scopes": [SCOPE1]}, + match=[query_param_matcher(params={"ids": SCOPE1["id"]})], + metadata={ + "scope_id": SCOPE1["id"], + }, + ), + string=RegisteredResponse( + service="auth", + path="/v2/api/scopes", + json={"scopes": [SCOPE2]}, + match=[query_param_matcher(params={"scope_strings": SCOPE2["scope_string"]})], + metadata={ + "scope_string": SCOPE2["scope_string"], + }, + ), +) diff --git a/src/globus_sdk/_testing/data/auth/update_client.py b/src/globus_sdk/_testing/data/auth/update_client.py new file mode 100644 index 000000000..95b92d907 --- /dev/null +++ b/src/globus_sdk/_testing/data/auth/update_client.py @@ -0,0 +1,81 @@ +import typing as t +import uuid + +from responses.matchers import json_params_matcher + +from globus_sdk._testing.models import RegisteredResponse, ResponseSet + +_COMMON_RESPONSE_RECORD = { + "fqdns": [], + "links": {"privacy_policy": None, "terms_and_conditions": None}, + "parent_client": None, + "preselect_idp": None, + "prompt_for_named_grant": True, + "redirect_uris": [], + "required_idp": None, + "scopes": [], + "userinfo_from_effective_identity": True, +} + +PUBLIC_CLIENT_RESPONSE_RECORD = { + "client_type": "public_installed_client", + "grant_types": ["authorization_code", "refresh_token"], + **_COMMON_RESPONSE_RECORD, +} + + +def register_response( + args: t.Mapping[str, t.Any], +) -> RegisteredResponse: + # Some name of args to create_client() have differenlty named fields. + body_fields: t.Dict[str, t.Any] = {} + for arg_name in args: + if arg_name == "publicly_visible": + body_fields["visibility"] = "public" if args[arg_name] else "private" + elif arg_name == "terms_and_conditions" or arg_name == "privacy_policy": + body_fields["links"] = { + arg_name: args[arg_name], + **body_fields.get("links", {}), + } + else: + body_fields[arg_name] = args[arg_name] + + client_id = str(uuid.uuid1()) + + # Default to a public client response unless arg says otherwise + client_response_record = { + **PUBLIC_CLIENT_RESPONSE_RECORD, + **body_fields, + "id": client_id, + } + + return RegisteredResponse( + service="auth", + method="PUT", + path=f"/v2/api/clients/{client_id}", + json={"client": client_response_record}, + metadata={ + # Test functions use 'args' to form request + "args": {**args, "client_id": client_id}, + # Test functions use 'response' to verify response + "response": body_fields, + }, + match=[json_params_matcher({"client": body_fields})], + ) + + +RESPONSES = ResponseSet( + default=register_response({}), + name=register_response({"name": str(uuid.uuid4()).replace("-", "")}), + publicly_visible=register_response({"publicly_visible": True}), + not_publicly_visible=register_response({"publicly_visible": False}), + redirect_uris=register_response({"redirect_uris": ["https://foo.com"]}), + links=register_response( + { + "terms_and_conditions": "https://foo.org", + "privacy_policy": "https://boo.org", + } + ), + required_idp=register_response({"required_idp": str(uuid.uuid1())}), + preselect_idp=register_response({"preselect_idp": str(uuid.uuid1())}), +) diff --git a/src/globus_sdk/_testing/data/auth/update_policy.py b/src/globus_sdk/_testing/data/auth/update_policy.py new file mode 100644 index 000000000..48f3d6854 --- /dev/null +++ b/src/globus_sdk/_testing/data/auth/update_policy.py @@ -0,0 +1,105 @@ +import typing as t +import uuid + +from responses.matchers import json_params_matcher + +from globus_sdk._testing.models import RegisteredResponse, ResponseSet + +POLICY_REQUEST_ARGS = { + "policy_id": str(uuid.uuid1()), +} + + +def make_request_body(request_args: t.Mapping[str, t.Any]) -> t.Dict[str, t.Any]: + request_body = {} + + for field in [ + "authentication_assurance_timeout", + "display_name", + "description", + "domain_constraints_include", + "domain_constraints_exclude", + ]: + if field in request_args: + request_body[field] = request_args[field] + + if "project_id" in request_args: + request_body["project_id"] = str(request_args["project_id"]) + + return request_body + + +def make_response_body(request_args: t.Mapping[str, t.Any]) -> t.Dict[str, t.Any]: + return { + "project_id": str(request_args.get("project_id", uuid.uuid1())), + "high_assurance": request_args.get("high_assurance", True), + "authentication_assurance_timeout": request_args.get( + "authentication_assurance_timeout", 25 + ), + "display_name": request_args.get( + "display_name", str(uuid.uuid4()).replace("-", "") + ), + "description": request_args.get( + "description", str(uuid.uuid4()).replace("-", "") + ), + "domain_constraints_include": request_args.get( + "domain_constraints_include", None + ), + "domain_constraints_exclude": request_args.get( + "domain_constraints_exclude", None + ), + } + + +def register_response( + args: t.Mapping[str, t.Any], +) -> RegisteredResponse: + request_args = {**POLICY_REQUEST_ARGS, **args} + request_body = make_request_body(request_args) + response_body = make_response_body(request_args) + + return RegisteredResponse( + service="auth", + method="PUT", + path=f"/v2/api/policies/{request_args['policy_id']}", + json={"policy": response_body}, + metadata={ + # Test functions use 'args' to form request + "args": request_args, + # Test functions use 'response' to verify response + "response": response_body, + }, + match=[json_params_matcher({"policy": request_body})], + ) + + +RESPONSES = ResponseSet( + default=register_response({}), + project_id_str=register_response({"project_id": str(uuid.uuid1())}), + project_id_uuid=register_response({"project_id": uuid.uuid1()}), + authentication_assurance_timeout=register_response( + {"authentication_assurance_timeout": 9100} + ), + display_name=register_response( + {"display_name": str(uuid.uuid4()).replace("-", "")} + ), + description=register_response({"description": str(uuid.uuid4()).replace("-", "")}), + no_domain_constrants_include=register_response( + {"domain_constraints_include": None} + ), + empty_domain_constrants_include=register_response( + {"domain_constraints_include": []} + ), + domain_constrants_include=register_response( + {"domain_constraints_include": ["globus.org", "uchicago.edu"]} + ), + no_domain_constrants_exclude=register_response( + {"domain_constraints_exclude": None} + ), + empty_domain_constrants_exclude=register_response( + {"domain_constraints_exclude": []} + ), + domain_constrants_exclude=register_response( + {"domain_constraints_exclude": ["globus.org", "uchicago.edu"]} + ), +) diff --git a/src/globus_sdk/_testing/data/auth/update_scope.py b/src/globus_sdk/_testing/data/auth/update_scope.py new file mode 100644 index 000000000..985a1af83 --- /dev/null +++ b/src/globus_sdk/_testing/data/auth/update_scope.py @@ -0,0 +1,113 @@ +import typing as t +import uuid + +from responses.matchers import json_params_matcher + +from globus_sdk import AuthClient +from globus_sdk._testing.models import RegisteredResponse, ResponseSet + +SCOPE_REQUEST_ARGS = { + "scope_id": str(uuid.uuid1()), +} + + +def make_request_body(request_args: t.Mapping[str, t.Any]) -> t.Dict[str, t.Any]: + request_body = {} + + for field in [ + "name", + "description", + "scope_suffix", + "advertised", + "allows_refresh_token", + "required_domains", + ]: + if field in request_args and request_args[field] is not None: + request_body[field] = request_args[field] + + if request_args.get("dependent_scopes", None) is not None: + request_body["dependent_scopes"] = [ + { + "scope": str(ds.scope), + "optional": ds.optional, + "requires_refresh_token": ds.requires_refresh_token, + } + for ds in request_args["dependent_scopes"] + ] + + return request_body + + +def make_response_body(request_args: t.Mapping[str, t.Any]) -> t.Dict[str, t.Any]: + client_id = str(uuid.uuid1()) + scope_suffix = request_args.get("scope_suffix", str(uuid.uuid4()).replace("-", "")) + + return { + "scope_string": f"https://auth.globus.org/scopes/{client_id}/{scope_suffix}", + "allows_refresh_token": request_args.get("allows_refresh_token", True), + "id": request_args["scope_id"], + "advertised": request_args.get("advertised", False), + "required_domains": request_args.get("required_domains", []), + "name": request_args.get("name", str(uuid.uuid4()).replace("-", "")), + "description": request_args.get( + "description", str(uuid.uuid4()).replace("-", "") + ), + "client": str(request_args.get("client_id", uuid.uuid1())), + "dependent_scopes": [ + { + "scope": str(ds.scope), + "optional": ds.optional, + "requires_refresh_token": ds.requires_refresh_token, + } + for ds in request_args.get("dependent_scopes", []) + ], + } + + +def register_response( + args: t.Mapping[str, t.Any], +) -> RegisteredResponse: + request_args = {**SCOPE_REQUEST_ARGS, **args} + request_body = make_request_body(request_args) + response_body = make_response_body(request_args) + + return RegisteredResponse( + service="auth", + method="PUT", + path=f"/v2/api/scopes/{request_args['scope_id']}", + json={"scope": response_body}, + metadata={ + # Test functions use 'args' to form request + "args": request_args, + # Test functions use 'response' to verify response + "response": response_body, + }, + match=[json_params_matcher({"scope": request_body})], + ) + + +RESPONSES = ResponseSet( + default=register_response({}), + name=register_response({"name": str(uuid.uuid4()).replace("-", "")}), + description=register_response({"description": str(uuid.uuid4()).replace("-", "")}), + scope_suffix=register_response( + {"scope_suffix": str(uuid.uuid4()).replace("-", "")} + ), + no_required_domains=register_response({"required_domains": []}), + required_domains=register_response( + {"required_domains": ["globus.org", "uchicago.edu"]} + ), + no_dependent_scopes=register_response({"dependent_scopes": []}), + dependent_scopes=register_response( + { + "dependent_scopes": [ + AuthClient.DependentScope(str(uuid.uuid1()), True, True), + AuthClient.DependentScope(uuid.uuid1(), False, False), + ], + } + ), + advertised=register_response({"advertised": True}), + not_advertised=register_response({"advertised": False}), + allows_refresh_token=register_response({"allows_refresh_token": True}), + disallows_refresh_token=register_response({"allows_refresh_token": False}), +) diff --git a/src/globus_sdk/services/auth/client/confidential_client.py b/src/globus_sdk/services/auth/client/confidential_client.py index 5ea613185..78608e1da 100644 --- a/src/globus_sdk/services/auth/client/confidential_client.py +++ b/src/globus_sdk/services/auth/client/confidential_client.py @@ -294,3 +294,107 @@ def oauth2_token_introspect( if include is not None: body["include"] = include return self.post("/v2/oauth2/token/introspect", data=body, encoding="form") + + # TODO: It is a little strange to place this in this class but Auth treats + # client creation different based on how the client was authenticated. + # TODO: This is a copy service_client.create_client() minus project_id. + def create_child_client( + self, + name: str, + public_client: bool, + publicly_visible: bool, + *, + redirect_uris: t.Iterable[str] | None = None, + terms_and_conditions: str | None = None, + privacy_policy: str | None = None, + required_idp: UUIDLike | None = None, + preselect_idp: UUIDLike | None = None, + create_params: dict[str, t.Any] | None = None, + ) -> GlobusHTTPResponse: + """ + Create a new client. Requires the ``manage_projects`` scope. + + :param name: The display name shown to users on consents. May not contain + linebreaks. + :type name: str + :param public_client: This is used to infer which OAuth grant_types the client + will be able to use. Should be false if the client is capable of keeping + secret credentials (such as clients running on a server) and true if it is + not (such as native apps). After creation this value is immutable. + :type public_client: bool + :param publicly_visible: If True, any authenticated entity can view it. + Otherwise, only entities in the same project as the client can view it. + :type publicly_visible: bool + :param redirect_uris: list of URIs that may be used in OAuth authorization + flows. + :type redirect_uris: iterable of str, optional + :param terms_and_conditions: URL of client's terms and conditions. + :type terms_and_conditions: str, optional + :param privacy_policy: URL of client's privacy policy. + :type privacy_policy: str, optional + :param required_idp: In order to use this client a user must have an identity + from this IdP in their identity set. + :type required_idp: str or uuid, optional + :param preselect_idp: This pre-selects the given IdP on the Globus Auth login + page if the user is not already authenticated. + :type preselect_idp: str or uuid, optional + :param create_params: Any additional parameters to be passed through. + :type create_params: dict, optional + + + .. tab-set:: + + .. tab-item:: Example Usage + + .. code-block:: pycon + + >>> ac = globus_sdk.AuthClient(...) + >>> project_id = ... + >>> r = ac.create_child_client( + ... "My client", + ... True, + ... True, + ... ) + >>> client_id = r["client"]["id"] + + .. tab-item:: Example Response Data + + .. expandtestfixture:: auth.create_child_client + + .. tab-item:: API Info + + ``POST /v2/api/clients`` + + .. extdoclink:: Create Client + :ref: auth/reference/#create_client + """ + body: dict[str, t.Any] = { + "name": name, + "public_client": public_client, + "visibility": "public" if publicly_visible else "private", + } + if redirect_uris is not None: + body["redirect_uris"] = list(utils.safe_strseq_iter(redirect_uris)) + if required_idp is not None: + body["required_idp"] = str(required_idp) + if preselect_idp is not None: + body["preselect_idp"] = str(preselect_idp) + + # terms_and_conditions and privacy_policy must both be set or unset + if type(terms_and_conditions) is not type(privacy_policy): + raise exc.GlobusSDKUsageError( + "terms_and_conditions and privacy policy must both be set or unset" + ) + links: dict[str, str] = {} + if terms_and_conditions is not None: + links["terms_and_conditions"] = terms_and_conditions + if privacy_policy is not None: + links["privacy_policy"] = privacy_policy + + if links: + body["links"] = links + + if create_params is not None: + body.update(create_params) + + return self.post("/v2/api/clients", data={"client": body}) diff --git a/src/globus_sdk/services/auth/client/native_client.py b/src/globus_sdk/services/auth/client/native_client.py index b32c825dc..1d78f87bb 100644 --- a/src/globus_sdk/services/auth/client/native_client.py +++ b/src/globus_sdk/services/auth/client/native_client.py @@ -5,6 +5,7 @@ from globus_sdk._types import ScopeCollectionType, UUIDLike from globus_sdk.authorizers import NullAuthorizer +from globus_sdk.response import GlobusHTTPResponse from ..flow_managers import GlobusNativeAppFlowManager from ..response import OAuthTokenResponse @@ -139,3 +140,50 @@ def oauth2_refresh_token( "client_id": self.client_id, } return self.oauth2_token(form_data, body_params=body_params) + + # TODO: It is a little strange to place this in this class but Auth treats + # client creation different based on how the client was authenticated. + def create_native_app_instance( + self, + template_id: UUIDLike, + name: str, + ) -> GlobusHTTPResponse: + """ + Create a new native app instance. The new instance is a confidential client. + + :param template_id: The client ID of the calling native app + :type template_id: str or uuid + :param name: The name given to the new app instance + :type name: str + + .. tab-set:: + + .. tab-item:: Example Usage + + .. code-block:: pycon + + >>> ac = globus_sdk.NativeAppAuthClient(...) + >>> template_id = ... + >>> r = ac.create_native_app_instance( + ... template_id, + ... "My new native app instance", + ... ) + >>> client_id = r["client"]["id"] + + .. tab-item:: Example Response Data + + .. expandtestfixture:: auth.create_native_app_instance + + .. tab-item:: API Info + + ``POST /v2/api/clients`` + + .. extdoclink:: Create Client + :ref: auth/reference/#create_client + """ + body: dict[str, t.Any] = { + "name": name, + "template_id": str(template_id), + } + + return self.post("/v2/api/clients", data={"client": body}) diff --git a/src/globus_sdk/services/auth/client/service_client.py b/src/globus_sdk/services/auth/client/service_client.py index 62dafff0c..bc105d7f8 100644 --- a/src/globus_sdk/services/auth/client/service_client.py +++ b/src/globus_sdk/services/auth/client/service_client.py @@ -15,9 +15,13 @@ from .._common import get_jwk_data, pem_decode_jwk_data from ..errors import AuthAPIError from ..response import ( + GetClientCredentialsResponse, + GetClientsResponse, GetIdentitiesResponse, GetIdentityProvidersResponse, + GetPoliciesResponse, GetProjectsResponse, + GetScopesResponse, ) if sys.version_info >= (3, 8): @@ -424,6 +428,50 @@ def get_identity_providers( # Developer APIs # + def get_project(self, project_id: UUIDLike) -> GlobusHTTPResponse: + """ + Look up a project. Requires the ``manage_projects`` scope. + + :param project_id: The ID of the project to lookup + :type project_id: str or uuid + + .. tab-set:: + + .. tab-item:: Example Usage + + .. code-block:: pycon + + >>> ac = globus_sdk.AuthClient(...) + >>> r = ac.get_project("927d7238-f917-4eb2-9ace-c523fa9ba34e") + >>> r.data + { + 'project": { + 'admin_ids": ["41143743-f3c8-4d60-bbdb-eeecaba85bd9"] + 'contact_email": "support@globus.org", + 'display_name": "Globus SDK Demo Project", + 'admin_group_ids": None, + 'id": "927d7238-f917-4eb2-9ace-c523fa9ba34e", + 'project_name": "Globus SDK Demo Project", + 'admins": { + 'identities": ["41143743-f3c8-4d60-bbdb-eeecaba85bd9"], + 'groups": [], + }, + } + } + + .. tab-item:: Example Response Data + + .. expandtestfixture:: auth.get_project + + .. tab-item:: API Info + + ``GET /v2/api/projects/{project_id}`` + + .. extdoclink:: Get Projects + :ref: auth/reference/#get_projects + """ + return self.get(f"/v2/api/projects/{project_id}") + def get_projects(self) -> IterableResponse: """ Look up projects on which the authenticated user is an admin. @@ -451,6 +499,7 @@ def get_projects(self) -> IterableResponse: 'identities": ["41143743-f3c8-4d60-bbdb-eeecaba85bd9"], 'groups": [], }, + } ] } @@ -631,3 +680,1187 @@ def delete_project(self, project_id: UUIDLike) -> GlobusHTTPResponse: :ref: auth/reference/#delete_project """ return self.delete(f"/v2/api/projects/{project_id}") + + def get_policy(self, policy_id: UUIDLike) -> GlobusHTTPResponse: + """ + Look up a policy. Requires the ``manage_projects`` scope. + + :param policy_id: The ID of the policy to lookup + :type policy_id: str or uuid + + .. tab-set:: + + .. tab-item:: Example Usage + + .. code-block:: pycon + + >>> ac = globus_sdk.AuthClient(...) + >>> r = ac.get_policy("f5eaae7e-807f-41be-891a-ec86ff88df8f") + >>> r.data + { + 'policy": { + 'high_assurance": False, + 'domain_constraints_include": ["globus.org"], + 'display_name": "Display Name", + 'description": "Description", + 'id": "f5eaae7e-807f-41be-891a-ec86ff88df8f", + 'domain_constraints_exclude": None, + 'project_id": "da84e531-1afb-43cb-8c87-135ab580516a", + 'authentication_assurance_timeout": 35 + } + } + + .. tab-item:: Example Response Data + + .. expandtestfixture:: auth.get_policy + + .. tab-item:: API Info + + ``GET /v2/api/policies/{policy_id}`` + + .. extdoclink:: Get Policies + :ref: auth/reference/#get_policies + """ + return self.get(f"/v2/api/policies/{policy_id}") + + def get_policies(self) -> IterableResponse: + """ + Look up policies in projects on which the authenticated user is an admin. + Requires the ``manage_projects`` scope. + + .. tab-set:: + + .. tab-item:: Example Usage + + .. code-block:: pycon + + >>> ac = globus_sdk.AuthClient(...) + >>> r = ac.get_policies() + >>> r.data + { + 'policies": [ + { + 'high_assurance": False, + 'domain_constraints_include": ["greenlight.org"], + 'display_name": "GreenLight domain Only Policy", + 'description": "Only allow access from @greenlight.org", + 'id": "99d2dc75-3acb-48ff-b5e5-2eee0a5121d1", + 'domain_constraints_exclude": None, + 'project_id": "da84e531-1afb-43cb-8c87-135ab580516a", + 'authentication_assurance_timeout": 35, + }, + { + 'high_assurance": True, + 'domain_constraints_include": None, + 'display_name": "No RedLight domain Policy", + 'description": "Disallow access from @redlight.org", + 'id": "5d93ebf0-b4c6-4928-9929-4ac47fc2786d", + 'domain_constraints_exclude": ["redlight.org"], + 'project_id": "da84e531-1afb-43cb-8c87-135ab580516a", + 'authentication_assurance_timeout": 35, + } + ] + } + + .. tab-item:: Example Response Data + + .. expandtestfixture:: auth.get_policies + + .. tab-item:: API Info + + ``GET /v2/api/policies`` + + .. extdoclink:: Get Policies + :ref: auth/reference/#get_policies + """ + return GetPoliciesResponse(self.get("/v2/api/policies")) + + def create_policy( + self, + project_id: UUIDLike, + high_assurance: bool, + authentication_assurance_timeout: int, + display_name: str, + description: str, + *, + domain_constraints_include: t.Iterable[str] | None = None, + domain_constraints_exclude: t.Iterable[str] | None = None, + ) -> GlobusHTTPResponse: + """ + Create a new Auth policy. Requires the ``manage_projects`` scope. + + :param project_id: ID of the project for the new policy + :type project_id: str or uuid + :param high_assurance: Whether or not this policy is applied to sessions. + :type high_assurance: bool + :param authentication_assurance_timeout: Number of seconds within which someone + must have authenticated to satisfy the policy + :type authentication_assurance_timeout: int + :param display_name: A user-friendly name for the policy + :type display_name: str + :param description: A user-friendly description to explain the purpose of the + policy + :type description: str + :param domain_constraints_include: A list of domains that can satisfy the policy + :type domain_constraints_include: iterable of str + :param domain_constraints_exclude: A list of domains that can not satisfy the + policy + :type domain_constraints_exclude: iterable of str + + .. tab-set:: + + .. tab-item:: Example Usage + + .. code-block:: pycon + + >>> ac = globus_sdk.AuthClient(...) + >>> client_id = ... + >>> r = ac.create_policy( + ... project_id="da84e531-1afb-43cb-8c87-135ab580516a", + ... high_assurance=True, + ... authentication_assurance_timeout=35, + ... display_name="No RedLight domain Policy", + ... description="Disallow access from @redlight.org", + ... domain_constraints_exclude=["redlight.org"], + ... ) + >>> policy_id = r["policy"]["id"] + + .. tab-item:: Example Response Data + + .. expandtestfixture:: auth.create_policy + + .. tab-item:: API Info + + ``POST /v2/api/policies`` + + .. extdoclink:: Create Policy + :ref: auth/reference/#create_policy + """ + body: dict[str, t.Any] = { + "project_id": str(project_id), + "high_assurance": high_assurance, + "authentication_assurance_timeout": authentication_assurance_timeout, + "display_name": display_name, + "description": description, + } + + if domain_constraints_include is not None: + body["domain_constraints_include"] = list(domain_constraints_include) + if domain_constraints_exclude is not None: + body["domain_constraints_exclude"] = list(domain_constraints_exclude) + return self.post("/v2/api/policies", data={"policy": body}) + + # TODO: Some API calls accept 'None' (null) so need to distinguish in update calls + # from when the caller did not provide a value. + # - update_policy(): + # - domain_constraints_include=[] != domain_constriants_include=None + # - update_client(): + # - terms_and_conditions=None + # - privacy_policy=None + # - required_idp=None + # - preselect_idp=None + class _DefaultNull: + ... + + _DEFAULT_NULL = _DefaultNull() + + def update_policy( + self, + policy_id: UUIDLike, + *, + project_id: UUIDLike | None = None, + authentication_assurance_timeout: int | None = None, + display_name: str | None = None, + description: str | None = None, + # TODO: include=null is not the same as include=[] + domain_constraints_include: t.Iterable[str] + | None + | _DefaultNull = _DEFAULT_NULL, + domain_constraints_exclude: t.Iterable[str] + | None + | _DefaultNull = _DEFAULT_NULL, + ) -> GlobusHTTPResponse: + """ + Update a policy. Requires the ``manage_projects`` scope. + + :param policy_id: ID of the policy to update + :type policy_id: str or uuid + + :param project_id: ID of the project for the new policy + :type project_id: str or uuid + :param authentication_assurance_timeout: Number of seconds within which someone + must have authenticated to satisfy the policy + :type authentication_assurance_timeout: int + :param display_name: A user-friendly name for the policy + :type display_name: str + :param description: A user-friendly description to explain the purpose of the + policy + :type description: str + :param domain_constraints_include: A list of domains that can satisfy the policy + :type domain_constraints_include: iterable of str + :param domain_constraints_exclude: A list of domains that can not satisfy the + policy + :type domain_constraints_exclude: iterable of str + + .. tab-set:: + + .. tab-item:: Example Usage + + .. code-block:: pycon + + >>> ac = globus_sdk.AuthClient(...) + >>> policy_id = ... + >>> r = ac.update_policy(scope_id, display_name="Greenlight Policy") + + .. tab-item:: Example Response Data + + .. expandtestfixture:: auth.update_policy + + .. tab-item:: API Info + + ``POST /v2/api/policies/{policy_id}`` + + .. extdoclink:: Update Policy + :ref: auth/reference/#update_policy + """ + body: dict[str, t.Any] = {} + + if project_id is not None: + body["project_id"] = str(project_id) + if authentication_assurance_timeout is not None: + body["authentication_assurance_timeout"] = authentication_assurance_timeout + if display_name is not None: + body["display_name"] = display_name + if description is not None: + body["description"] = description + + if domain_constraints_include is not self._DEFAULT_NULL: + body["domain_constraints_include"] = ( + None + if domain_constraints_include is None + else list(domain_constraints_include) # type: ignore [arg-type] + ) + + if domain_constraints_exclude is not self._DEFAULT_NULL: + body["domain_constraints_exclude"] = ( + None + if domain_constraints_exclude is None + else list(domain_constraints_exclude) # type: ignore [arg-type] + ) + + return self.put(f"/v2/api/policies/{policy_id}", data={"policy": body}) + + def delete_policy(self, policy_id: UUIDLike) -> GlobusHTTPResponse: + """ + Delete a policy. Requires the ``manage_projects`` scope. + + :param policy_id: The ID of the policy to delete + :type policy_id: str or uuid + + .. tab-set:: + + .. tab-item:: Example Usage + + .. code-block:: pycon + + >>> ac = globus_sdk.AuthClient(...) + >>> policy_id = ... + >>> r = ac.delete_policy(policy_id) + + .. tab-item:: Example Response Data + + .. expandtestfixture:: auth.delete_policy + + .. tab-item:: API Info + + ``DELETE /v2/api/policies/{policy_id}`` + + .. extdoclink:: Delete Policy + :ref: auth/reference/#delete_policy + """ + return self.delete(f"/v2/api/policies/{policy_id}") + + def get_client( + self, + *, + client_id: UUIDLike | None = None, + fqdn: str | None = None, + ) -> GlobusHTTPResponse: + """ + Look up a client by ``client_id`` or (exclusive) by ``fqdn``. + Requires the ``manage_projects`` scope. + + :param client_id: The ID of the client to look up + :type client_id: str or uuid + :param fqdn: The fully-qualified domain name of the client to look up + :type fqdn: str + + .. tab-set:: + + .. tab-item:: Example Usage + + .. code-block:: pycon + + >>> ac = globus_sdk.AuthClient(...) + >>> # by client_id + >>> r = ac.get_client(client_id="6336437e-37e8-4559-82a8-674390c1fd2e") + >>> r.data + { + 'client": { + 'required_idp": None, + 'name": "Great client of FOO", + 'redirect_uris": [], + 'links": { + 'privacy_policy": None, + 'terms_and_conditions": None + }, + 'scopes": [], + 'grant_types": [ + 'authorization_code", + 'client_credentials", + 'refresh_token" + ], + 'id": "6336437e-37e8-4559-82a8-674390c1fd2e", + 'prompt_for_named_grant": False, + 'fqdns": ["globus.org"], + 'project": "da84e531-1afb-43cb-8c87-135ab580516a", + 'client_type": "client_identity", + 'visibility": "private", + 'parent_client": None, + 'userinfo_from_effective_identity": True, + 'preselect_idp": None, + 'public_client": False + } + } + >>> # by fqdn + >>> fqdn = ... + >>> r = ac.get_client(fqdn=fqdn) + + .. tab-item:: Example Response Data + + .. expandtestfixture:: auth.get_client + + .. tab-item:: API Info + + ``GET /v2/api/clients/{client_id}`` + ``GET /v2/api/clients?fqdn={fqdn}`` + + .. extdoclink:: Get Clients + :ref: auth/reference/#get_clients + """ # noqa: E501 + if client_id is not None and fqdn is not None: + raise exc.GlobusSDKUsageError( + "AuthClient.get_client does not take both " + "'client_id' and 'fqdn'. These are mutually exclusive." + ) + + if client_id is None and fqdn is None: + raise exc.GlobusSDKUsageError( + "AuthClient.get_client requires either 'client_id' or 'fqdn'." + ) + + if client_id is not None: + return self.get(f"/v2/api/clients/{client_id}") + return self.get("/v2/api/clients", query_params={"fqdn": fqdn}) + + def get_clients(self) -> IterableResponse: + """ + Look up clients in projects on which the authenticated user is an admin. + Requires the ``manage_projects`` scope. + + .. tab-set:: + + .. tab-item:: Example Usage + + .. code-block:: pycon + + >>> ac = globus_sdk.AuthClient(...) + >>> r = ac.get_clients() + >>> r.data + { + 'clients": [ + { + "required_idp": None, + "name": "Great client of FOO", + "redirect_uris": [], + "links": {"privacy_policy": None, "terms_and_conditions": None}, + "scopes": [], + "grant_types": ["authorization_code", "client_credentials", "refresh_token"], + "id": "b6001d11-8765-49d3-a503-ba323fc74eee", + "prompt_for_named_grant": False, + "fqdns": ["foo.net"], + "project": "da84e531-1afb-43cb-8c87-135ab580516a", + "client_type": "client_identity", + "visibility": "private", + "parent_client": None, + "userinfo_from_effective_identity": True, + "preselect_idp": None, + "public_client": False, + }, + { + "required_idp": None, + "name": "Lessor client of BAR", + "redirect_uris": [], + "links": {"privacy_policy": None, "terms_and_conditions": None}, + "scopes": [], + "grant_types": ["authorization_code", "client_credentials", "refresh_token"], + "id": "b87f7415-ddf9-4868-8e55-d10c065f733d", + "prompt_for_named_grant": False, + "fqdns": ["bar.org"], + "project": "da84e531-1afb-43cb-8c87-135ab580516a", + "client_type": "client_identity", + "visibility": "private", + "parent_client": None, + "userinfo_from_effective_identity": True, + "preselect_idp": None, + "public_client": False, + } + ] + } + + .. tab-item:: Example Response Data + + .. expandtestfixture:: auth.get_clients + + .. tab-item:: API Info + + ``GET /v2/api/clients`` + + .. extdoclink:: Get Clients + :ref: auth/reference/#get_clients + """ # noqa: E501 + return GetClientsResponse(self.get("/v2/api/clients")) + + def create_client( + self, + name: str, + public_client: bool, + project_id: UUIDLike, + publicly_visible: bool, + *, + redirect_uris: t.Iterable[str] | None = None, + terms_and_conditions: str | None = None, + privacy_policy: str | None = None, + required_idp: UUIDLike | None = None, + preselect_idp: UUIDLike | None = None, + create_params: dict[str, t.Any] | None = None, + ) -> GlobusHTTPResponse: + """ + Create a new client. Requires the ``manage_projects`` scope. + + :param name: The display name shown to users on consents. May not contain + linebreaks. + :type name: str + :param public_client: This is used to infer which OAuth grant_types the client + will be able to use. Should be false if the client is capable of keeping + secret credentials (such as clients running on a server) and true if it is + not (such as native apps). After creation this value is immutable. + :type public_client: bool + :param project_id: ID representing the project this client belongs to. + :type project_id: str or uuid + :param publicly_visible: If True, any authenticated entity can view it. + Otherwise, only entities in the same project as the client can view it. + :type publicly_visible: bool + :param redirect_uris: list of URIs that may be used in OAuth authorization + flows. + :type redirect_uris: iterable of str, optional + :param terms_and_conditions: URL of client's terms and conditions. + :type terms_and_conditions: str, optional + :param privacy_policy: URL of client's privacy policy. + :type privacy_policy: str, optional + :param required_idp: In order to use this client a user must have an identity + from this IdP in their identity set. + :type required_idp: str or uuid, optional + :param preselect_idp: This pre-selects the given IdP on the Globus Auth login + page if the user is not already authenticated. + :type preselect_idp: str or uuid, optional + :param create_params: Any additional parameters to be passed through. + :type create_params: dict, optional + + + .. tab-set:: + + .. tab-item:: Example Usage + + .. code-block:: pycon + + >>> ac = globus_sdk.AuthClient(...) + >>> project_id = ... + >>> r = ac.create_client( + ... "My client", + ... True, + ... project_id, + ... True, + ... ) + >>> client_id = r["client"]["id"] + + .. tab-item:: Example Response Data + + .. expandtestfixture:: auth.create_client + + .. tab-item:: API Info + + ``POST /v2/api/clients`` + + .. extdoclink:: Create Client + :ref: auth/reference/#create_client + """ + body: dict[str, t.Any] = { + "name": name, + "public_client": public_client, + "visibility": "public" if publicly_visible else "private", + "project": project_id, + } + if redirect_uris is not None: + body["redirect_uris"] = list(utils.safe_strseq_iter(redirect_uris)) + if required_idp is not None: + body["required_idp"] = str(required_idp) + if preselect_idp is not None: + body["preselect_idp"] = str(preselect_idp) + + # terms_and_conditions and privacy_policy must both be set or unset + if type(terms_and_conditions) is not type(privacy_policy): + raise exc.GlobusSDKUsageError( + "terms_and_conditions and privacy policy must both be set or unset" + ) + links: dict[str, str] = {} + if terms_and_conditions is not None: + links["terms_and_conditions"] = terms_and_conditions + if privacy_policy is not None: + links["privacy_policy"] = privacy_policy + + if links: + body["links"] = links + + if create_params is not None: + body.update(create_params) + + return self.post("/v2/api/clients", data={"client": body}) + + def update_client( + self, + client_id: UUIDLike, + *, + name: str | None = None, + publicly_visible: bool | None = None, + redirect_uris: t.Iterable[str] | None = None, + terms_and_conditions: str | None | _DefaultNull = _DEFAULT_NULL, + privacy_policy: str | None | _DefaultNull = _DEFAULT_NULL, + required_idp: UUIDLike | None | _DefaultNull = _DEFAULT_NULL, + preselect_idp: UUIDLike | None | _DefaultNull = _DEFAULT_NULL, + create_params: dict[str, t.Any] | None = None, + ) -> GlobusHTTPResponse: + """ + Update a client. Requires the ``manage_projects`` scope. + + :param client_id: ID of the client to update + :type client_id: str or uuid + :param name: The display name shown to users on consents. May not contain + linebreaks. + :type name: str + :param publicly_visible: If True, any authenticated entity can view it. + Otherwise, only entities in the same project as the client can view it. + :type publicly_visible: bool + :param redirect_uris: list of URIs that may be used in OAuth authorization + flows. + :type redirect_uris: iterable of str + :param terms_and_conditions: URL of client's terms and conditions. + :type terms_and_conditions: str + :param privacy_policy: URL of client's privacy policy. + :type privacy_policy: str + :param required_idp: In order to use this client a user must have an identity + from this IdP in their identity set. + :type required_idp: str or uuid + :param preselect_idp: This pre-selects the given IdP on the Globus Auth login + page if the user is not already authenticated. + :type preselect_idp: str or uuid + :param create_params: Any additional parameters to be passed through. + :type create_params: dict + + + .. tab-set:: + + .. tab-item:: Example Usage + + When creating a project, your account is not necessarily included as an + admin. The following snippet uses the ``manage_projects`` scope as well + as the ``openid`` and ``email`` scopes to get the current user ID and + email address and use those data to setup the project. + + .. code-block:: pycon + + >>> ac = globus_sdk.AuthClient(...) + >>> client_id = ... + >>> r = ac.create_upadte(client_id, name="Foo Utility") + + .. tab-item:: Example Response Data + + .. expandtestfixture:: auth.update_client + + .. tab-item:: API Info + + ``POST /v2/api/clients/{client_id}`` + + .. extdoclink:: Update Client + :ref: auth/reference/#update_client + """ + body: dict[str, t.Any] = {} + + if name is not None: + body["name"] = name + if publicly_visible is not None: + body["visibility"] = "public" if publicly_visible else "private" + if redirect_uris is not None: + body["redirect_uris"] = list(redirect_uris) + if required_idp is not self._DEFAULT_NULL: + body["required_idp"] = None if required_idp is None else str(required_idp) + if preselect_idp is not self._DEFAULT_NULL: + body["preselect_idp"] = None if required_idp is None else str(preselect_idp) + + # terms_and_conditions and privacy_policy must both be set or unset + if type(terms_and_conditions) is not type(privacy_policy): + raise exc.GlobusSDKUsageError( + "terms_and_conditions and privacy policy must both be set or unset" + ) + + links: dict[str, str] = {} + if terms_and_conditions is not self._DEFAULT_NULL: + links["terms_and_conditions"] = ( + terms_and_conditions # type: ignore [assignment] + if terms_and_conditions is not None + else None + ) + if privacy_policy is not self._DEFAULT_NULL: + links["privacy_policy"] = ( + privacy_policy # type: ignore [assignment] + if privacy_policy is not None + else None + ) + + if links: + body["links"] = links + + if create_params is not None: + body.update(create_params) + + return self.put(f"/v2/api/clients/{client_id}", data={"client": body}) + + def delete_client(self, client_id: UUIDLike) -> GlobusHTTPResponse: + """ + Delete a client. Requires the ``manage_projects`` scope. + + :param client_id: The ID of the client to delete + :type client_id: str or uuid + + .. tab-set:: + + .. tab-item:: Example Usage + + .. code-block:: pycon + + >>> ac = globus_sdk.AuthClient(...) + >>> client_id = ... + >>> r = ac.delete_policy(client_id) + + .. tab-item:: Example Response Data + + .. expandtestfixture:: auth.delete_client + + .. tab-item:: API Info + + ``DELETE /v2/api/clients/{client_id}`` + + .. extdoclink:: Delete Client + :ref: auth/reference/#delete_client + """ + return self.delete(f"/v2/api/clients/{client_id}") + + def get_client_credentials(self, client_id: UUIDLike) -> IterableResponse: + """ + Look up client credentials by ``client_id``. Requires the + ``manage_projects`` scope. + + :param client_id: The ID of the client that owns the credentials + :type client_id: str or uuid + + .. tab-set:: + + .. tab-item:: Example Usage + + .. code-block:: pycon + + >>> ac = globus_sdk.AuthClient(...) + >>> r = ac.get_credentials("6336437e-37e8-4559-82a8-674390c1fd2e") + >>> r.data + { + 'credentials": [ + 'name": "foo", + 'id": "cf88318e-b2dd-43fd-8ea5-2086fc69ffac", + 'created": "2023-10-21T22:46:15.845937+00:00", + 'client": "6336437e-37e8-4559-82a8-674390c1fd2e", + 'secret": None, + ] + } + + .. tab-item:: Example Response Data + + .. expandtestfixture:: auth.get_client_credentials + + .. tab-item:: API Info + + ``GET /v2/api/clients/{client_id}/credentials`` + + .. extdoclink:: Get Client Credentials + :ref: auth/reference/#get_client_credentials + """ # noqa: E501 + return GetClientCredentialsResponse( + self.get(f"/v2/api/clients/{client_id}/credentials") + ) + + def create_client_credential( + self, + client_id: UUIDLike, + name: str, + ) -> GlobusHTTPResponse: + """ + Create a new client credential. Requires the ``manage_projects`` scope. + + :param client_id: ID for the client + :type client_id: str or uuid + :param name: The display name of the new credential. + :type name: str + + .. tab-set:: + + .. tab-item:: Example Usage + + .. code-block:: pycon + + >>> ac = globus_sdk.AuthClient(...) + >>> client_id = ... + >>> name = ... + >>> r = ac.create_client_credential( + ... "25afc56d-02af-4175-8c90-9941ebb623dd", + ... "New Credentials", + ... ) + >>> r.data + { + "name": "New Credentials", + "id": "3a53cb4d-edd6-4ae3-900e-25b38b5fce02", + "created": "2023-10-21T22:46:15.845937+00:00", + "client": "25afc56d-02af-4175-8c90-9941ebb623dd", + "secret": "abc123", + } + + .. tab-item:: Example Response Data + + .. expandtestfixture:: auth.create_client_credential + + .. tab-item:: API Info + + ``POST /v2/api/clients/{client_id}/credentials`` + + .. extdoclink:: Create Client Credentials + :ref: auth/reference/#create_client_credential + """ + return self.post( + f"/v2/api/clients/{client_id}/credentials", + data={"credential": {"name": name}}, + ) + + def delete_client_credential( + self, + client_id: UUIDLike, + credential_id: UUIDLike, + ) -> GlobusHTTPResponse: + """ + Delete a credential. Requires the ``manage_projects`` scope. + + :param client_id: The ID of the client that owns the credential to delete + :type client_id: str or uuid + :param credential_id: The ID of the credential to delete + :type credential_id: str or uuid + + .. tab-set:: + + .. tab-item:: Example Usage + + .. code-block:: pycon + + >>> ac = globus_sdk.AuthClient(...) + >>> client_id = ... + >>> credential_id = ... + >>> r = ac.delete_policy(client_id, credential_id) + + .. tab-item:: Example Response Data + + .. expandtestfixture:: auth.delete_client_credential + + .. tab-item:: API Info + + ``DELETE /v2/api/clients/{client_id}/credentials/{credential_id}`` + + .. extdoclink:: Delete Credential + :ref: auth/reference/#delete_client_credentials + """ + return self.delete(f"/v2/api/clients/{client_id}/credentials/{credential_id}") + + # Is this affected by authorization type? + def get_scope(self, scope_id: UUIDLike) -> GlobusHTTPResponse: + """ + Look up a scope by ``scope_id``. Requires the ``manage_projects`` scope. + + :param scope_id: The ID of the scope to look up + :type scope_id: str or uuid + + .. tab-set:: + + .. tab-item:: Example Usage + + .. code-block:: pycon + + >>> ac = globus_sdk.AuthClient(...) + >>> r = ac.get_scope(scope_id="6336437e-37e8-4559-82a8-674390c1fd2e") + >>> r.data + { + 'scope": { + 'scope_string": "https://auth.globus.org/scopes/3f33d83f-ec0a-4190-887d-0622e7c4ee9a/manager", + 'allows_refresh_token": False, + 'id": "87cf7b34-e1e1-4805-a8d5-51ab59fe6000", + 'advertised": False, + 'required_domains": [], + 'name": "Client manage scope", + 'description": "Manage configuration of this client", + 'client": "3f33d83f-ec0a-4190-887d-0622e7c4ee9a", + 'dependent_scopes": [], + } + } + + .. tab-item:: Example Response Data + + .. expandtestfixture:: auth.get_scope + + .. tab-item:: API Info + + ``GET /v2/api/scopes/{scope_id}`` + + .. extdoclink:: Get Scopes + :ref: auth/reference/#get_scopes + """ # noqa: E501 + return self.get(f"/v2/api/scopes/{scope_id}") + + def get_scopes( + self, + *, + scope_strings: t.Iterable[str] | str | None = None, + ids: t.Iterable[UUIDLike] | UUIDLike | None = None, + query_params: dict[str, t.Any] | None = None, + ) -> IterableResponse: + """ + Look up scopes in projects on which the authenticated user is an admin. + The scopes response can be filted by ``scope_strings`` or (exclusive) + ``ids``. Requires the ``manage_projects`` scope. + + :param scope_strings: The scope_strings of the scopes to look up + :type scope_strings: iterable of str + :param ids: The ID of the scopes to look up + :type ids: iterable of str or uuid + :param query_params: Any additional parameters to be passed through + as query params. + :type query_params: dict, optional + + .. tab-set:: + + .. tab-item:: Example Usage + + .. code-block:: pycon + + >>> ac = globus_sdk.AuthClient(...) + >>> # get all scopes + >>> r = ac.get_scopes() + >>> r.data + { + 'scopes": [ + { + "scope_string": "https://auth.globus.org/scopes/3f33d83f-ec0a-4190-887d-0622e7c4ee9a/manage", + "allows_refresh_token": False, + "id": "70147193-f88a-4da9-9d6e-677c15e790e5", + "advertised": False, + "required_domains": [], + "name": "Client manage scope", + "description": "Manage configuration of this client", + "client": "3f33d83f-ec0a-4190-887d-0622e7c4ee9a", + "dependent_scopes": [], + }, + { + "scope_string": "https://auth.globus.org/scopes/dfc9a6d3-3373-4a6d-b0a1-b7026d1559d6/view", + "allows_refresh_token": False, + "id": "3793042a-203c-4e86-8dfe-17d407d0bb5f", + "advertised": False, + "required_domains": [], + "name": "Client view scope", + "description": "View configuration of this client", + "client": "dfc9a6d3-3373-4a6d-b0a1-b7026d1559d6", + "dependent_scopes": [], + } + ] + } + + >>> # by all scope ids + >>> scope_ids = ... + >>> r = ac.get_scopes(ids=scopes_ides) + + >>> # by all scope strings + >>> scope_strings = ... + >>> r = ac.get_scopes(scope_strings=scope_strings) + + .. tab-item:: Example Response Data + + .. expandtestfixture:: auth.get_scopes + + .. tab-item:: API Info + + ``GET /v2/api/scopes`` + ``GET /v2/api/scopes?ids=...`` + ``GET /v2/api/scopes?scope_strings=...`` + + .. extdoclink:: Get Scopes + :ref: auth/reference/#get_scopes + """ # noqa: E501 + if scope_strings is not None and ids is not None: + raise exc.GlobusSDKUsageError( + "AuthClient.get_scopes does not take both " + "'scopes_strings' and 'ids'. These are mutually exclusive." + ) + + if query_params is None: + query_params = {} + + if scope_strings is not None: + query_params["scope_strings"] = utils.commajoin(scope_strings) + if ids is not None: + query_params["ids"] = utils.commajoin(ids) + + return GetScopesResponse(self.get("/v2/api/scopes", query_params=query_params)) + + # TODO: Should we relocate this? Would we prefer a different way to pass this + # information? + class DependentScope(t.NamedTuple): + scope: UUIDLike + optional: bool + requires_refresh_token: bool + + def create_scope( + self, + client_id: UUIDLike, + name: str, + description: str, + scope_suffix: str, + *, + required_domains: t.Iterable[str] | None = None, + dependent_scopes: t.Iterable[DependentScope] | None = None, + advertised: bool = False, + allows_refresh_token: bool = True, + ) -> GlobusHTTPResponse: + """ + Create a new scope. Requires the ``manage_projects`` scope. + + :param client_id: ID of the client for the new scope + :type client_id: str or uuid + :param name: A display name used to display consents to users, + along with description + :type name: str + :param description: A description used to display consents to users, along with + name + :type description: str + :param scope_suffix: String consisting of lowercase letters, number, and + underscores. This will be the final part of the scope_string + :type scope_suffix: str + :param required_domains: Domains the user must have linked identities in in + order to make use of the scope + :type required_domains: iterable of str + :param dependent_scopes: Scopes included in the consent for this new scope + :type dependent_scopes: iterable of DependentScope + :param advertised: If True, scope is visible to anyone regardless of client + visibility, otherwise, scope visibility is based on client visibility. + :type advertised: bool + :param allows_refresh_token: Whether or not the scope allows refresh tokens + to be issued. + :type allows_refresh_token: bool + + .. tab-set:: + + .. tab-item:: Example Usage + + .. code-block:: pycon + + >>> ac = globus_sdk.AuthClient(...) + >>> client_id = ... + >>> r = ac.create_scope( + ... client_id, + ... "Client Management", + ... "Manage client configuration", + ... "manage", + ... ) + >>> scope_id = r["scope"]["id"] + + .. tab-item:: Example Response Data + + .. expandtestfixture:: auth.create_scope + + .. tab-item:: API Info + + ``POST /v2/api/clients/{client_id}/scopes`` + + .. extdoclink:: Create Scope + :ref: auth/reference/#create_scope + """ + body: dict[str, t.Any] = { + "name": name, + "description": description, + "scope_suffix": scope_suffix, + "advertised": advertised, + "allows_refresh_token": allows_refresh_token, + } + if required_domains is not None: + body["required_domains"] = required_domains + + if dependent_scopes is not None: + body["dependent_scopes"] = [ + { + "scope": str(ds.scope), + "optional": ds.optional, + "requires_refresh_token": ds.requires_refresh_token, + } + for ds in dependent_scopes + ] + + return self.post(f"/v2/api/clients/{client_id}/scopes", data={"scope": body}) + + def update_scope( + self, + scope_id: UUIDLike, + *, + name: str | None = None, + description: str | None = None, + scope_suffix: str | None = None, + required_domains: t.Iterable[DependentScope] | None = None, + dependent_scopes: t.Iterable[DependentScope] | None = None, + advertised: bool | None = None, + allows_refresh_token: bool | None = None, + ) -> GlobusHTTPResponse: + """ + Update a scope. Requires the ``manage_projects`` scope. + + :param scope_id: ID of the scope to update + :type scope_id: str or uuid + :param name: A display name used to display consents to users, + along with description + :type name: str + :param description: A description used to display consents to users, along with + name + :type description: str + :param scope_suffix: String consisting of lowercase letters, number, and + underscores. This will be the final part of the scope_string + :type scope_suffix: str + :param required_domains: Domains the user must have linked identities in in + order to make use of the scope + :type required_domains: iterable of str + :param dependent_scopes: Scopes included in the consent for this new scope + :type dependent_scopes: iterable of DependentScope + :param advertised: If True, scope is visible to anyone regardless of client + visibility, otherwise, scope visibility is based on client visibility. + :type advertised: bool + :param allows_refresh_token: Whether or not the scope allows refresh tokens + to be issued. + :type allows_refresh_token: bool + + .. tab-set:: + + .. tab-item:: Example Usage + + .. code-block:: pycon + + >>> ac = globus_sdk.AuthClient(...) + >>> scope_id = ... + >>> r = ac.update_scope(scope_id, scope_suffix="manage") + + .. tab-item:: Example Response Data + + .. expandtestfixture:: auth.update_scope + + .. tab-item:: API Info + + ``POST /v2/api/scopes/{scope_id}`` + + .. extdoclink:: Update Scope + :ref: auth/reference/#update_scope + """ + body: dict[str, t.Any] = {} + if name is not None: + body["name"] = name + if description is not None: + body["description"] = description + if scope_suffix is not None: + body["scope_suffix"] = scope_suffix + if advertised is not None: + body["advertised"] = advertised + if allows_refresh_token is not None: + body["allows_refresh_token"] = allows_refresh_token + + if required_domains is not None: + body["required_domains"] = required_domains + + if dependent_scopes is not None: + body["dependent_scopes"] = [ + { + "scope": str(ds.scope), + "optional": ds.optional, + "requires_refresh_token": ds.requires_refresh_token, + } + for ds in dependent_scopes + ] + + return self.put(f"/v2/api/scopes/{scope_id}", data={"scope": body}) + + def delete_scope(self, scope_id: UUIDLike) -> GlobusHTTPResponse: + """ + Delete a scope. Requires the ``manage_projects`` scope. + + :param scope_id: The ID of the scope to delete + :type scope_id: str or uuid + + .. tab-set:: + + .. tab-item:: Example Usage + + .. code-block:: pycon + + >>> ac = globus_sdk.AuthClient(...) + >>> scope_id = ... + >>> r = ac.delete_policy(scope_id) + + .. tab-item:: Example Response Data + + .. expandtestfixture:: auth.delete_scope + + .. tab-item:: API Info + + ``DELETE /v2/api/scopes/{scope_id}`` + + .. extdoclink:: Delete Scopes + :ref: auth/reference/#delete_scope + """ + return self.delete(f"/v2/api/scopes/{scope_id}") + + # TODO: Auth currently only allows this for confidential clients. There is a bug + # report for accepting bearer tokens. We could leave this out for now or move it + # to confidential_client.py. I'd prefer to leave it out. + # https://app.shortcut.com/globus/story/27992/register-fqdn-should-support-bearer-tokens + def add_fqdn( + self, + client_id: UUIDLike, + fqdn: str, + ) -> GlobusHTTPResponse: + # TODO: Add docstring + return self.post(f"/v2/api/clients/{client_id}/fqdns", data={"fqdn": fqdn}) diff --git a/src/globus_sdk/services/auth/response/__init__.py b/src/globus_sdk/services/auth/response/__init__.py index 238209d27..c22beec92 100644 --- a/src/globus_sdk/services/auth/response/__init__.py +++ b/src/globus_sdk/services/auth/response/__init__.py @@ -1,11 +1,19 @@ +from .clients import GetClientsResponse +from .credentials import GetClientCredentialsResponse from .identities import GetIdentitiesResponse, GetIdentityProvidersResponse from .oauth import OAuthDependentTokenResponse, OAuthTokenResponse +from .policies import GetPoliciesResponse from .projects import GetProjectsResponse +from .scopes import GetScopesResponse __all__ = ( + "GetClientCredentialsResponse", + "GetClientsResponse", "GetIdentitiesResponse", "GetIdentityProvidersResponse", + "GetPoliciesResponse", "GetProjectsResponse", + "GetScopesResponse", "OAuthTokenResponse", "OAuthDependentTokenResponse", ) diff --git a/src/globus_sdk/services/auth/response/clients.py b/src/globus_sdk/services/auth/response/clients.py new file mode 100644 index 000000000..8c0d25717 --- /dev/null +++ b/src/globus_sdk/services/auth/response/clients.py @@ -0,0 +1,11 @@ +from globus_sdk.response import IterableResponse + + +class GetClientsResponse(IterableResponse): + """ + Response class specific to the Get Clients API + + Provides iteration on the "clients" array in the response. + """ + + default_iter_key = "clients" diff --git a/src/globus_sdk/services/auth/response/credentials.py b/src/globus_sdk/services/auth/response/credentials.py new file mode 100644 index 000000000..584247a9f --- /dev/null +++ b/src/globus_sdk/services/auth/response/credentials.py @@ -0,0 +1,11 @@ +from globus_sdk.response import IterableResponse + + +class GetClientCredentialsResponse(IterableResponse): + """ + Response class specific to the Get Credentials API + + Provides iteration on the "credentials" array in the response. + """ + + default_iter_key = "credentials" diff --git a/src/globus_sdk/services/auth/response/policies.py b/src/globus_sdk/services/auth/response/policies.py new file mode 100644 index 000000000..5de43a1ea --- /dev/null +++ b/src/globus_sdk/services/auth/response/policies.py @@ -0,0 +1,11 @@ +from globus_sdk.response import IterableResponse + + +class GetPoliciesResponse(IterableResponse): + """ + Response class specific to the Get Policies API + + Provides iteration on the "policies" array in the response. + """ + + default_iter_key = "policies" diff --git a/src/globus_sdk/services/auth/response/scopes.py b/src/globus_sdk/services/auth/response/scopes.py new file mode 100644 index 000000000..502828e24 --- /dev/null +++ b/src/globus_sdk/services/auth/response/scopes.py @@ -0,0 +1,11 @@ +from globus_sdk.response import IterableResponse + + +class GetScopesResponse(IterableResponse): + """ + Response class specific to the Get Scopes API + + Provides iteration on the "scopes" array in the response. + """ + + default_iter_key = "scopes" diff --git a/tests/functional/services/auth/confidential_client/test_create_child_client.py b/tests/functional/services/auth/confidential_client/test_create_child_client.py new file mode 100644 index 000000000..f4f888236 --- /dev/null +++ b/tests/functional/services/auth/confidential_client/test_create_child_client.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +import pytest + +from globus_sdk import GlobusSDKUsageError +from globus_sdk._testing import load_response + + +@pytest.mark.parametrize( + "case_name", + ( + "name", + "public_client", + "private_client", + "publicly_visible", + "not_publicly_visible", + "redirect_uris", + "links", + "required_idp", + "preselect_idp", + ), +) +def test_create_child_client_args( + auth_client, + case_name: str, +): + meta = load_response(auth_client.create_child_client, case=case_name).metadata + + res = auth_client.create_child_client(**meta["args"]) + for k, v in meta["response"].items(): + assert res["client"][k] == v + + +def test_links_requirement(auth_client): + """ + Verify that terms_and_conditions and privacy_policy must be used together. + """ + with pytest.raises(GlobusSDKUsageError): + auth_client.create_child_client( + "FOO", + "TRUE", + publicly_visible=True, + terms_and_conditions="https://foo.net", + ) + + with pytest.raises(GlobusSDKUsageError): + auth_client.create_child_client( + "FOO", + "TRUE", + publicly_visible=True, + privacy_policy="https://foo.net", + ) diff --git a/tests/functional/services/auth/native_client/conftest.py b/tests/functional/services/auth/native_client/conftest.py new file mode 100644 index 000000000..78c4d6417 --- /dev/null +++ b/tests/functional/services/auth/native_client/conftest.py @@ -0,0 +1,11 @@ +import pytest + +import globus_sdk + + +@pytest.fixture +def auth_client(no_retry_transport): + class CustomAuthClient(globus_sdk.NativeAppAuthClient): + transport_class = no_retry_transport + + return CustomAuthClient("dummy_client_id") diff --git a/tests/functional/services/auth/native_client/test_create_native_app_instance.py b/tests/functional/services/auth/native_client/test_create_native_app_instance.py new file mode 100644 index 000000000..dd52a6d9d --- /dev/null +++ b/tests/functional/services/auth/native_client/test_create_native_app_instance.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +import pytest + +from globus_sdk._testing import load_response + + +@pytest.mark.parametrize( + "case_name", + ( + "template_id_str", + "template_id_uuid", + "name", + ), +) +def test_create_native_app_instance( + auth_client, + case_name: str, +): + meta = load_response( + auth_client.create_native_app_instance, case=case_name + ).metadata + + res = auth_client.create_native_app_instance(**meta["args"]) + for k, v in meta["response"].items(): + assert res["client"][k] == v diff --git a/tests/functional/services/auth/service_client/test_create_client.py b/tests/functional/services/auth/service_client/test_create_client.py new file mode 100644 index 000000000..58a8e35bf --- /dev/null +++ b/tests/functional/services/auth/service_client/test_create_client.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +import uuid + +import pytest + +from globus_sdk import GlobusSDKUsageError +from globus_sdk._testing import load_response + + +@pytest.mark.parametrize( + "case_name", + ( + "name", + "public_client", + "private_client", + "project_id", + "publicly_visible", + "not_publicly_visible", + "redirect_uris", + "links", + "required_idp", + "preselect_idp", + ), +) +def test_create_client_args( + service_client, + case_name: str, +): + meta = load_response(service_client.create_client, case=case_name).metadata + + res = service_client.create_client(**meta["args"]) + for k, v in meta["response"].items(): + assert res["client"][k] == v + + +def test_links_requirement(service_client): + """ + Verify that terms_and_conditions and privacy_policy must be used together. + """ + with pytest.raises(GlobusSDKUsageError): + service_client.create_client( + "FOO", + "TRUE", + uuid.uuid1(), + publicly_visible=True, + terms_and_conditions="https://foo.net", + ) + + with pytest.raises(GlobusSDKUsageError): + service_client.create_client( + "FOO", + "TRUE", + uuid.uuid1(), + publicly_visible=True, + privacy_policy="https://foo.net", + ) diff --git a/tests/functional/services/auth/service_client/test_create_client_credential.py b/tests/functional/services/auth/service_client/test_create_client_credential.py new file mode 100644 index 000000000..6096acb58 --- /dev/null +++ b/tests/functional/services/auth/service_client/test_create_client_credential.py @@ -0,0 +1,32 @@ +from __future__ import annotations + +import uuid + +import pytest + +from globus_sdk._testing import load_response + + +@pytest.mark.parametrize("uuid_type", (str, uuid.UUID)) +def test_create_credential( + service_client, + uuid_type: type[str] | type[uuid.UUID], +): + meta = load_response(service_client.create_client_credential).metadata + + res = service_client.create_client_credential( + meta["client_id"] if uuid_type is str else uuid.UUID(meta["client_id"]), + meta["name"], + ) + + assert res["credential"]["id"] == meta["credential_id"] + + +def test_create_credential_set_name( + service_client, +): + meta = load_response(service_client.create_client_credential, case="name").metadata + + res = service_client.create_client_credential(meta["client_id"], meta["name"]) + + assert res["credential"]["name"] == meta["name"] diff --git a/tests/functional/services/auth/service_client/test_create_policy.py b/tests/functional/services/auth/service_client/test_create_policy.py new file mode 100644 index 000000000..9e8a52e6a --- /dev/null +++ b/tests/functional/services/auth/service_client/test_create_policy.py @@ -0,0 +1,32 @@ +from __future__ import annotations + +import pytest + +from globus_sdk._testing import load_response + + +@pytest.mark.parametrize( + "case_name", + ( + "project_id_str", + "project_id_uuid", + "high_assurance", + "not_high_assurance", + "authentication_assurance_timeout", + "display_name", + "description", + "domain_constraints_include", + "empty_domain_constraints_include", + "domain_constraints_exclude", + "empty_domain_constraints_exclude", + ), +) +def test_create_policy( + service_client, + case_name: str, +): + meta = load_response(service_client.create_policy, case=case_name).metadata + + res = service_client.create_policy(**meta["args"]) + for k, v in meta["response"].items(): + assert res["policy"][k] == v diff --git a/tests/functional/services/auth/service_client/test_create_scope.py b/tests/functional/services/auth/service_client/test_create_scope.py new file mode 100644 index 000000000..c7ea900f0 --- /dev/null +++ b/tests/functional/services/auth/service_client/test_create_scope.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +import pytest + +from globus_sdk._testing import load_response + + +@pytest.mark.parametrize( + "case_name", + ( + "client_id_str", + "client_id_uuid", + "name", + "description", + "scope_suffix", + "no_required_domains", + "required_domains", + "no_dependent_scopes", + "dependent_scopes", + "advertised", + "not_advertised", + "allows_refresh_token", + "disallows_refresh_token", + ), +) +def test_create_scope( + service_client, + case_name: str, +): + meta = load_response(service_client.create_scope, case=case_name).metadata + + res = service_client.create_scope(**meta["args"]) + for k, v in meta["response"].items(): + assert res["scope"][k] == v diff --git a/tests/functional/services/auth/service_client/test_delete_client.py b/tests/functional/services/auth/service_client/test_delete_client.py new file mode 100644 index 000000000..af4c20fa3 --- /dev/null +++ b/tests/functional/services/auth/service_client/test_delete_client.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +import uuid + +import pytest + +from globus_sdk._testing import load_response + + +@pytest.mark.parametrize( + "uuid_type", + (str, uuid.UUID), +) +def test_delete_client( + service_client, + uuid_type: type[str] | type[uuid.UUID], +): + meta = load_response(service_client.delete_client).metadata + + if uuid_type is str: + res = service_client.delete_client(client_id=meta["client_id"]) + else: + res = service_client.delete_client(client_id=uuid.UUID(meta["client_id"])) + + assert res["client"]["id"] == meta["client_id"] diff --git a/tests/functional/services/auth/service_client/test_delete_client_credential.py b/tests/functional/services/auth/service_client/test_delete_client_credential.py new file mode 100644 index 000000000..01cf8ff1a --- /dev/null +++ b/tests/functional/services/auth/service_client/test_delete_client_credential.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +import uuid + +import pytest + +from globus_sdk._testing import load_response + + +@pytest.mark.parametrize( + "uuid_type", + (str, uuid.UUID), +) +def test_delete_credential( + service_client, + uuid_type: type[str] | type[uuid.UUID], +): + meta = load_response(service_client.delete_client_credential).metadata + + if uuid_type is str: + res = service_client.delete_client_credential( + client_id=meta["client_id"], + credential_id=meta["credential_id"], + ) + else: + res = service_client.delete_client_credential( + client_id=uuid.UUID(meta["client_id"]), + credential_id=uuid.UUID(meta["credential_id"]), + ) + + assert res["credential"]["id"] == meta["credential_id"] diff --git a/tests/functional/services/auth/service_client/test_delete_policy.py b/tests/functional/services/auth/service_client/test_delete_policy.py new file mode 100644 index 000000000..538554c42 --- /dev/null +++ b/tests/functional/services/auth/service_client/test_delete_policy.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +import uuid + +import pytest + +from globus_sdk._testing import load_response + + +@pytest.mark.parametrize( + "uuid_type", + (str, uuid.UUID), +) +def test_delete_policy( + service_client, + uuid_type: type[str] | type[uuid.UUID], +): + meta = load_response(service_client.delete_policy).metadata + + if uuid_type is str: + res = service_client.delete_policy(meta["policy_id"]) + else: + res = service_client.delete_policy(uuid.UUID(meta["policy_id"])) + + assert res["policy"]["id"] == meta["policy_id"] diff --git a/tests/functional/services/auth/service_client/test_delete_scope.py b/tests/functional/services/auth/service_client/test_delete_scope.py new file mode 100644 index 000000000..bd672f272 --- /dev/null +++ b/tests/functional/services/auth/service_client/test_delete_scope.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +import uuid + +import pytest + +from globus_sdk._testing import load_response + + +@pytest.mark.parametrize( + "uuid_type", + (str, uuid.UUID), +) +def test_delete_scope( + service_client, + uuid_type: type[str] | type[uuid.UUID], +): + meta = load_response(service_client.delete_scope).metadata + + if uuid_type is str: + res = service_client.delete_scope(meta["scope_id"]) + else: + res = service_client.delete_scope(uuid.UUID(meta["scope_id"])) + + assert res["scope"]["id"] == meta["scope_id"] diff --git a/tests/functional/services/auth/service_client/test_get_client.py b/tests/functional/services/auth/service_client/test_get_client.py new file mode 100644 index 000000000..478f55ef8 --- /dev/null +++ b/tests/functional/services/auth/service_client/test_get_client.py @@ -0,0 +1,48 @@ +from __future__ import annotations + +import uuid + +import pytest + +from globus_sdk import GlobusSDKUsageError +from globus_sdk._testing import load_response + + +@pytest.mark.parametrize( + "uuid_type", + (str, uuid.UUID), +) +def test_get_client_by_id( + service_client, + uuid_type: type[str] | type[uuid.UUID], +): + meta = load_response(service_client.get_client).metadata + + if uuid_type is str: + res = service_client.get_client(client_id=meta["client_id"]) + else: + res = service_client.get_client(client_id=uuid.UUID(meta["client_id"])) + + assert res["client"]["id"] == meta["client_id"] + + +def test_get_client_by_fqdn( + service_client, +): + meta = load_response(service_client.get_client, case="fqdn").metadata + res = service_client.get_client(fqdn=meta["fqdn"]) + + assert res["client"]["id"] == meta["client_id"] + + +def test_get_client_exactly_one_of_id_or_fqdn( + service_client, +): + with pytest.raises(GlobusSDKUsageError): + service_client.get_client() + + with pytest.raises(GlobusSDKUsageError): + service_client.get_client( + client_id="1b72b72e-5251-454d-af67-0be35911d174", + fqdn="globus.org", + ) diff --git a/tests/functional/services/auth/service_client/test_get_client_credentials.py b/tests/functional/services/auth/service_client/test_get_client_credentials.py new file mode 100644 index 000000000..9cbc97992 --- /dev/null +++ b/tests/functional/services/auth/service_client/test_get_client_credentials.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +import uuid + +import pytest + +from globus_sdk._testing import load_response + + +@pytest.mark.parametrize( + "uuid_type", + (str, uuid.UUID), +) +def test_get_client_credentials( + service_client, + uuid_type: type[str] | type[uuid.UUID], +): + meta = load_response(service_client.get_client_credentials).metadata + + if uuid_type is str: + res = service_client.get_client_credentials(meta["client_id"]) + else: + res = service_client.get_client_credentials(uuid.UUID(meta["client_id"])) + + assert {cred["id"] for cred in res["credentials"]} == {meta["credential_id"]} diff --git a/tests/functional/services/auth/service_client/test_get_clients.py b/tests/functional/services/auth/service_client/test_get_clients.py new file mode 100644 index 000000000..6565fe22d --- /dev/null +++ b/tests/functional/services/auth/service_client/test_get_clients.py @@ -0,0 +1,8 @@ +from globus_sdk._testing import load_response + + +def test_get_clients(service_client): + meta = load_response(service_client.get_clients).metadata + res = service_client.get_clients() + + assert {client["id"] for client in res["clients"]} == set(meta["client_ids"]) diff --git a/tests/functional/services/auth/service_client/test_get_policies.py b/tests/functional/services/auth/service_client/test_get_policies.py new file mode 100644 index 000000000..69f8ae4d5 --- /dev/null +++ b/tests/functional/services/auth/service_client/test_get_policies.py @@ -0,0 +1,8 @@ +from globus_sdk._testing import load_response + + +def test_get_policies(service_client): + meta = load_response(service_client.get_policies).metadata + res = service_client.get_policies() + + assert {policy["id"] for policy in res["policies"]} == set(meta["policy_ids"]) diff --git a/tests/functional/services/auth/service_client/test_get_policy.py b/tests/functional/services/auth/service_client/test_get_policy.py new file mode 100644 index 000000000..1a06e5004 --- /dev/null +++ b/tests/functional/services/auth/service_client/test_get_policy.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +import uuid + +import pytest + +from globus_sdk._testing import load_response + + +@pytest.mark.parametrize( + "uuid_type", + (str, uuid.UUID), +) +def test_get_policy( + service_client, + uuid_type: type[str] | type[uuid.UUID], +): + meta = load_response(service_client.get_policy).metadata + + if uuid_type is str: + res = service_client.get_policy(meta["policy_id"]) + else: + res = service_client.get_policy(uuid.UUID(meta["policy_id"])) + + assert res["policy"]["id"] == meta["policy_id"] diff --git a/tests/functional/services/auth/service_client/test_get_project.py b/tests/functional/services/auth/service_client/test_get_project.py new file mode 100644 index 000000000..f809a3d41 --- /dev/null +++ b/tests/functional/services/auth/service_client/test_get_project.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +import uuid + +import pytest + +from globus_sdk._testing import load_response + + +@pytest.mark.parametrize( + "uuid_type", + (str, uuid.UUID), +) +def test_get_project( + service_client, + uuid_type: type[str] | type[uuid.UUID], +): + meta = load_response(service_client.get_project).metadata + + if uuid_type is str: + res = service_client.get_project(meta["project_id"]) + else: + res = service_client.get_project(uuid.UUID(meta["project_id"])) + + assert res["project"]["id"] == meta["project_id"] diff --git a/tests/functional/services/auth/service_client/test_get_scope.py b/tests/functional/services/auth/service_client/test_get_scope.py new file mode 100644 index 000000000..bf13cac7d --- /dev/null +++ b/tests/functional/services/auth/service_client/test_get_scope.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +import uuid + +import pytest + +from globus_sdk._testing import load_response + + +@pytest.mark.parametrize( + "uuid_type", + (str, uuid.UUID), +) +def test_get_scope( + service_client, + uuid_type: type[str] | type[uuid.UUID], +): + meta = load_response(service_client.get_scope).metadata + + if uuid_type is str: + res = service_client.get_scope(scope_id=meta["scope_id"]) + else: + res = service_client.get_scope(scope_id=uuid.UUID(meta["scope_id"])) + + assert res["scope"]["id"] == meta["scope_id"] diff --git a/tests/functional/services/auth/service_client/test_get_scopes.py b/tests/functional/services/auth/service_client/test_get_scopes.py new file mode 100644 index 000000000..760cea606 --- /dev/null +++ b/tests/functional/services/auth/service_client/test_get_scopes.py @@ -0,0 +1,33 @@ +import pytest + +from globus_sdk import GlobusSDKUsageError +from globus_sdk._testing import load_response + + +def test_get_scopes(service_client): + meta = load_response(service_client.get_scopes).metadata + res = service_client.get_scopes() + + assert {scope["id"] for scope in res["scopes"]} == set(meta["scope_ids"]) + + +def test_get_scopes_by_ids(service_client): + meta = load_response(service_client.get_scopes, case="id").metadata + res = service_client.get_scopes(ids=[meta["scope_id"]]) + + assert res["scopes"][0]["id"] == meta["scope_id"] + + +def test_get_scopes_by_strings(service_client): + meta = load_response(service_client.get_scopes, case="string").metadata + res = service_client.get_scopes(scope_strings=[meta["scope_string"]]) + + assert res["scopes"][0]["scope_string"] == meta["scope_string"] + + +def test_get_scopes_id_strings_mutually_exclusive(service_client): + with pytest.raises(GlobusSDKUsageError): + service_client.get_scopes( + scope_strings=["foo"], + ids=["18a8cd00-700a-4fcb-b6da-6efca558c369"], + ) diff --git a/tests/functional/services/auth/service_client/test_update_client.py b/tests/functional/services/auth/service_client/test_update_client.py new file mode 100644 index 000000000..da338b01b --- /dev/null +++ b/tests/functional/services/auth/service_client/test_update_client.py @@ -0,0 +1,54 @@ +from __future__ import annotations + +import uuid + +import pytest + +from globus_sdk import GlobusSDKUsageError +from globus_sdk._testing import load_response + + +@pytest.mark.parametrize( + "case_name", + ( + "name", + "publicly_visible", + "not_publicly_visible", + "redirect_uris", + "links", + "required_idp", + "preselect_idp", + ), +) +def test_update_client_args( + service_client, + case_name: str, +): + meta = load_response(service_client.update_client, case=case_name).metadata + + res = service_client.update_client(**meta["args"]) + for k, v in meta["response"].items(): + assert res["client"][k] == v + + +def test_links_requirement(service_client): + """ + Verify that terms_and_conditions and privacy_policy must be used together. + """ + with pytest.raises(GlobusSDKUsageError): + service_client.create_client( + "FOO", + "TRUE", + uuid.uuid1(), + publicly_visible=True, + terms_and_conditions="https://foo.net", + ) + + with pytest.raises(GlobusSDKUsageError): + service_client.create_client( + "FOO", + "TRUE", + uuid.uuid1(), + publicly_visible=True, + privacy_policy="https://foo.net", + ) diff --git a/tests/functional/services/auth/service_client/test_update_policy.py b/tests/functional/services/auth/service_client/test_update_policy.py new file mode 100644 index 000000000..6c075eaf2 --- /dev/null +++ b/tests/functional/services/auth/service_client/test_update_policy.py @@ -0,0 +1,32 @@ +from __future__ import annotations + +import pytest + +from globus_sdk._testing import load_response + + +@pytest.mark.parametrize( + "case_name", + ( + "project_id_str", + "project_id_uuid", + "authentication_assurance_timeout", + "display_name", + "description", + "no_domain_constrants_include", + "empty_domain_constrants_include", + "domain_constrants_include", + "no_domain_constrants_exclude", + "empty_domain_constrants_exclude", + "domain_constrants_exclude", + ), +) +def test_update_policy( + service_client, + case_name: str, +): + meta = load_response(service_client.update_policy, case=case_name).metadata + + res = service_client.update_policy(**meta["args"]) + for k, v in meta["response"].items(): + assert res["policy"][k] == v diff --git a/tests/functional/services/auth/service_client/test_update_scope.py b/tests/functional/services/auth/service_client/test_update_scope.py new file mode 100644 index 000000000..956325222 --- /dev/null +++ b/tests/functional/services/auth/service_client/test_update_scope.py @@ -0,0 +1,32 @@ +from __future__ import annotations + +import pytest + +from globus_sdk._testing import load_response + + +@pytest.mark.parametrize( + "case_name", + ( + "name", + "description", + "scope_suffix", + "no_required_domains", + "required_domains", + "no_dependent_scopes", + "dependent_scopes", + "advertised", + "not_advertised", + "allows_refresh_token", + "disallows_refresh_token", + ), +) +def test_update_scope( + service_client, + case_name: str, +): + meta = load_response(service_client.update_scope, case=case_name).metadata + + res = service_client.update_scope(**meta["args"]) + for k, v in meta["response"].items(): + assert res["scope"][k] == v