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:
parent
ff730acf1a
commit
cdf5a22315
12 changed files with 80 additions and 84 deletions
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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],
|
||||
};
|
||||
}
|
|
@ -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],
|
||||
}),
|
||||
|
|
|
@ -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."
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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'],
|
||||
]);
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue