diff --git a/catalog/app/components/Lock/Lock.tsx b/catalog/app/components/Lock/Lock.tsx index a0a99594857..022d8db2470 100644 --- a/catalog/app/components/Lock/Lock.tsx +++ b/catalog/app/components/Lock/Lock.tsx @@ -5,21 +5,27 @@ import { fade } from '@material-ui/core/styles' const useStyles = M.makeStyles((t) => ({ root: { + alignItems: 'center', background: fade(t.palette.background.paper, 0.5), bottom: 0, + cursor: 'not-allowed', + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', left: 0, position: 'absolute', right: 0, top: 0, - zIndex: 1, + zIndex: 3, // above Select, Checkbox and sticky table header }, })) interface LockProps { className?: string + children?: React.ReactNode } -export default function Lock({ className }: LockProps) { +export default function Lock({ children, className }: LockProps) { const classes = useStyles() - return
+ return
{children}
} diff --git a/catalog/app/containers/Admin/Table/Table.tsx b/catalog/app/containers/Admin/Table/Table.tsx index c7413991240..5745109c7eb 100644 --- a/catalog/app/containers/Admin/Table/Table.tsx +++ b/catalog/app/containers/Admin/Table/Table.tsx @@ -133,6 +133,7 @@ const useToolbarStyles = M.makeStyles((t) => ({ }, actions: { color: t.palette.text.secondary, + display: 'flex', }, title: { flex: '0 0 auto', diff --git a/catalog/app/containers/Admin/UsersAndRoles/Roles.tsx b/catalog/app/containers/Admin/UsersAndRoles/Roles.tsx index 96206205213..67abc01924b 100644 --- a/catalog/app/containers/Admin/UsersAndRoles/Roles.tsx +++ b/catalog/app/containers/Admin/UsersAndRoles/Roles.tsx @@ -18,8 +18,10 @@ import * as Form from '../Form' import * as Table from '../Table' import AttachedPolicies from './AttachedPolicies' +import SsoConfig from './SsoConfig' import { MAX_POLICIES_PER_ROLE, getArnLink } from './shared' +import HAS_SSO_CONFIG_QUERY from './gql/HasSsoConfig.generated' import ROLES_QUERY from './gql/Roles.generated' import ROLE_CREATE_MANAGED_MUTATION from './gql/RoleCreateManaged.generated' import ROLE_CREATE_UNMANAGED_MUTATION from './gql/RoleCreateUnmanaged.generated' @@ -703,6 +705,9 @@ export default function Roles() { const defaultRoleId = data.defaultRole?.id const isDefaultRoleSettingDisabled = !!data.admin?.isDefaultRoleSettingDisabled + const ssoConfigData = GQL.useQueryS(HAS_SSO_CONFIG_QUERY) + const hasSsoConfig = !!ssoConfigData.admin?.ssoConfig + const filtering = Table.useFiltering({ rows, filterBy: ({ name }) => name, @@ -713,15 +718,21 @@ export default function Roles() { }) const dialogs = Dialogs.use() - const toolbarActions = [ - { - title: 'Create', - icon: add, - fn: React.useCallback(() => { - dialogs.open(({ close }) => ) - }, [dialogs.open]), // eslint-disable-line react-hooks/exhaustive-deps - }, - ] + const createAction = { + title: 'Create', + icon: add, + fn: React.useCallback(() => { + dialogs.open(({ close }) => ) + }, [dialogs.open]), // eslint-disable-line react-hooks/exhaustive-deps + } + const ssoConfigAction = { + title: 'SSO Config', + icon: assignment_ind, + fn: React.useCallback(() => { + dialogs.open(({ close }) => ) + }, [dialogs.open]), // eslint-disable-line react-hooks/exhaustive-deps + } + const toolbarActions = hasSsoConfig ? [ssoConfigAction, createAction] : [createAction] const inlineActions = (role: Role) => [ role.arn diff --git a/catalog/app/containers/Admin/UsersAndRoles/SsoConfig.tsx b/catalog/app/containers/Admin/UsersAndRoles/SsoConfig.tsx new file mode 100644 index 00000000000..e99e26de511 --- /dev/null +++ b/catalog/app/containers/Admin/UsersAndRoles/SsoConfig.tsx @@ -0,0 +1,171 @@ +import * as FF from 'final-form' +import * as React from 'react' +import * as RF from 'react-final-form' +import * as M from '@material-ui/core' +import * as Lab from '@material-ui/lab' + +import Lock from 'components/Lock' +import { loadMode } from 'components/FileEditor/loader' +import type * as Model from 'model' +import type * as Dialogs from 'utils/GlobalDialogs' +import * as GQL from 'utils/GraphQL' +import assertNever from 'utils/assertNever' +import * as validators from 'utils/validators' + +import SET_SSO_CONFIG_MUTATION from './gql/SetSsoConfig.generated' +import SSO_CONFIG_QUERY from './gql/SsoConfig.generated' + +const TextEditor = React.lazy(() => import('components/FileEditor/TextEditor')) + +type TextFieldProps = RF.FieldRenderProps & M.TextFieldProps + +const TEXT_EDITOR_TYPE = { brace: 'yaml' as const } + +const ERRORS = { + required: 'Enter an SSO config', +} + +function TextField({ errors, input, meta }: TextFieldProps) { + // TODO: lint yaml + const errorMessage = meta.submitFailed && errors[meta.error] + return ( + + ) +} + +const useStyles = M.makeStyles((t) => ({ + lock: { + bottom: t.spacing(6.5), + top: t.spacing(8), + }, + error: { + marginTop: t.spacing(2), + }, +})) + +type FormValues = Record<'config', string> + +interface FormProps { + formApi: RF.FormRenderProps + close: Dialogs.Close + ssoConfig: Pick | null + error: null | Error +} + +function Form({ + close, + error, + ssoConfig, + formApi: { + dirtySinceLastSubmit, + handleSubmit, + hasValidationErrors, + pristine, + submitFailed, + submitting, + }, +}: FormProps) { + const classes = useStyles() + return ( + <> + + SSO role mapping config + + + } + /> + {!!error && !dirtySinceLastSubmit && ( + + {error.message} + + )} + + + close('cancel')} color="primary" disabled={submitting}> + Cancel + + + Save + + + {submitting && ( + + + + )} + + ) +} + +interface DataProps { + children: (props: FormProps) => React.ReactNode + close: Dialogs.Close +} + +function Data({ children, close }: DataProps) { + const data = GQL.useQueryS(SSO_CONFIG_QUERY) + loadMode('yaml') + const setSsoConfig = GQL.useMutation(SET_SSO_CONFIG_MUTATION) + const [error, setError] = React.useState(null) + + const onSubmit = React.useCallback( + async ({ config }: FormValues) => { + try { + if (!config) { + throw new Error('Enter an SSO config') + } + const { + admin: { setSsoConfig: r }, + } = await setSsoConfig({ config }) + switch (r.__typename) { + case 'Ok': + return close('submit') + case 'InvalidInput': + return setError(new Error('Unable to update SSO config')) + case 'OperationError': + return setError(new Error(`Unable to update SSO config: ${r.message}`)) + default: + assertNever(r) + } + } catch (e) { + return setError(e instanceof Error ? e : new Error('Error updating SSO config')) + } + }, + [close, setSsoConfig], + ) + + return ( + + {(formApi) => + children({ formApi, close, error: error, ssoConfig: data.admin?.ssoConfig }) + } + + ) +} + +interface SuspendedProps { + close: Dialogs.Close +} + +export default function Suspended({ close }: SuspendedProps) { + return ( + }> + {(props) =>
} + + ) +} diff --git a/catalog/app/containers/Admin/UsersAndRoles/gql/HasSsoConfig.generated.ts b/catalog/app/containers/Admin/UsersAndRoles/gql/HasSsoConfig.generated.ts new file mode 100644 index 00000000000..4c93b15dfbb --- /dev/null +++ b/catalog/app/containers/Admin/UsersAndRoles/gql/HasSsoConfig.generated.ts @@ -0,0 +1,57 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core' +import * as Types from '../../../../model/graphql/types.generated' + +export type containers_Admin_UsersAndRoles_gql_HasSsoConfigQueryVariables = Types.Exact<{ + [key: string]: never +}> + +export type containers_Admin_UsersAndRoles_gql_HasSsoConfigQuery = { + readonly __typename: 'Query' +} & { + readonly admin: { readonly __typename: 'AdminQueries' } & { + readonly ssoConfig: Types.Maybe< + { readonly __typename: 'SsoConfig' } & Pick + > + } +} + +export const containers_Admin_UsersAndRoles_gql_HasSsoConfigDocument = { + kind: 'Document', + definitions: [ + { + kind: 'OperationDefinition', + operation: 'query', + name: { kind: 'Name', value: 'containers_Admin_UsersAndRoles_gql_HasSsoConfig' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'admin' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'ssoConfig' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: 'timestamp' } }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + ], +} as unknown as DocumentNode< + containers_Admin_UsersAndRoles_gql_HasSsoConfigQuery, + containers_Admin_UsersAndRoles_gql_HasSsoConfigQueryVariables +> + +export { containers_Admin_UsersAndRoles_gql_HasSsoConfigDocument as default } diff --git a/catalog/app/containers/Admin/UsersAndRoles/gql/HasSsoConfig.graphql b/catalog/app/containers/Admin/UsersAndRoles/gql/HasSsoConfig.graphql new file mode 100644 index 00000000000..e24b8b0e7c6 --- /dev/null +++ b/catalog/app/containers/Admin/UsersAndRoles/gql/HasSsoConfig.graphql @@ -0,0 +1,7 @@ +query { + admin { + ssoConfig { + timestamp + } + } +} diff --git a/catalog/app/containers/Admin/UsersAndRoles/gql/SetSsoConfig.generated.ts b/catalog/app/containers/Admin/UsersAndRoles/gql/SetSsoConfig.generated.ts new file mode 100644 index 00000000000..8a23384b116 --- /dev/null +++ b/catalog/app/containers/Admin/UsersAndRoles/gql/SetSsoConfig.generated.ts @@ -0,0 +1,131 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core' +import * as Types from '../../../../model/graphql/types.generated' + +export type containers_Admin_UsersAndRoles_gql_SetSsoConfigMutationVariables = + Types.Exact<{ + config: Types.Scalars['String'] + }> + +export type containers_Admin_UsersAndRoles_gql_SetSsoConfigMutation = { + readonly __typename: 'Mutation' +} & { + readonly admin: { readonly __typename: 'AdminMutations' } & { + readonly setSsoConfig: + | { readonly __typename: 'Ok' } + | ({ readonly __typename: 'InvalidInput' } & { + readonly errors: ReadonlyArray< + { readonly __typename: 'InputError' } & Pick< + Types.InputError, + 'path' | 'message' + > + > + }) + | ({ readonly __typename: 'OperationError' } & Pick< + Types.OperationError, + 'message' + >) + } +} + +export const containers_Admin_UsersAndRoles_gql_SetSsoConfigDocument = { + kind: 'Document', + definitions: [ + { + kind: 'OperationDefinition', + operation: 'mutation', + name: { kind: 'Name', value: 'containers_Admin_UsersAndRoles_gql_SetSsoConfig' }, + variableDefinitions: [ + { + kind: 'VariableDefinition', + variable: { kind: 'Variable', name: { kind: 'Name', value: 'config' } }, + type: { + kind: 'NonNullType', + type: { kind: 'NamedType', name: { kind: 'Name', value: 'String' } }, + }, + }, + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'admin' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'setSsoConfig' }, + arguments: [ + { + kind: 'Argument', + name: { kind: 'Name', value: 'config' }, + value: { + kind: 'Variable', + name: { kind: 'Name', value: 'config' }, + }, + }, + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: '__typename' } }, + { + kind: 'InlineFragment', + typeCondition: { + kind: 'NamedType', + name: { kind: 'Name', value: 'InvalidInput' }, + }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'errors' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'path' }, + }, + { + kind: 'Field', + name: { kind: 'Name', value: 'message' }, + }, + ], + }, + }, + ], + }, + }, + { + kind: 'InlineFragment', + typeCondition: { + kind: 'NamedType', + name: { kind: 'Name', value: 'OperationError' }, + }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: 'message' } }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + ], +} as unknown as DocumentNode< + containers_Admin_UsersAndRoles_gql_SetSsoConfigMutation, + containers_Admin_UsersAndRoles_gql_SetSsoConfigMutationVariables +> + +export { containers_Admin_UsersAndRoles_gql_SetSsoConfigDocument as default } diff --git a/catalog/app/containers/Admin/UsersAndRoles/gql/SetSsoConfig.graphql b/catalog/app/containers/Admin/UsersAndRoles/gql/SetSsoConfig.graphql new file mode 100644 index 00000000000..513b4f8e7b3 --- /dev/null +++ b/catalog/app/containers/Admin/UsersAndRoles/gql/SetSsoConfig.graphql @@ -0,0 +1,16 @@ +mutation ($config: String!) { + admin { + setSsoConfig(config: $config) { + __typename + ... on InvalidInput { + errors { + path + message + } + } + ... on OperationError { + message + } + } + } +} diff --git a/catalog/app/containers/Admin/UsersAndRoles/gql/SsoConfig.generated.ts b/catalog/app/containers/Admin/UsersAndRoles/gql/SsoConfig.generated.ts new file mode 100644 index 00000000000..cc40c56e0d8 --- /dev/null +++ b/catalog/app/containers/Admin/UsersAndRoles/gql/SsoConfig.generated.ts @@ -0,0 +1,58 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core' +import * as Types from '../../../../model/graphql/types.generated' + +export type containers_Admin_UsersAndRoles_gql_SsoConfigQueryVariables = Types.Exact<{ + [key: string]: never +}> + +export type containers_Admin_UsersAndRoles_gql_SsoConfigQuery = { + readonly __typename: 'Query' +} & { + readonly admin: { readonly __typename: 'AdminQueries' } & { + readonly ssoConfig: Types.Maybe< + { readonly __typename: 'SsoConfig' } & Pick + > + } +} + +export const containers_Admin_UsersAndRoles_gql_SsoConfigDocument = { + kind: 'Document', + definitions: [ + { + kind: 'OperationDefinition', + operation: 'query', + name: { kind: 'Name', value: 'containers_Admin_UsersAndRoles_gql_SsoConfig' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'admin' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'ssoConfig' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: 'text' } }, + { kind: 'Field', name: { kind: 'Name', value: 'timestamp' } }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + ], +} as unknown as DocumentNode< + containers_Admin_UsersAndRoles_gql_SsoConfigQuery, + containers_Admin_UsersAndRoles_gql_SsoConfigQueryVariables +> + +export { containers_Admin_UsersAndRoles_gql_SsoConfigDocument as default } diff --git a/catalog/app/containers/Admin/UsersAndRoles/gql/SsoConfig.graphql b/catalog/app/containers/Admin/UsersAndRoles/gql/SsoConfig.graphql new file mode 100644 index 00000000000..9329555d511 --- /dev/null +++ b/catalog/app/containers/Admin/UsersAndRoles/gql/SsoConfig.graphql @@ -0,0 +1,8 @@ +query { + admin { + ssoConfig { + text + timestamp + } + } +} diff --git a/catalog/app/utils/GraphQL/Provider.tsx b/catalog/app/utils/GraphQL/Provider.tsx index 75197214f48..e1f1414d268 100644 --- a/catalog/app/utils/GraphQL/Provider.tsx +++ b/catalog/app/utils/GraphQL/Provider.tsx @@ -108,6 +108,7 @@ export default function GraphQLProvider({ children }: React.PropsWithChildren<{} p.bucket?.name && p.policy?.id ? `${p.bucket.name}/${p.policy.id}` : null, RoleBucketPermission: (p: any) => p.bucket?.name && p.role?.id ? `${p.bucket.name}/${p.role.id}` : null, + SsoConfig: (c) => c.timestamp as string, Status: () => null, StatusReport: (r) => (typeof r.timestamp === 'string' ? r.timestamp : null), StatusReportList: () => null, @@ -335,6 +336,10 @@ export default function GraphQLProvider({ children }: React.PropsWithChildren<{} R.evolve({ admin: { user: { list: rmUser } } }), ) } + if (result.admin?.setSsoConfig?.__typename === 'Ok') { + cache.invalidate({ __typename: 'Query' }, 'admin') + cache.invalidate({ __typename: 'Query' }, 'roles') + } }, }, }, diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 2de6b10a37c..143b6e1b55e 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -23,7 +23,7 @@ Entries inside each section should be ordered by type: ## Catalog, Lambdas * [Fixed] **SECURITY**: Remove `polyfill.io` references ([#4038](https://github.com/quiltdata/quilt/pull/4038)) * [Changed] Renamed "Admin settings" to "Admin" ([#4045](https://github.com/quiltdata/quilt/pull/4045)) -* [Added] Disable role assignment for SSO-mapped users ([#4070](https://github.com/quiltdata/quilt/pull/4070)) +* [Added] Disable role assignment for SSO-mapped users and add SSO config editor ([#4070](https://github.com/quiltdata/quilt/pull/4070), [#4072](https://github.com/quiltdata/quilt/pull/4072)) # 6.0.0a5 - 2024-06-25 ## Python API