From cdf5a223155ff3ad36f3472932375e5c02dbef9b Mon Sep 17 00:00:00 2001 From: simeng-li Date: Mon, 4 Dec 2023 15:07:33 +0800 Subject: [PATCH] refactor(core,schemas,console): optimize the sso connector endpoints naming (#5047) * refactor(core,schemas): rename the sso-connector-factory terms to connector-provider shouls align the terms of api. Replace the factory using provider. * refactor(core,console): rename the sso-connector-providers response property name rename the sso-connector-providers response property name * chore(core): update api doc content update api doc content * feat(core): declare the SAMLResponse field in ACS api body declare the SAMLResponse field in ACS api body * refactor(console,core): categorize standard SSO providers at client side only categorize standard SSO providers at client side only * fix(core): fix rebase issue fix rebase issue * chore(console): remove useless useMemo remove useless useMemo * chore(core): update the api content update the api content --- .../SsoConnectorRadio/index.tsx | 4 +-- .../SsoConnectorRadioGroup/index.tsx | 4 +-- .../EnterpriseSso/SsoCreationModal/index.tsx | 26 ++++++++--------- .../EnterpriseSso/SsoCreationModal/utils.ts | 24 ++++++++++++++++ packages/core/src/routes/authn.ts | 4 ++- .../routes/sso-connector/index.openapi.json | 23 ++++++++------- .../core/src/routes/sso-connector/index.ts | 28 +++++-------------- .../core/src/routes/swagger/utils/general.ts | 2 +- .../src/api/sso-connector.ts | 19 ++++--------- .../src/tests/api/sso-connectors.test.ts | 15 ++++------ packages/schemas/src/types/sso-connector.ts | 13 ++++----- packages/schemas/tables/sso_connectors.sql | 2 +- 12 files changed, 80 insertions(+), 84 deletions(-) create mode 100644 packages/console/src/pages/EnterpriseSso/SsoCreationModal/utils.ts diff --git a/packages/console/src/pages/EnterpriseSso/SsoCreationModal/SsoConnectorRadioGroup/SsoConnectorRadio/index.tsx b/packages/console/src/pages/EnterpriseSso/SsoCreationModal/SsoConnectorRadioGroup/SsoConnectorRadio/index.tsx index ccd72eb5b..7a5c74ef7 100644 --- a/packages/console/src/pages/EnterpriseSso/SsoCreationModal/SsoConnectorRadioGroup/SsoConnectorRadio/index.tsx +++ b/packages/console/src/pages/EnterpriseSso/SsoCreationModal/SsoConnectorRadioGroup/SsoConnectorRadio/index.tsx @@ -1,4 +1,4 @@ -import { type SsoConnectorFactoryDetail, Theme } from '@logto/schemas'; +import { type SsoConnectorProviderDetail, Theme } from '@logto/schemas'; import classNames from 'classnames'; import ImageWithErrorFallback from '@/ds-components/ImageWithErrorFallback'; @@ -7,7 +7,7 @@ import useTheme from '@/hooks/use-theme'; import * as styles from './index.module.scss'; type Props = { - data: SsoConnectorFactoryDetail; + data: SsoConnectorProviderDetail; }; function SsoConnectorRadio({ data: { logo, logoDark, description, name } }: Props) { diff --git a/packages/console/src/pages/EnterpriseSso/SsoCreationModal/SsoConnectorRadioGroup/index.tsx b/packages/console/src/pages/EnterpriseSso/SsoCreationModal/SsoConnectorRadioGroup/index.tsx index b63ec4cad..e71ac50e0 100644 --- a/packages/console/src/pages/EnterpriseSso/SsoCreationModal/SsoConnectorRadioGroup/index.tsx +++ b/packages/console/src/pages/EnterpriseSso/SsoCreationModal/SsoConnectorRadioGroup/index.tsx @@ -1,4 +1,4 @@ -import { type SsoConnectorFactoryDetail } from '@logto/schemas'; +import { type SsoConnectorProviderDetail } from '@logto/schemas'; import classNames from 'classnames'; import { type ConnectorRadioGroupSize } from '@/components/CreateConnectorForm/ConnectorRadioGroup'; @@ -12,7 +12,7 @@ type Props = { value?: string; className?: string; size: ConnectorRadioGroupSize; - connectors: SsoConnectorFactoryDetail[]; + connectors: SsoConnectorProviderDetail[]; onChange: (providerName: string) => void; }; diff --git a/packages/console/src/pages/EnterpriseSso/SsoCreationModal/index.tsx b/packages/console/src/pages/EnterpriseSso/SsoCreationModal/index.tsx index 119bf96b3..a7a166b24 100644 --- a/packages/console/src/pages/EnterpriseSso/SsoCreationModal/index.tsx +++ b/packages/console/src/pages/EnterpriseSso/SsoCreationModal/index.tsx @@ -1,5 +1,5 @@ import { - type SsoConnectorFactoriesResponse, + type SsoConnectorProvidersResponse, type SsoConnectorWithProviderConfig, type RequestErrorBody, } from '@logto/schemas'; @@ -24,6 +24,7 @@ import { trySubmitSafe } from '@/utils/form'; import SsoConnectorRadioGroup from './SsoConnectorRadioGroup'; import * as styles from './index.module.scss'; +import { categorizeSsoConnectorProviders } from './utils'; type Props = { isOpen: boolean; @@ -39,8 +40,8 @@ const duplicateConnectorNameErrorCode = 'single_sign_on.duplicate_connector_name function SsoCreationModal({ isOpen, onClose: rawOnClose }: Props) { const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const [selectedProviderName, setSelectedProviderName] = useState(); - const { data, error } = useSWR( - 'api/sso-connector-factories' + const { data, error } = useSWR( + 'api/sso-connector-providers' ); const { reset, @@ -54,19 +55,18 @@ function SsoCreationModal({ isOpen, onClose: rawOnClose }: Props) { const isLoading = !data && !error; - const { standardConnectors = [], providerConnectors = [] } = data ?? {}; + const { standardProviders, enterpriseProviders } = categorizeSsoConnectorProviders(data); - const radioGroupSize = useMemo( - () => getConnectorRadioGroupSize(standardConnectors.length + providerConnectors.length), - [standardConnectors, providerConnectors] + const radioGroupSize = getConnectorRadioGroupSize( + standardProviders.length + enterpriseProviders.length ); const isAnyConnectorSelected = useMemo( () => - [...standardConnectors, ...providerConnectors].some( + [...standardProviders, ...enterpriseProviders].some( ({ providerName }) => selectedProviderName === providerName ), - [providerConnectors, selectedProviderName, standardConnectors] + [enterpriseProviders, selectedProviderName, standardProviders] ); // `rawOnClose` does not clean the state of the modal. @@ -141,9 +141,9 @@ function SsoCreationModal({ isOpen, onClose: rawOnClose }: Props) { {isLoading && } {error?.message} @@ -151,9 +151,9 @@ function SsoCreationModal({ isOpen, onClose: rawOnClose }: Props) { diff --git a/packages/console/src/pages/EnterpriseSso/SsoCreationModal/utils.ts b/packages/console/src/pages/EnterpriseSso/SsoCreationModal/utils.ts new file mode 100644 index 000000000..43177166d --- /dev/null +++ b/packages/console/src/pages/EnterpriseSso/SsoCreationModal/utils.ts @@ -0,0 +1,24 @@ +import { type SsoConnectorProvidersResponse, SsoProviderName } from '@logto/schemas'; + +const standardSsoConnectorProviders = Object.freeze([SsoProviderName.OIDC, SsoProviderName.SAML]); + +export function categorizeSsoConnectorProviders(providers: SsoConnectorProvidersResponse = []): { + standardProviders: SsoConnectorProvidersResponse; + enterpriseProviders: SsoConnectorProvidersResponse; +} { + const standardProviders = new Set(); + const enterpriseProviders = new Set(); + + for (const provider of providers) { + if (standardSsoConnectorProviders.includes(provider.providerName)) { + standardProviders.add(provider); + } else { + enterpriseProviders.add(provider); + } + } + + return { + standardProviders: [...standardProviders], + enterpriseProviders: [...enterpriseProviders], + }; +} diff --git a/packages/core/src/routes/authn.ts b/packages/core/src/routes/authn.ts index 8edd18fd4..269ba500c 100644 --- a/packages/core/src/routes/authn.ts +++ b/packages/core/src/routes/authn.ts @@ -162,6 +162,8 @@ export default function authnRoutes( * @property body The SAML assertion response body. * @property body.RelayState We use this to find the connector session. * RelayState is a SAML standard parameter that will be transmitted between the identity provider and the service provider. + * @property body.SAMLResponse The SAML assertion response. + * * @returns Redirect to the redirect uri find in the connector session storage. * * @remark @@ -172,7 +174,7 @@ export default function authnRoutes( router.post( `/authn/${ssoPath}/saml/:connectorId`, koaGuard({ - body: z.object({ RelayState: z.string() }).catchall(z.unknown()), + body: z.object({ RelayState: z.string(), SAMLResponse: z.string() }).catchall(z.unknown()), params: z.object({ connectorId: z.string().min(1) }), status: [302, 404], }), diff --git a/packages/core/src/routes/sso-connector/index.openapi.json b/packages/core/src/routes/sso-connector/index.openapi.json index 9b7cd35b5..14f781ab2 100644 --- a/packages/core/src/routes/sso-connector/index.openapi.json +++ b/packages/core/src/routes/sso-connector/index.openapi.json @@ -2,22 +2,21 @@ "tags": [ { "name": "SSO connectors", - "description": "Endpoints for managing single sign-on (SSO) connectors. Your sign-in experience can use these well-configured SSO connectors to authenticate users and sync user attributes from external identity providers (IdPs).\n\nSSO connectors are created by SSO connector factories." + "description": "Endpoints for managing single sign-on (SSO) connectors. Your sign-in experience can use these well-configured SSO connectors to authenticate users and sync user attributes from external identity providers (IdPs).\n\nSSO connectors are created by SSO connector provider factories." }, { - "name": "SSO connector factories", - "description": "Endpoints for SSO (single sign-on) connector factories.\n\nSSO connector factories provide the metadata and configuration templates for creating SSO connectors." + "name": "SSO connector providers", + "description": "Endpoints for SSO (single sign-on) connector providers.\n\nSSO connector providers provide the metadata and configuration templates for creating SSO connectors." } ], "paths": { - "/api/sso-connector-factories": { - "description": "SSO connector factories are used to create Enterprise SSO connectors. The created connectors are used to connect to external SSO providers.", + "/api/sso-connector-providers": { "get": { - "summary": "Get SSO connector factories", - "description": "Returns all SSO connector factories, including standard protocol factories and pre-configured SSO providers (e.g. Google Workspace, Okta, etc.).", + "summary": "List all the supported SSO connector provider details", + "description": "Get a complete list of supported SSO connector providers.", "responses": { "200": { - "description": "An array of SSO connector factories." + "description": "A list of SSO provider data." } } } @@ -25,16 +24,16 @@ "/api/sso-connectors": { "get": { "summary": "List SSO connectors", - "description": "Get SSO connectors with pagination. In addition to the raw SSO connector data, a copy of fetched or parsed IdP configs and a copy of connector factory's data will be attached.", + "description": "Get SSO connectors with pagination. In addition to the raw SSO connector data, a copy of fetched or parsed IdP configs and a copy of connector provider's data will be attached.", "responses": { "200": { - "description": "An array of SSO connectors." + "description": "A list of SSO connectors." } } }, "post": { "summary": "Create SSO connector", - "description": "Create an new SSO connector by a specified connector factory.", + "description": "Create an new SSO connector instance for a given provider.", "responses": { "200": { "description": "The created SSO connector." @@ -48,7 +47,7 @@ "/api/sso-connectors/{id}": { "get": { "summary": "Get SSO connector", - "description": "Get SSO connector data by ID. In addition to the raw SSO connector data, a copy of fetched or parsed IdP configs and a copy of connector factory's data will be attached.", + "description": "Get SSO connector data by ID. In addition to the raw SSO connector data, a copy of fetched or parsed IdP configs and a copy of connector provider's data will be attached.", "responses": { "200": { "description": "The SSO connector data with the given ID." diff --git a/packages/core/src/routes/sso-connector/index.ts b/packages/core/src/routes/sso-connector/index.ts index b47809012..7d6e1943b 100644 --- a/packages/core/src/routes/sso-connector/index.ts +++ b/packages/core/src/routes/sso-connector/index.ts @@ -1,7 +1,6 @@ -import { SsoConnectors } from '@logto/schemas'; import { - ssoConnectorFactoriesResponseGuard, - type SsoConnectorFactoryDetail, + SsoConnectors, + ssoConnectorProvidersResponseGuard, ssoConnectorWithProviderConfigGuard, } from '@logto/schemas'; import { generateStandardShortId } from '@logto/shared'; @@ -14,7 +13,7 @@ import koaGuard from '#src/middleware/koa-guard.js'; import koaPagination from '#src/middleware/koa-pagination.js'; import koaQuotaGuard from '#src/middleware/koa-quota-guard.js'; import { ssoConnectorCreateGuard, ssoConnectorPatchGuard } from '#src/routes/sso-connector/type.js'; -import { ssoConnectorFactories, standardSsoConnectorProviders } from '#src/sso/index.js'; +import { ssoConnectorFactories } from '#src/sso/index.js'; import { isSupportedSsoProvider, isSupportedSsoConnector } from '#src/sso/utils.js'; import { tableToPathname } from '#src/utils/SchemaRouter.js'; import assertThat from '#src/utils/assert-that.js'; @@ -52,34 +51,21 @@ export default function singleSignOnRoutes(...args: Rout const pathname = `/${tableToPathname(SsoConnectors.table)}`; /* - Get all supported single sign on connector factory details + Get all supported single sign on connector provider details - standardConnectors: OIDC, SAML, etc. - providerConnectors: Google, Okta, etc. */ router.get( - '/sso-connector-factories', + '/sso-connector-providers', koaGuard({ - response: ssoConnectorFactoriesResponseGuard, + response: ssoConnectorProvidersResponseGuard, status: [200], }), async (ctx, next) => { const { locale } = ctx; const factories = Object.values(ssoConnectorFactories); - const standardConnectors = new Set(); - const providerConnectors = new Set(); - for (const factory of factories) { - if (standardSsoConnectorProviders.includes(factory.providerName)) { - standardConnectors.add(parseFactoryDetail(factory, locale)); - } else { - providerConnectors.add(parseFactoryDetail(factory, locale)); - } - } - - ctx.body = { - standardConnectors: [...standardConnectors], - providerConnectors: [...providerConnectors], - }; + ctx.body = factories.map((factory) => parseFactoryDetail(factory, locale)); return next(); } diff --git a/packages/core/src/routes/swagger/utils/general.ts b/packages/core/src/routes/swagger/utils/general.ts index b7c7ca5dc..850ebc082 100644 --- a/packages/core/src/routes/swagger/utils/general.ts +++ b/packages/core/src/routes/swagger/utils/general.ts @@ -21,7 +21,7 @@ const tagMap = new Map([ ['logs', 'Audit logs'], ['sign-in-exp', 'Sign-in experience'], ['sso-connectors', 'SSO connectors'], - ['sso-connector-factories', 'SSO connector factories'], + ['sso-connector-providers', 'SSO connector providers'], ['.well-known', 'Well-known'], ]); diff --git a/packages/integration-tests/src/api/sso-connector.ts b/packages/integration-tests/src/api/sso-connector.ts index b5dccda1f..17c99bf12 100644 --- a/packages/integration-tests/src/api/sso-connector.ts +++ b/packages/integration-tests/src/api/sso-connector.ts @@ -1,18 +1,11 @@ -import { type CreateSsoConnector, type SsoConnector } from '@logto/schemas'; +import { + type CreateSsoConnector, + type SsoConnector, + type SsoConnectorProvidersResponse, +} from '@logto/schemas'; import { authedAdminApi } from '#src/api/api.js'; -export type SsoConnectorFactoryDetail = { - providerName: string; - logo: string; - description: string; -}; - -export type ConnectorFactoryResponse = { - standardConnectors: SsoConnectorFactoryDetail[]; - providerConnectors: SsoConnectorFactoryDetail[]; -}; - export type SsoConnectorWithProviderConfig = SsoConnector & { providerLogo: string; providerLogoDark: string; @@ -20,7 +13,7 @@ export type SsoConnectorWithProviderConfig = SsoConnector & { }; export const getSsoConnectorFactories = async () => - authedAdminApi.get('sso-connector-factories').json(); + authedAdminApi.get('sso-connector-providers').json(); export const createSsoConnector = async (data: Partial) => authedAdminApi diff --git a/packages/integration-tests/src/tests/api/sso-connectors.test.ts b/packages/integration-tests/src/tests/api/sso-connectors.test.ts index 51cca6c3e..59837522f 100644 --- a/packages/integration-tests/src/tests/api/sso-connectors.test.ts +++ b/packages/integration-tests/src/tests/api/sso-connectors.test.ts @@ -16,19 +16,14 @@ import { import { expectRejects } from '#src/helpers/index.js'; describe('sso-connector library', () => { - it('should return sso-connector-factories', async () => { + it('should return sso-connector-providers', async () => { const response = await getSsoConnectorFactories(); - expect(response).toHaveProperty('standardConnectors'); - expect(response).toHaveProperty('providerConnectors'); + expect(response.length).toBeGreaterThan(0); - expect(response.standardConnectors.length).toBe(2); - expect( - response.standardConnectors.find(({ providerName }) => providerName === 'OIDC') - ).toBeDefined(); - expect( - response.standardConnectors.find(({ providerName }) => providerName === 'SAML') - ).toBeDefined(); + for (const provider of Object.values(SsoProviderName)) { + expect(response.find((data) => data.providerName === provider)).toBeDefined(); + } }); }); diff --git a/packages/schemas/src/types/sso-connector.ts b/packages/schemas/src/types/sso-connector.ts index fe72d752b..6e1b273ee 100644 --- a/packages/schemas/src/types/sso-connector.ts +++ b/packages/schemas/src/types/sso-connector.ts @@ -48,7 +48,7 @@ export type SupportedSsoConnector = Omit & { providerName: SsoProviderName; }; -const ssoConnectorFactoryDetailGuard = z.object({ +const ssoConnectorProviderDetailGuard = z.object({ providerName: z.nativeEnum(SsoProviderName), logo: z.string(), logoDark: z.string(), @@ -56,21 +56,18 @@ const ssoConnectorFactoryDetailGuard = z.object({ name: z.string(), }); -export type SsoConnectorFactoryDetail = z.infer; +export type SsoConnectorProviderDetail = z.infer; -export const ssoConnectorFactoriesResponseGuard = z.object({ - standardConnectors: z.array(ssoConnectorFactoryDetailGuard), - providerConnectors: z.array(ssoConnectorFactoryDetailGuard), -}); +export const ssoConnectorProvidersResponseGuard = z.array(ssoConnectorProviderDetailGuard); -export type SsoConnectorFactoriesResponse = z.infer; +export type SsoConnectorProvidersResponse = z.infer; // API response guard for all the SSO connectors CRUD APIs export const ssoConnectorWithProviderConfigGuard = SsoConnectors.guard .omit({ providerName: true }) .merge( z.object({ - name: z.string(), // For display purpose, generate from i18n key name defined in factory. + name: z.string(), // For display purpose, generate from i18n key name defined by SSO factory. providerName: z.nativeEnum(SsoProviderName), providerLogo: z.string(), providerLogoDark: z.string(), diff --git a/packages/schemas/tables/sso_connectors.sql b/packages/schemas/tables/sso_connectors.sql index 61250275e..14c182c22 100644 --- a/packages/schemas/tables/sso_connectors.sql +++ b/packages/schemas/tables/sso_connectors.sql @@ -4,7 +4,7 @@ create table sso_connectors ( references tenants (id) on update cascade on delete cascade, /** The globally unique identifier of the SSO connector. */ id varchar(128) not null, - /** The connector factory name of the SSO provider. */ + /** The identifier of connector's SSO provider */ provider_name varchar(128) not null, /** The name of the SSO provider for display. */ connector_name varchar(128) not null,