diff --git a/api/python/quilt3-admin/queries.graphql b/api/python/quilt3-admin/queries.graphql index 4947f7f53ba..98b81cbdd8c 100644 --- a/api/python/quilt3-admin/queries.graphql +++ b/api/python/quilt3-admin/queries.graphql @@ -47,6 +47,13 @@ fragment OperationErrorSelection on OperationError { name context } +fragment SsoConfigSelection on SsoConfig { + text + timestamp + uploader { + ...UserSelection + } +} query rolesList { roles { @@ -187,3 +194,21 @@ mutation usersRemoveRoles($name: String!, $roles: [String!]!, $fallback: String) } } } + +query ssoConfigGet { + admin { + ssoConfig { + ...SsoConfigSelection + } + } +} + +mutation ssoConfigSet($config: String) { + admin { + setSsoConfig(config: $config) { + ...SsoConfigSelection + ...InvalidInputSelection + ...OperationErrorSelection + } + } +} diff --git a/api/python/quilt3/admin/__init__.py b/api/python/quilt3/admin/__init__.py index 3f70b0f7777..e89ecb03e64 100644 --- a/api/python/quilt3/admin/__init__.py +++ b/api/python/quilt3/admin/__init__.py @@ -4,6 +4,6 @@ # This wraps code generated by aridne-codegen to provide a more user-friendly API. -from . import roles, users +from . import roles, sso_config, users from .exceptions import Quilt3AdminError, UserNotFoundError -from .types import ManagedRole, UnmanagedRole, User +from .types import ManagedRole, SSOConfig, UnmanagedRole, User diff --git a/api/python/quilt3/admin/_graphql_client/__init__.py b/api/python/quilt3/admin/_graphql_client/__init__.py index 399900def00..9f25eaa4a0f 100644 --- a/api/python/quilt3/admin/_graphql_client/__init__.py +++ b/api/python/quilt3/admin/_graphql_client/__init__.py @@ -8,6 +8,8 @@ InvalidInputSelectionErrors, ManagedRoleSelection, OperationErrorSelection, + SsoConfigSelection, + SsoConfigSelectionUploader, UnmanagedRoleSelection, UserSelection, UserSelectionExtraRolesManagedRole, @@ -21,6 +23,14 @@ RolesListRolesManagedRole, RolesListRolesUnmanagedRole, ) +from .sso_config_get import SsoConfigGet, SsoConfigGetAdmin, SsoConfigGetAdminSsoConfig +from .sso_config_set import ( + SsoConfigSet, + SsoConfigSetAdmin, + SsoConfigSetAdminSetSsoConfigInvalidInput, + SsoConfigSetAdminSetSsoConfigOperationError, + SsoConfigSetAdminSetSsoConfigSsoConfig, +) from .users_add_roles import ( UsersAddRoles, UsersAddRolesAdmin, @@ -120,6 +130,16 @@ "RolesList", "RolesListRolesManagedRole", "RolesListRolesUnmanagedRole", + "SsoConfigGet", + "SsoConfigGetAdmin", + "SsoConfigGetAdminSsoConfig", + "SsoConfigSelection", + "SsoConfigSelectionUploader", + "SsoConfigSet", + "SsoConfigSetAdmin", + "SsoConfigSetAdminSetSsoConfigInvalidInput", + "SsoConfigSetAdminSetSsoConfigOperationError", + "SsoConfigSetAdminSetSsoConfigSsoConfig", "UnmanagedRoleSelection", "Upload", "UserInput", diff --git a/api/python/quilt3/admin/_graphql_client/client.py b/api/python/quilt3/admin/_graphql_client/client.py index 12686f8ff48..6218be65f9c 100644 --- a/api/python/quilt3/admin/_graphql_client/client.py +++ b/api/python/quilt3/admin/_graphql_client/client.py @@ -11,6 +11,13 @@ RolesListRolesManagedRole, RolesListRolesUnmanagedRole, ) +from .sso_config_get import SsoConfigGet, SsoConfigGetAdminSsoConfig +from .sso_config_set import ( + SsoConfigSet, + SsoConfigSetAdminSetSsoConfigInvalidInput, + SsoConfigSetAdminSetSsoConfigOperationError, + SsoConfigSetAdminSetSsoConfigSsoConfig, +) from .users_add_roles import UsersAddRoles, UsersAddRolesAdminUserMutate from .users_create import ( UsersCreate, @@ -852,3 +859,151 @@ def users_remove_roles( ) data = self.get_data(response) return UsersRemoveRoles.model_validate(data).admin.user.mutate + + def sso_config_get(self, **kwargs: Any) -> Optional[SsoConfigGetAdminSsoConfig]: + query = gql( + """ + query ssoConfigGet { + admin { + ssoConfig { + ...SsoConfigSelection + } + } + } + + fragment ManagedRoleSelection on ManagedRole { + id + name + arn + } + + fragment RoleSelection on Role { + __typename + ...UnmanagedRoleSelection + ...ManagedRoleSelection + } + + fragment SsoConfigSelection on SsoConfig { + text + timestamp + uploader { + ...UserSelection + } + } + + fragment UnmanagedRoleSelection on UnmanagedRole { + id + name + arn + } + + fragment UserSelection on User { + name + email + dateJoined + lastLogin + isActive + isAdmin + isSsoOnly + isService + role { + ...RoleSelection + } + extraRoles { + ...RoleSelection + } + } + """ + ) + variables: Dict[str, object] = {} + response = self.execute( + query=query, operation_name="ssoConfigGet", variables=variables, **kwargs + ) + data = self.get_data(response) + return SsoConfigGet.model_validate(data).admin.sso_config + + def sso_config_set( + self, config: Union[Optional[str], UnsetType] = UNSET, **kwargs: Any + ) -> Union[ + SsoConfigSetAdminSetSsoConfigSsoConfig, + SsoConfigSetAdminSetSsoConfigInvalidInput, + SsoConfigSetAdminSetSsoConfigOperationError, + ]: + query = gql( + """ + mutation ssoConfigSet($config: String) { + admin { + setSsoConfig(config: $config) { + __typename + ...SsoConfigSelection + ...InvalidInputSelection + ...OperationErrorSelection + } + } + } + + fragment InvalidInputSelection on InvalidInput { + errors { + path + message + name + context + } + } + + fragment ManagedRoleSelection on ManagedRole { + id + name + arn + } + + fragment OperationErrorSelection on OperationError { + message + name + context + } + + fragment RoleSelection on Role { + __typename + ...UnmanagedRoleSelection + ...ManagedRoleSelection + } + + fragment SsoConfigSelection on SsoConfig { + text + timestamp + uploader { + ...UserSelection + } + } + + fragment UnmanagedRoleSelection on UnmanagedRole { + id + name + arn + } + + fragment UserSelection on User { + name + email + dateJoined + lastLogin + isActive + isAdmin + isSsoOnly + isService + role { + ...RoleSelection + } + extraRoles { + ...RoleSelection + } + } + """ + ) + variables: Dict[str, object] = {"config": config} + response = self.execute( + query=query, operation_name="ssoConfigSet", variables=variables, **kwargs + ) + data = self.get_data(response) + return SsoConfigSet.model_validate(data).admin.set_sso_config diff --git a/api/python/quilt3/admin/_graphql_client/fragments.py b/api/python/quilt3/admin/_graphql_client/fragments.py index 33cd794855c..1568dbcc7c4 100644 --- a/api/python/quilt3/admin/_graphql_client/fragments.py +++ b/api/python/quilt3/admin/_graphql_client/fragments.py @@ -80,8 +80,19 @@ class UserSelectionExtraRolesManagedRole(ManagedRoleSelection): typename__: Literal["ManagedRole"] = Field(alias="__typename") +class SsoConfigSelection(BaseModel): + text: str + timestamp: datetime + uploader: "SsoConfigSelectionUploader" + + +class SsoConfigSelectionUploader(UserSelection): + pass + + InvalidInputSelection.model_rebuild() ManagedRoleSelection.model_rebuild() OperationErrorSelection.model_rebuild() UnmanagedRoleSelection.model_rebuild() UserSelection.model_rebuild() +SsoConfigSelection.model_rebuild() diff --git a/api/python/quilt3/admin/_graphql_client/sso_config_get.py b/api/python/quilt3/admin/_graphql_client/sso_config_get.py new file mode 100644 index 00000000000..b57c6dbb47c --- /dev/null +++ b/api/python/quilt3/admin/_graphql_client/sso_config_get.py @@ -0,0 +1,25 @@ +# Generated by ariadne-codegen +# Source: queries.graphql + +from typing import Optional + +from pydantic import Field + +from .base_model import BaseModel +from .fragments import SsoConfigSelection + + +class SsoConfigGet(BaseModel): + admin: "SsoConfigGetAdmin" + + +class SsoConfigGetAdmin(BaseModel): + sso_config: Optional["SsoConfigGetAdminSsoConfig"] = Field(alias="ssoConfig") + + +class SsoConfigGetAdminSsoConfig(SsoConfigSelection): + pass + + +SsoConfigGet.model_rebuild() +SsoConfigGetAdmin.model_rebuild() diff --git a/api/python/quilt3/admin/_graphql_client/sso_config_set.py b/api/python/quilt3/admin/_graphql_client/sso_config_set.py new file mode 100644 index 00000000000..e66706fc267 --- /dev/null +++ b/api/python/quilt3/admin/_graphql_client/sso_config_set.py @@ -0,0 +1,41 @@ +# Generated by ariadne-codegen +# Source: queries.graphql + +from typing import Literal, Union + +from pydantic import Field + +from .base_model import BaseModel +from .fragments import ( + InvalidInputSelection, + OperationErrorSelection, + SsoConfigSelection, +) + + +class SsoConfigSet(BaseModel): + admin: "SsoConfigSetAdmin" + + +class SsoConfigSetAdmin(BaseModel): + set_sso_config: Union[ + "SsoConfigSetAdminSetSsoConfigSsoConfig", + "SsoConfigSetAdminSetSsoConfigInvalidInput", + "SsoConfigSetAdminSetSsoConfigOperationError", + ] = Field(alias="setSsoConfig", discriminator="typename__") + + +class SsoConfigSetAdminSetSsoConfigSsoConfig(SsoConfigSelection): + typename__: Literal["SsoConfig"] = Field(alias="__typename") + + +class SsoConfigSetAdminSetSsoConfigInvalidInput(InvalidInputSelection): + typename__: Literal["InvalidInput"] = Field(alias="__typename") + + +class SsoConfigSetAdminSetSsoConfigOperationError(OperationErrorSelection): + typename__: Literal["OperationError"] = Field(alias="__typename") + + +SsoConfigSet.model_rebuild() +SsoConfigSetAdmin.model_rebuild() diff --git a/api/python/quilt3/admin/sso_config.py b/api/python/quilt3/admin/sso_config.py new file mode 100644 index 00000000000..cf4f5c448be --- /dev/null +++ b/api/python/quilt3/admin/sso_config.py @@ -0,0 +1,18 @@ +import typing as T + +from . import types, util + + +def get() -> T.Optional[types.SSOConfig]: + """ + Get the current SSO configuration. + """ + result = util.get_client().sso_config_get() + return None if result is None else types.SSOConfig(**result.model_dump()) + + +def set(config: T.Optional[str]) -> types.SSOConfig: + """ + Set the SSO configuration. + """ + return types.SSOConfig(**util.handle_errors(util.get_client().sso_config_set(config)).model_dump()) diff --git a/api/python/quilt3/admin/types.py b/api/python/quilt3/admin/types.py index 959a3c00601..004f1cf5194 100644 --- a/api/python/quilt3/admin/types.py +++ b/api/python/quilt3/admin/types.py @@ -37,3 +37,10 @@ class User: is_service: bool role: Optional[AnnotatedRole] extra_roles: List[AnnotatedRole] + + +@pydantic.dataclasses.dataclass +class SSOConfig: + text: str + timestamp: datetime + uploader: User diff --git a/api/python/tests/test_admin_api.py b/api/python/tests/test_admin_api.py index 69446664b1b..87cd44a5259 100644 --- a/api/python/tests/test_admin_api.py +++ b/api/python/tests/test_admin_api.py @@ -32,6 +32,12 @@ "role": UNMANAGED_ROLE, "extraRoles": [MANAGED_ROLE], } +SSO_CONFIG = { + "__typename": "SsoConfig", + "text": "", + "timestamp": datetime.datetime(2024, 6, 14, 11, 42, 27, 857128, tzinfo=datetime.timezone.utc), + "uploader": USER, +} MUTATION_ERRORS = ( ( { @@ -324,3 +330,31 @@ def test_remove_roles(data, result): admin.users.remove_roles("test", ["ManagedRole"], fallback="UnamanagedRole") else: assert admin.users.remove_roles("test", ["ManagedRole"], fallback="UnamanagedRole") == result + + +@pytest.mark.parametrize( + "data,result", + [ + (SSO_CONFIG, admin.SSOConfig(**_as_dataclass_kwargs(SSO_CONFIG))), + (None, None), + ], +) +def test_sso_config_get(data, result): + with mock_client(_make_nested_dict("admin.sso_config", data), "ssoConfigGet"): + assert admin.sso_config.get() == result + + +@pytest.mark.parametrize( + "data,result", + [ + (SSO_CONFIG, admin.SSOConfig(**_as_dataclass_kwargs(SSO_CONFIG))), + *MUTATION_ERRORS, + ], +) +def test_sso_config_set(data, result): + with mock_client(_make_nested_dict("admin.set_sso_config", data), "ssoConfigSet", variables={"config": ""}): + if isinstance(result, type) and issubclass(result, Exception): + with pytest.raises(result): + admin.sso_config.set("") + else: + assert admin.sso_config.set("") == result diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 28984473775..d192e4a71e2 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -18,6 +18,8 @@ Entries inside each section should be ordered by type: # unreleased - YYYY-MM-DD ## Python API +* [Added] New `quilt3.admin.sso_config` sub-module for management of SSO configuration ([#4065](https://github.com/quiltdata/quilt/pull/4065)) + ## CLI ## Catalog, Lambdas diff --git a/docs/api-reference/Admin.md b/docs/api-reference/Admin.md index 2ac177a5daf..2d6b08cc194 100644 --- a/docs/api-reference/Admin.md +++ b/docs/api-reference/Admin.md @@ -11,6 +11,9 @@ ## User(name: str, email: str, date\_joined: datetime.datetime, last\_login: datetime.datetime, is\_active: bool, is\_admin: bool, is\_sso\_only: bool, is\_service: bool, role: Optional[Annotated[Union[quilt3.admin.types.ManagedRole, quilt3.admin.types.UnmanagedRole], FieldInfo(annotation=NoneType, required=True, discriminator='typename\_\_')]], extra\_roles: List[Annotated[Union[quilt3.admin.types.ManagedRole, quilt3.admin.types.UnmanagedRole], FieldInfo(annotation=NoneType, required=True, discriminator='typename\_\_')]]) -> None {#User} +## SSOConfig(text: str, timestamp: datetime.datetime, uploader: quilt3.admin.types.User) -> None {#SSOConfig} + + # quilt3.admin.roles @@ -128,3 +131,16 @@ __Arguments__ * __roles__: Roles to remove from the user. * __fallback__: If set, the role to assign to the user if the active role is removed. + +# quilt3.admin.sso_config + + +## get() -> Optional[quilt3.admin.types.SSOConfig] {#get} + +Get the current SSO configuration. + + +## set(config: Optional[str]) -> quilt3.admin.types.SSOConfig {#set} + +Set the SSO configuration. + diff --git a/gendocs/pydocmd.yml b/gendocs/pydocmd.yml index 6c0e1253f99..71c685f9a09 100644 --- a/gendocs/pydocmd.yml +++ b/gendocs/pydocmd.yml @@ -34,6 +34,7 @@ generate: - quilt3.admin.types+ - quilt3.admin.roles+ - quilt3.admin.users+ + - quilt3.admin.sso_config+ # MkDocs pages configuration. The `<<` operator is sugar added by pydocmd # that allows you to use an external Markdown file (eg. your project's README)