From d63757634afce86c031245add0fd450ebf23ad0b Mon Sep 17 00:00:00 2001 From: kattoczko <50657366+kattoczko@users.noreply.github.com> Date: Fri, 8 Sep 2023 13:09:53 +0100 Subject: [PATCH] feat: introduce role mappings form on authentication page (#1256) --- packages/types/index.d.ts | 9 +- .../src/components/SsoProviders/index.ee.tsx | 1 - ...psert-saml-auth-providers-role-mappings.ts | 14 ++ .../get-saml-auth-provider-role-mappings.ts | 12 + .../graphql/queries/get-saml-auth-provider.ts | 1 + packages/web/src/hooks/useSamlAuthProvider.ts | 8 +- .../hooks/useSamlAuthProviderRoleMappings.ts | 29 +++ packages/web/src/locales/en.json | 8 +- .../src/pages/Authentication/RoleMappings.tsx | 109 +++++++++ .../RoleMappingsFieldsArray.tsx | 92 ++++++++ .../Authentication/SamlConfiguration.tsx | 211 ++++++++++++++++++ .../web/src/pages/Authentication/index.tsx | 206 ++--------------- 12 files changed, 506 insertions(+), 194 deletions(-) create mode 100644 packages/web/src/graphql/mutations/upsert-saml-auth-providers-role-mappings.ts create mode 100644 packages/web/src/graphql/queries/get-saml-auth-provider-role-mappings.ts create mode 100644 packages/web/src/hooks/useSamlAuthProviderRoleMappings.ts create mode 100644 packages/web/src/pages/Authentication/RoleMappings.tsx create mode 100644 packages/web/src/pages/Authentication/RoleMappingsFieldsArray.tsx create mode 100644 packages/web/src/pages/Authentication/SamlConfiguration.tsx diff --git a/packages/types/index.d.ts b/packages/types/index.d.ts index c45d85fa..74f49837 100644 --- a/packages/types/index.d.ts +++ b/packages/types/index.d.ts @@ -430,6 +430,13 @@ type TSamlAuthProvider = { loginUrl: string; }; +type TSamlAuthProviderRole = { + id: string; + samlAuthProviderId: string; + roleId: string; + remoteRoleName: string; +}; + type AppConfig = { id: string; key: string; @@ -453,7 +460,7 @@ type Notification = { createdAt: string; documentationUrl: string; description: string; -} +}; declare module 'axios' { interface AxiosResponse { diff --git a/packages/web/src/components/SsoProviders/index.ee.tsx b/packages/web/src/components/SsoProviders/index.ee.tsx index 7617c30d..f41fd346 100644 --- a/packages/web/src/components/SsoProviders/index.ee.tsx +++ b/packages/web/src/components/SsoProviders/index.ee.tsx @@ -4,7 +4,6 @@ import Button from '@mui/material/Button'; import Stack from '@mui/material/Stack'; import Divider from '@mui/material/Divider'; -import * as URLS from 'config/urls'; import useSamlAuthProviders from 'hooks/useSamlAuthProviders.ee'; import useFormatMessage from 'hooks/useFormatMessage'; diff --git a/packages/web/src/graphql/mutations/upsert-saml-auth-providers-role-mappings.ts b/packages/web/src/graphql/mutations/upsert-saml-auth-providers-role-mappings.ts new file mode 100644 index 00000000..a50e31b3 --- /dev/null +++ b/packages/web/src/graphql/mutations/upsert-saml-auth-providers-role-mappings.ts @@ -0,0 +1,14 @@ +import { gql } from '@apollo/client'; + +export const UPSERT_SAML_AUTH_PROVIDERS_ROLE_MAPPINGS = gql` + mutation UpsertSamlAuthProvidersRoleMappings( + $input: UpsertSamlAuthProvidersRoleMappingsInput + ) { + upsertSamlAuthProvidersRoleMappings(input: $input) { + id + samlAuthProviderId + roleId + remoteRoleName + } + } +`; diff --git a/packages/web/src/graphql/queries/get-saml-auth-provider-role-mappings.ts b/packages/web/src/graphql/queries/get-saml-auth-provider-role-mappings.ts new file mode 100644 index 00000000..f13b130f --- /dev/null +++ b/packages/web/src/graphql/queries/get-saml-auth-provider-role-mappings.ts @@ -0,0 +1,12 @@ +import { gql } from '@apollo/client'; + +export const GET_SAML_AUTH_PROVIDER_ROLE_MAPPINGS = gql` + query GetSamlAuthProviderRoleMappings($id: String!) { + getSamlAuthProviderRoleMappings(id: $id) { + id + samlAuthProviderId + roleId + remoteRoleName + } + } +`; diff --git a/packages/web/src/graphql/queries/get-saml-auth-provider.ts b/packages/web/src/graphql/queries/get-saml-auth-provider.ts index 7ce6840d..26477788 100644 --- a/packages/web/src/graphql/queries/get-saml-auth-provider.ts +++ b/packages/web/src/graphql/queries/get-saml-auth-provider.ts @@ -3,6 +3,7 @@ import { gql } from '@apollo/client'; export const GET_SAML_AUTH_PROVIDER = gql` query GetSamlAuthProvider { getSamlAuthProvider { + id name certificate signatureAlgorithm diff --git a/packages/web/src/hooks/useSamlAuthProvider.ts b/packages/web/src/hooks/useSamlAuthProvider.ts index 14c3a73d..f00ca7a6 100644 --- a/packages/web/src/hooks/useSamlAuthProvider.ts +++ b/packages/web/src/hooks/useSamlAuthProvider.ts @@ -1,20 +1,22 @@ -import { useQuery } from '@apollo/client'; +import { QueryResult, useQuery } from '@apollo/client'; import { TSamlAuthProvider } from '@automatisch/types'; import { GET_SAML_AUTH_PROVIDER } from 'graphql/queries/get-saml-auth-provider'; type UseSamlAuthProviderReturn = { - provider: TSamlAuthProvider; + provider?: TSamlAuthProvider; loading: boolean; + refetch: QueryResult['refetch']; }; export default function useSamlAuthProvider(): UseSamlAuthProviderReturn { - const { data, loading } = useQuery(GET_SAML_AUTH_PROVIDER, { + const { data, loading, refetch } = useQuery(GET_SAML_AUTH_PROVIDER, { context: { autoSnackbar: false }, }); return { provider: data?.getSamlAuthProvider, loading, + refetch, }; } diff --git a/packages/web/src/hooks/useSamlAuthProviderRoleMappings.ts b/packages/web/src/hooks/useSamlAuthProviderRoleMappings.ts new file mode 100644 index 00000000..30cb68b8 --- /dev/null +++ b/packages/web/src/hooks/useSamlAuthProviderRoleMappings.ts @@ -0,0 +1,29 @@ +import * as React from 'react'; +import { useLazyQuery } from '@apollo/client'; +import { TSamlAuthProviderRole } from '@automatisch/types'; + +import { GET_SAML_AUTH_PROVIDER_ROLE_MAPPINGS } from 'graphql/queries/get-saml-auth-provider-role-mappings'; + +type QueryResponse = { + getSamlAuthProviderRoleMappings: TSamlAuthProviderRole[]; +}; + +export default function useSamlAuthProviderRoleMappings(providerId?: string) { + const [getSamlAuthProviderRoleMappings, { data, loading }] = + useLazyQuery(GET_SAML_AUTH_PROVIDER_ROLE_MAPPINGS); + + React.useEffect(() => { + if (providerId) { + getSamlAuthProviderRoleMappings({ + variables: { + id: providerId, + }, + }); + } + }, [providerId]); + + return { + roleMappings: data?.getSamlAuthProviderRoleMappings || [], + loading, + }; +} diff --git a/packages/web/src/locales/en.json b/packages/web/src/locales/en.json index 365f42ed..19e90277 100644 --- a/packages/web/src/locales/en.json +++ b/packages/web/src/locales/en.json @@ -238,5 +238,11 @@ "authenticationForm.roleAttributeName": "Role attribute name", "authenticationForm.defaultRole": "Default role", "authenticationForm.successfullySaved": "The provider has been saved.", - "authenticationForm.save": "Save" + "authenticationForm.save": "Save", + "roleMappingsForm.title": "Role mappings", + "roleMappingsForm.remoteRoleName": "Remote role name", + "roleMappingsForm.role": "Role", + "roleMappingsForm.appendRoleMapping": "Append", + "roleMappingsForm.save": "Save", + "roleMappingsForm.successfullySaved": "Role mappings have been saved." } diff --git a/packages/web/src/pages/Authentication/RoleMappings.tsx b/packages/web/src/pages/Authentication/RoleMappings.tsx new file mode 100644 index 00000000..3f4d9bc7 --- /dev/null +++ b/packages/web/src/pages/Authentication/RoleMappings.tsx @@ -0,0 +1,109 @@ +import { useMemo } from 'react'; +import Stack from '@mui/material/Stack'; +import { TSamlAuthProvider, TSamlAuthProviderRole } from '@automatisch/types'; +import Typography from '@mui/material/Typography'; +import LoadingButton from '@mui/lab/LoadingButton'; +import Divider from '@mui/material/Divider'; +import { useMutation } from '@apollo/client'; +import { useSnackbar } from 'notistack'; + +import { UPSERT_SAML_AUTH_PROVIDERS_ROLE_MAPPINGS } from 'graphql/mutations/upsert-saml-auth-providers-role-mappings'; +import useFormatMessage from 'hooks/useFormatMessage'; +import useSamlAuthProviderRoleMappings from 'hooks/useSamlAuthProviderRoleMappings'; +import Form from 'components/Form'; + +import RoleMappingsFieldArray from './RoleMappingsFieldsArray'; + +type RoleMappingsProps = { + provider?: TSamlAuthProvider; + providerLoading: boolean; +}; + +function generateFormRoleMappings(roleMappings: TSamlAuthProviderRole[]) { + if (roleMappings.length === 0) { + return [{ roleId: '', remoteRoleName: '' }]; + } + + return roleMappings.map(({ roleId, remoteRoleName }) => ({ + roleId, + remoteRoleName, + })); +} + +function RoleMappings({ provider, providerLoading }: RoleMappingsProps) { + const formatMessage = useFormatMessage(); + const { enqueueSnackbar } = useSnackbar(); + const { roleMappings, loading: roleMappingsLoading } = + useSamlAuthProviderRoleMappings(provider?.id); + const [ + upsertSamlAuthProvidersRoleMappings, + { loading: upsertRoleMappingsLoading }, + ] = useMutation(UPSERT_SAML_AUTH_PROVIDERS_ROLE_MAPPINGS); + + const handleRoleMappingsUpdate = async (values: any) => { + try { + if (provider?.id) { + await upsertSamlAuthProvidersRoleMappings({ + variables: { + input: { + samlAuthProviderId: provider.id, + samlAuthProvidersRoleMappings: values.roleMappings.map( + ({ + roleId, + remoteRoleName, + }: { + roleId: string; + remoteRoleName: string; + }) => ({ + roleId, + remoteRoleName, + }) + ), + }, + }, + }); + enqueueSnackbar(formatMessage('roleMappingsForm.successfullySaved'), { + variant: 'success', + }); + } + } catch (error) { + throw new Error('Failed while saving!'); + } + }; + + const defaultValues = useMemo( + () => ({ + roleMappings: generateFormRoleMappings(roleMappings), + }), + [roleMappings] + ); + + if (providerLoading || !provider?.id || roleMappingsLoading) { + return null; + } + + return ( + <> + + + {formatMessage('roleMappingsForm.title')} + +
+ + + + {formatMessage('roleMappingsForm.save')} + + +
+ + ); +} + +export default RoleMappings; diff --git a/packages/web/src/pages/Authentication/RoleMappingsFieldsArray.tsx b/packages/web/src/pages/Authentication/RoleMappingsFieldsArray.tsx new file mode 100644 index 00000000..60378f6c --- /dev/null +++ b/packages/web/src/pages/Authentication/RoleMappingsFieldsArray.tsx @@ -0,0 +1,92 @@ +import { useFieldArray, useFormContext } from 'react-hook-form'; +import { IRole } from '@automatisch/types'; +import MuiTextField from '@mui/material/TextField'; +import Stack from '@mui/material/Stack'; +import DeleteIcon from '@mui/icons-material/Delete'; +import IconButton from '@mui/material/IconButton'; +import Button from '@mui/material/Button'; + +import useRoles from 'hooks/useRoles.ee'; +import useFormatMessage from 'hooks/useFormatMessage'; + +import ControlledAutocomplete from 'components/ControlledAutocomplete'; +import TextField from 'components/TextField'; +import { Divider } from '@mui/material'; + +function generateRoleOptions(roles: IRole[]) { + return roles?.map(({ name: label, id: value }) => ({ label, value })); +} + +function RoleMappingsFieldArray() { + const formatMessage = useFormatMessage(); + const { control } = useFormContext(); + const { roles, loading: rolesLoading } = useRoles(); + const { fields, append, remove } = useFieldArray({ + control, + name: 'roleMappings', + }); + + const handleAppendMapping = () => append({ roleId: '', remoteRoleName: '' }); + const handleRemoveMapping = (index: number) => () => remove(index); + + return ( + <> + {fields.map((field, index) => ( +
+ + + + ( + + )} + loading={rolesLoading} + required + /> + + + + + + {index < fields.length - 1 && } +
+ ))} + + + ); +} + +export default RoleMappingsFieldArray; diff --git a/packages/web/src/pages/Authentication/SamlConfiguration.tsx b/packages/web/src/pages/Authentication/SamlConfiguration.tsx new file mode 100644 index 00000000..d0233a69 --- /dev/null +++ b/packages/web/src/pages/Authentication/SamlConfiguration.tsx @@ -0,0 +1,211 @@ +import * as React from 'react'; +import Stack from '@mui/material/Stack'; +import MuiTextField from '@mui/material/TextField'; +import LoadingButton from '@mui/lab/LoadingButton'; +import { IRole } from '@automatisch/types'; +import { useSnackbar } from 'notistack'; +import { TSamlAuthProvider } from '@automatisch/types'; +import { QueryResult, useMutation } from '@apollo/client'; + +import Form from 'components/Form'; +import TextField from 'components/TextField'; +import ControlledAutocomplete from 'components/ControlledAutocomplete'; +import Switch from 'components/Switch'; + +import { UPSERT_SAML_AUTH_PROVIDER } from 'graphql/mutations/upsert-saml-auth-provider'; +import useFormatMessage from 'hooks/useFormatMessage'; +import useRoles from 'hooks/useRoles.ee'; + +type SamlConfigurationProps = { + provider?: TSamlAuthProvider; + providerLoading: boolean; + refetchProvider: QueryResult['refetch']; +}; + +const defaultValues = { + active: false, + name: '', + certificate: '', + signatureAlgorithm: 'sha1', + issuer: '', + entryPoint: '', + firstnameAttributeName: '', + surnameAttributeName: '', + emailAttributeName: '', + roleAttributeName: '', + defaultRoleId: '', +}; + +function generateRoleOptions(roles: IRole[]) { + return roles?.map(({ name: label, id: value }) => ({ label, value })); +} + +function SamlConfiguration({ + provider, + providerLoading, + refetchProvider, +}: SamlConfigurationProps) { + const formatMessage = useFormatMessage(); + const { roles, loading: rolesLoading } = useRoles(); + const { enqueueSnackbar } = useSnackbar(); + const [upsertSamlAuthProvider, { loading }] = useMutation( + UPSERT_SAML_AUTH_PROVIDER + ); + + const handleProviderUpdate = async ( + providerDataToUpdate: Partial + ) => { + try { + const { + name, + certificate, + signatureAlgorithm, + issuer, + entryPoint, + firstnameAttributeName, + surnameAttributeName, + emailAttributeName, + roleAttributeName, + active, + defaultRoleId, + } = providerDataToUpdate; + + await upsertSamlAuthProvider({ + variables: { + input: { + name, + certificate, + signatureAlgorithm, + issuer, + entryPoint, + firstnameAttributeName, + surnameAttributeName, + emailAttributeName, + roleAttributeName, + active, + defaultRoleId, + }, + }, + }); + + if (!provider?.id) { + await refetchProvider(); + } + + enqueueSnackbar(formatMessage('authenticationForm.successfullySaved'), { + variant: 'success', + }); + } catch (error) { + throw new Error('Failed while saving!'); + } + }; + + if (providerLoading) { + return null; + } + + return ( +
+ + + + + ( + + )} + /> + + + + + + + ( + + )} + loading={rolesLoading} + /> + + {formatMessage('authenticationForm.save')} + + +
+ ); +} + +export default SamlConfiguration; diff --git a/packages/web/src/pages/Authentication/index.tsx b/packages/web/src/pages/Authentication/index.tsx index cbbd54bd..00a9417d 100644 --- a/packages/web/src/pages/Authentication/index.tsx +++ b/packages/web/src/pages/Authentication/index.tsx @@ -1,95 +1,22 @@ -import * as React from 'react'; import Grid from '@mui/material/Grid'; import Stack from '@mui/material/Stack'; -import MuiTextField from '@mui/material/TextField'; -import LoadingButton from '@mui/lab/LoadingButton'; -import { IRole } from '@automatisch/types'; -import { useSnackbar } from 'notistack'; -import { TSamlAuthProvider } from '@automatisch/types'; -import { useMutation } from '@apollo/client'; import PageTitle from 'components/PageTitle'; import Container from 'components/Container'; -import Form from 'components/Form'; -import TextField from 'components/TextField'; -import ControlledAutocomplete from 'components/ControlledAutocomplete'; -import Switch from 'components/Switch'; -import { UPSERT_SAML_AUTH_PROVIDER } from 'graphql/mutations/upsert-saml-auth-provider'; import useFormatMessage from 'hooks/useFormatMessage'; -import useRoles from 'hooks/useRoles.ee'; import useSamlAuthProvider from 'hooks/useSamlAuthProvider'; -const defaultValues = { - active: false, - name: '', - certificate: '', - signatureAlgorithm: 'sha1', - issuer: '', - entryPoint: '', - firstnameAttributeName: '', - surnameAttributeName: '', - emailAttributeName: '', - roleAttributeName: '', - defaultRoleId: '', -}; - -function generateRoleOptions(roles: IRole[]) { - return roles?.map(({ name: label, id: value }) => ({ label, value })); -} +import SamlConfiguration from './SamlConfiguration'; +import RoleMappings from './RoleMappings'; function AuthenticationPage() { const formatMessage = useFormatMessage(); - const { roles, loading: rolesLoading } = useRoles(); - const { provider, loading: providerLoading } = useSamlAuthProvider(); - const { enqueueSnackbar } = useSnackbar(); - const [upsertSamlAuthProvider, { loading }] = useMutation( - UPSERT_SAML_AUTH_PROVIDER - ); - - const handleProviderUpdate = async ( - providerDataToUpdate: Partial - ) => { - try { - const { - name, - certificate, - signatureAlgorithm, - issuer, - entryPoint, - firstnameAttributeName, - surnameAttributeName, - emailAttributeName, - roleAttributeName, - active, - defaultRoleId, - } = providerDataToUpdate; - - await upsertSamlAuthProvider({ - variables: { - input: { - name, - certificate, - signatureAlgorithm, - issuer, - entryPoint, - firstnameAttributeName, - surnameAttributeName, - emailAttributeName, - roleAttributeName, - active, - defaultRoleId, - }, - }, - }); - - enqueueSnackbar(formatMessage('authenticationForm.successfullySaved'), { - variant: 'success', - }); - } catch (error) { - throw new Error('Failed while saving!'); - } - }; + const { + provider, + loading: providerLoading, + refetch: refetchProvider, + } = useSamlAuthProvider(); return ( @@ -98,114 +25,17 @@ function AuthenticationPage() { {formatMessage('authenticationPage.title')} - {!providerLoading && ( -
- - - - - ( - - )} - /> - - - - - - - ( - - )} - loading={rolesLoading} - /> - - {formatMessage('authenticationForm.save')} - - -
- )} + + + +