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