0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

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
This commit is contained in:
simeng-li 2023-12-04 15:07:33 +08:00 committed by GitHub
parent ff730acf1a
commit cdf5a22315
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 80 additions and 84 deletions

View file

@ -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) {

View file

@ -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;
};

View file

@ -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<string>();
const { data, error } = useSWR<SsoConnectorFactoriesResponse, RequestError>(
'api/sso-connector-factories'
const { data, error } = useSWR<SsoConnectorProvidersResponse, RequestError>(
'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 && <Skeleton numberOfLoadingConnectors={2} />}
{error?.message}
<SsoConnectorRadioGroup
name="providerConnectors"
name="enterpriseProviders"
value={selectedProviderName}
connectors={providerConnectors}
connectors={enterpriseProviders}
size={radioGroupSize}
onChange={handleSsoSelection}
/>
@ -151,9 +151,9 @@ function SsoCreationModal({ isOpen, onClose: rawOnClose }: Props) {
<DynamicT forKey="enterprise_sso.create_modal.text_divider" />
</div>
<SsoConnectorRadioGroup
name="standardConnectors"
name="standardProviders"
value={selectedProviderName}
connectors={standardConnectors}
connectors={standardProviders}
size={radioGroupSize}
onChange={handleSsoSelection}
/>

View file

@ -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<SsoConnectorProvidersResponse[number]>();
const enterpriseProviders = new Set<SsoConnectorProvidersResponse[number]>();
for (const provider of providers) {
if (standardSsoConnectorProviders.includes(provider.providerName)) {
standardProviders.add(provider);
} else {
enterpriseProviders.add(provider);
}
}
return {
standardProviders: [...standardProviders],
enterpriseProviders: [...enterpriseProviders],
};
}

View file

@ -162,6 +162,8 @@ export default function authnRoutes<T extends AnonymousRouter>(
* @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<T extends AnonymousRouter>(
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],
}),

View file

@ -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."

View file

@ -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<T extends AuthedRouter>(...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<SsoConnectorFactoryDetail>();
const providerConnectors = new Set<SsoConnectorFactoryDetail>();
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();
}

View file

@ -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'],
]);

View file

@ -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<ConnectorFactoryResponse>();
authedAdminApi.get('sso-connector-providers').json<SsoConnectorProvidersResponse>();
export const createSsoConnector = async (data: Partial<CreateSsoConnector>) =>
authedAdminApi

View file

@ -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();
}
});
});

View file

@ -48,7 +48,7 @@ export type SupportedSsoConnector = Omit<SsoConnector, 'providerName'> & {
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<typeof ssoConnectorFactoryDetailGuard>;
export type SsoConnectorProviderDetail = z.infer<typeof ssoConnectorProviderDetailGuard>;
export const ssoConnectorFactoriesResponseGuard = z.object({
standardConnectors: z.array(ssoConnectorFactoryDetailGuard),
providerConnectors: z.array(ssoConnectorFactoryDetailGuard),
});
export const ssoConnectorProvidersResponseGuard = z.array(ssoConnectorProviderDetailGuard);
export type SsoConnectorFactoriesResponse = z.infer<typeof ssoConnectorFactoriesResponseGuard>;
export type SsoConnectorProvidersResponse = z.infer<typeof ssoConnectorProvidersResponseGuard>;
// 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(),

View file

@ -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,