mirror of
https://github.com/logto-io/logto.git
synced 2024-12-30 20:33:54 -05:00
Merge pull request #6766 from logto-io/yemq-log-10133-add-isVisible-to-applications-table
feat(schemas,core,console): add SAML application type
This commit is contained in:
commit
e97e63c62e
17 changed files with 177 additions and 37 deletions
|
@ -28,6 +28,14 @@ import { isPaidPlan } from '@/utils/subscription';
|
||||||
import Footer from './Footer';
|
import Footer from './Footer';
|
||||||
import styles from './index.module.scss';
|
import styles from './index.module.scss';
|
||||||
|
|
||||||
|
type AvailableApplicationTypeForCreation = Extract<
|
||||||
|
ApplicationType,
|
||||||
|
| ApplicationType.Native
|
||||||
|
| ApplicationType.SPA
|
||||||
|
| ApplicationType.Traditional
|
||||||
|
| ApplicationType.MachineToMachine
|
||||||
|
>;
|
||||||
|
|
||||||
type FormData = {
|
type FormData = {
|
||||||
type: ApplicationType;
|
type: ApplicationType;
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -160,8 +168,7 @@ function CreateForm({
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
>
|
>
|
||||||
{Object.values(ApplicationType)
|
{Object.values(ApplicationType)
|
||||||
// Other application types (e.g. "Protected") should not show up in the creation modal
|
.filter((value): value is AvailableApplicationTypeForCreation =>
|
||||||
.filter((value) =>
|
|
||||||
[
|
[
|
||||||
ApplicationType.Native,
|
ApplicationType.Native,
|
||||||
ApplicationType.SPA,
|
ApplicationType.SPA,
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import type { ApplicationType } from '@logto/schemas';
|
import { ApplicationType, Theme } from '@logto/schemas';
|
||||||
import { Theme } from '@logto/schemas';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
darkModeApplicationIconMap,
|
darkModeApplicationIconMap,
|
||||||
|
@ -16,7 +15,9 @@ type Props = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const getIcon = (type: ApplicationType, isLightMode: boolean, isThirdParty?: boolean) => {
|
const getIcon = (type: ApplicationType, isLightMode: boolean, isThirdParty?: boolean) => {
|
||||||
if (isThirdParty) {
|
// We have ensured that SAML applications are always third party in DB schema, we use `??` here to make TypeScript happy.
|
||||||
|
// TODO: @darcy fix this when SAML application <Icon /> is ready
|
||||||
|
if (isThirdParty ?? type === ApplicationType.SAML) {
|
||||||
return isLightMode ? thirdPartyApplicationIcon : thirdPartyApplicationIconDark;
|
return isLightMode ? thirdPartyApplicationIcon : thirdPartyApplicationIconDark;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { ApplicationType } from '@logto/schemas';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
import { guides } from '@/assets/docs/guides';
|
import { guides } from '@/assets/docs/guides';
|
||||||
|
@ -98,7 +99,9 @@ export const useAppGuideMetadata = (): {
|
||||||
return accumulated;
|
return accumulated;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isThirdParty) {
|
// We have ensured that SAML applications are always third party in DB schema, we use `||` here to make TypeScript happy.
|
||||||
|
// TODO: @darcy fix this when SAML third-party app guide is ready
|
||||||
|
if (target === ApplicationType.SAML || isThirdParty) {
|
||||||
return {
|
return {
|
||||||
...accumulated,
|
...accumulated,
|
||||||
[thirdPartyAppCategory]: [...accumulated[thirdPartyAppCategory], guide],
|
[thirdPartyAppCategory]: [...accumulated[thirdPartyAppCategory], guide],
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { type Application } from '@logto/schemas';
|
import { type Application, ApplicationType } from '@logto/schemas';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import ApplicationIcon from '@/components/ApplicationIcon';
|
import ApplicationIcon from '@/components/ApplicationIcon';
|
||||||
|
@ -21,7 +21,9 @@ function ApplicationPreview({ data: { id, name, isThirdParty, type } }: Props) {
|
||||||
<ItemPreview
|
<ItemPreview
|
||||||
title={name}
|
title={name}
|
||||||
subtitle={
|
subtitle={
|
||||||
isThirdParty
|
// We have ensured that SAML applications are always third party in DB schema, we use `||` here to make TypeScript happy.
|
||||||
|
// TODO: @darcy fix this when SAML app preview is ready
|
||||||
|
isThirdParty || type === ApplicationType.SAML
|
||||||
? t(`${applicationTypeI18nKey.thirdParty}.title`)
|
? t(`${applicationTypeI18nKey.thirdParty}.title`)
|
||||||
: t(`${applicationTypeI18nKey[type]}.title`)
|
: t(`${applicationTypeI18nKey[type]}.title`)
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,8 @@ import TraditionalWebAppDark from '@/assets/icons/traditional-web-app-dark.svg?r
|
||||||
import TraditionalWebApp from '@/assets/icons/traditional-web-app.svg?react';
|
import TraditionalWebApp from '@/assets/icons/traditional-web-app.svg?react';
|
||||||
|
|
||||||
type ApplicationIconMap = {
|
type ApplicationIconMap = {
|
||||||
[key in ApplicationType]: SvgComponent;
|
// TODO: @darcy Add SAML icon when we support SAML application in console
|
||||||
|
[key in Exclude<ApplicationType, ApplicationType.SAML>]: SvgComponent;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const lightModeApplicationIconMap: ApplicationIconMap = Object.freeze({
|
export const lightModeApplicationIconMap: ApplicationIconMap = Object.freeze({
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { type ApplicationResponse } from '@logto/schemas';
|
import { ApplicationType, type ApplicationResponse } from '@logto/schemas';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
@ -26,24 +26,30 @@ function GuideDrawer({ app, secrets, onClose }: Props) {
|
||||||
const { getStructuredAppGuideMetadata } = useAppGuideMetadata();
|
const { getStructuredAppGuideMetadata } = useAppGuideMetadata();
|
||||||
const [selectedGuide, setSelectedGuide] = useState<SelectedGuide>();
|
const [selectedGuide, setSelectedGuide] = useState<SelectedGuide>();
|
||||||
|
|
||||||
|
const appType = useMemo(
|
||||||
|
// TODO: @darcy Revisit this section during the implementation of the SAML app guide, note that SAML is currently treated as a Traditional app to prevent TypeScript errors (this is actually feasible since the API for creating SAML apps has not yet been enabled). However, SAML apps cannot modify OIDC config, so the final guide may differ.
|
||||||
|
() => (app.type === ApplicationType.SAML ? ApplicationType.Traditional : app.type),
|
||||||
|
[app.type]
|
||||||
|
);
|
||||||
|
|
||||||
const structuredMetadata = useMemo(
|
const structuredMetadata = useMemo(
|
||||||
() => getStructuredAppGuideMetadata({ categories: [app.type] }),
|
() => getStructuredAppGuideMetadata({ categories: [appType] }),
|
||||||
[getStructuredAppGuideMetadata, app.type]
|
[getStructuredAppGuideMetadata, appType]
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasSingleGuide = useMemo(() => {
|
const hasSingleGuide = useMemo(() => {
|
||||||
return structuredMetadata[app.type].length === 1;
|
return structuredMetadata[appType].length === 1;
|
||||||
}, [app.type, structuredMetadata]);
|
}, [appType, structuredMetadata]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasSingleGuide) {
|
if (hasSingleGuide) {
|
||||||
const guide = structuredMetadata[app.type][0];
|
const guide = structuredMetadata[appType][0];
|
||||||
if (guide) {
|
if (guide) {
|
||||||
const { id, metadata } = guide;
|
const { id, metadata } = guide;
|
||||||
setSelectedGuide({ id, metadata });
|
setSelectedGuide({ id, metadata });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [hasSingleGuide, app.type, structuredMetadata]);
|
}, [hasSingleGuide, appType, structuredMetadata]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.drawerContainer}>
|
<div className={styles.drawerContainer}>
|
||||||
|
@ -75,8 +81,8 @@ function GuideDrawer({ app, secrets, onClose }: Props) {
|
||||||
{!selectedGuide && (
|
{!selectedGuide && (
|
||||||
<GuideCardGroup
|
<GuideCardGroup
|
||||||
className={styles.cardGroup}
|
className={styles.cardGroup}
|
||||||
categoryName={t(`categories.${app.type}`)}
|
categoryName={t(`categories.${appType}`)}
|
||||||
guides={structuredMetadata[app.type]}
|
guides={structuredMetadata[appType]}
|
||||||
onClickGuide={(guide) => {
|
onClickGuide={(guide) => {
|
||||||
setSelectedGuide(guide);
|
setSelectedGuide(guide);
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -123,7 +123,9 @@ function ApplicationDetailsContent({ data, secrets, oidcConfig, onApplicationUpd
|
||||||
icon={<ApplicationIcon type={data.type} isThirdParty={data.isThirdParty} />}
|
icon={<ApplicationIcon type={data.type} isThirdParty={data.isThirdParty} />}
|
||||||
title={data.name}
|
title={data.name}
|
||||||
primaryTag={
|
primaryTag={
|
||||||
data.isThirdParty
|
// We have ensured that SAML applications are always third party in DB schema, we use `||` here to make TypeScript happy.
|
||||||
|
// TODO: @darcy fix this when we add SAML apps details page
|
||||||
|
data.isThirdParty || data.type === ApplicationType.SAML
|
||||||
? t(`${applicationTypeI18nKey.thirdParty}.title`)
|
? t(`${applicationTypeI18nKey.thirdParty}.title`)
|
||||||
: t(`${applicationTypeI18nKey[data.type]}.title`)
|
: t(`${applicationTypeI18nKey[data.type]}.title`)
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,7 +42,7 @@ type FormProps = {
|
||||||
|
|
||||||
function ConfigForm({
|
function ConfigForm({
|
||||||
ssoConnector,
|
ssoConnector,
|
||||||
applications,
|
applications: allApplications,
|
||||||
idpInitiatedAuthConfig,
|
idpInitiatedAuthConfig,
|
||||||
mutateIdpInitiatedConfig,
|
mutateIdpInitiatedConfig,
|
||||||
}: FormProps) {
|
}: FormProps) {
|
||||||
|
@ -50,6 +50,21 @@ function ConfigForm({
|
||||||
const { getTo } = useTenantPathname();
|
const { getTo } = useTenantPathname();
|
||||||
const api = useApi();
|
const api = useApi();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See definition of `applicationsSearchUrl`, there is only non-third party SPA/Traditional applications here, and SAML applications are always third party secured by DB schema, we need to manually exclude other application types here to make TypeScript happy.
|
||||||
|
*/
|
||||||
|
const applications = useMemo(
|
||||||
|
() =>
|
||||||
|
allApplications.filter(
|
||||||
|
(
|
||||||
|
application
|
||||||
|
): application is Omit<Application, 'type'> & {
|
||||||
|
type: Exclude<ApplicationType, ApplicationType.SAML>;
|
||||||
|
} => application.type !== ApplicationType.SAML
|
||||||
|
),
|
||||||
|
[allApplications]
|
||||||
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
register,
|
register,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// TODO: @darcyYe refactor this file later to remove disable max line comment
|
// TODO: @darcyYe refactor this file later to remove disable max line comment
|
||||||
|
/* eslint-disable max-lines */
|
||||||
import type { Role } from '@logto/schemas';
|
import type { Role } from '@logto/schemas';
|
||||||
import {
|
import {
|
||||||
Applications,
|
Applications,
|
||||||
|
@ -147,10 +147,14 @@ export default function applicationRoutes<T extends ManagementApiRouter>(
|
||||||
response: Applications.guard,
|
response: Applications.guard,
|
||||||
status: [200, 400, 422, 500],
|
status: [200, 400, 422, 500],
|
||||||
}),
|
}),
|
||||||
|
// eslint-disable-next-line complexity
|
||||||
async (ctx, next) => {
|
async (ctx, next) => {
|
||||||
const { oidcClientMetadata, protectedAppMetadata, ...rest } = ctx.guard.body;
|
const { oidcClientMetadata, protectedAppMetadata, ...rest } = ctx.guard.body;
|
||||||
|
|
||||||
|
if (rest.type === ApplicationType.SAML) {
|
||||||
|
throw new RequestError('application.use_saml_app_api');
|
||||||
|
}
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
rest.type === ApplicationType.MachineToMachine &&
|
rest.type === ApplicationType.MachineToMachine &&
|
||||||
quota.guardTenantUsageByKey('machineToMachineLimit'),
|
quota.guardTenantUsageByKey('machineToMachineLimit'),
|
||||||
|
@ -262,6 +266,11 @@ export default function applicationRoutes<T extends ManagementApiRouter>(
|
||||||
|
|
||||||
const { isAdmin, protectedAppMetadata, ...rest } = body;
|
const { isAdmin, protectedAppMetadata, ...rest } = body;
|
||||||
|
|
||||||
|
const pendingUpdateApplication = await queries.applications.findApplicationById(id);
|
||||||
|
if (pendingUpdateApplication.type === ApplicationType.SAML) {
|
||||||
|
throw new RequestError('application.use_saml_app_api');
|
||||||
|
}
|
||||||
|
|
||||||
// @deprecated
|
// @deprecated
|
||||||
// User can enable the admin access of Machine-to-Machine apps by switching on a toggle on Admin Console.
|
// User can enable the admin access of Machine-to-Machine apps by switching on a toggle on Admin Console.
|
||||||
// Since those apps sit in the user tenant, we provide an internal role to apply the necessary scopes.
|
// Since those apps sit in the user tenant, we provide an internal role to apply the necessary scopes.
|
||||||
|
@ -292,8 +301,7 @@ export default function applicationRoutes<T extends ManagementApiRouter>(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (protectedAppMetadata) {
|
if (protectedAppMetadata) {
|
||||||
const { type, protectedAppMetadata: originProtectedAppMetadata } =
|
const { type, protectedAppMetadata: originProtectedAppMetadata } = pendingUpdateApplication;
|
||||||
await queries.applications.findApplicationById(id);
|
|
||||||
assertThat(type === ApplicationType.Protected, 'application.protected_application_only');
|
assertThat(type === ApplicationType.Protected, 'application.protected_application_only');
|
||||||
assertThat(
|
assertThat(
|
||||||
originProtectedAppMetadata,
|
originProtectedAppMetadata,
|
||||||
|
@ -319,9 +327,10 @@ export default function applicationRoutes<T extends ManagementApiRouter>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.body = await (Object.keys(rest).length > 0
|
ctx.body =
|
||||||
? queries.applications.updateApplicationById(id, rest, 'replace')
|
Object.keys(rest).length > 0
|
||||||
: queries.applications.findApplicationById(id));
|
? await queries.applications.updateApplicationById(id, rest, 'replace')
|
||||||
|
: pendingUpdateApplication;
|
||||||
|
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
@ -337,6 +346,11 @@ export default function applicationRoutes<T extends ManagementApiRouter>(
|
||||||
async (ctx, next) => {
|
async (ctx, next) => {
|
||||||
const { id } = ctx.guard.params;
|
const { id } = ctx.guard.params;
|
||||||
const { type, protectedAppMetadata } = await queries.applications.findApplicationById(id);
|
const { type, protectedAppMetadata } = await queries.applications.findApplicationById(id);
|
||||||
|
|
||||||
|
if (type === ApplicationType.SAML) {
|
||||||
|
throw new RequestError('application.use_saml_app_api');
|
||||||
|
}
|
||||||
|
|
||||||
if (type === ApplicationType.Protected && protectedAppMetadata) {
|
if (type === ApplicationType.Protected && protectedAppMetadata) {
|
||||||
assertThat(
|
assertThat(
|
||||||
!protectedAppMetadata.customDomains || protectedAppMetadata.customDomains.length === 0,
|
!protectedAppMetadata.customDomains || protectedAppMetadata.customDomains.length === 0,
|
||||||
|
@ -359,3 +373,4 @@ export default function applicationRoutes<T extends ManagementApiRouter>(
|
||||||
|
|
||||||
applicationCustomDataRoutes(router, tenant);
|
applicationCustomDataRoutes(router, tenant);
|
||||||
}
|
}
|
||||||
|
/* eslint-enable max-lines */
|
||||||
|
|
|
@ -27,7 +27,8 @@ describe('application secrets', () => {
|
||||||
await Promise.all(applications.map(async ({ id }) => deleteApplication(id).catch(noop)));
|
await Promise.all(applications.map(async ({ id }) => deleteApplication(id).catch(noop)));
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each(Object.values(ApplicationType))(
|
// Exclude SAML app since it has different API for operations.
|
||||||
|
it.each(Object.values(ApplicationType).filter((type) => type !== ApplicationType.SAML))(
|
||||||
'should or not to create application secret for %s applications per type',
|
'should or not to create application secret for %s applications per type',
|
||||||
async (type) => {
|
async (type) => {
|
||||||
const application = await createApplication(
|
const application = await createApplication(
|
||||||
|
|
|
@ -26,7 +26,7 @@ describe('application APIs', () => {
|
||||||
expect(fetchedApplication.id).toBe(application.id);
|
expect(fetchedApplication.id).toBe(application.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw error when creating a third party application with invalid type', async () => {
|
it('should throw error when creating an OIDC third party application with invalid type', async () => {
|
||||||
await expectRejects(
|
await expectRejects(
|
||||||
createApplication('test-create-app', ApplicationType.Native, {
|
createApplication('test-create-app', ApplicationType.Native, {
|
||||||
isThirdParty: true,
|
isThirdParty: true,
|
||||||
|
@ -35,7 +35,16 @@ describe('application APIs', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create third party application successfully', async () => {
|
it('should throw error when creating a SAML application', async () => {
|
||||||
|
await expectRejects(createApplication('test-create-saml-app', ApplicationType.SAML), {
|
||||||
|
code: 'application.use_saml_app_api',
|
||||||
|
status: 400,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: add tests for blocking updating SAML application with `PATCH /applications/:id` API, we can not do it before we implement the `POST /saml-applications` API
|
||||||
|
|
||||||
|
it('should create OIDC third party application successfully', async () => {
|
||||||
const applicationName = 'test-third-party-app';
|
const applicationName = 'test-third-party-app';
|
||||||
|
|
||||||
const application = await createApplication(applicationName, ApplicationType.Traditional, {
|
const application = await createApplication(applicationName, ApplicationType.Traditional, {
|
||||||
|
|
|
@ -64,8 +64,10 @@ export type ApplicationMetadata = {
|
||||||
description: string;
|
description: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const applicationTypesMetadata = Object.entries(ApplicationType).map(([key, value]) => ({
|
export const applicationTypesMetadata = Object.entries(ApplicationType)
|
||||||
type: value,
|
.filter(([_, value]) => value !== ApplicationType.SAML)
|
||||||
name: `${key} app`,
|
.map(([key, value]) => ({
|
||||||
description: `This is a ${key} app`,
|
type: value,
|
||||||
})) satisfies ApplicationMetadata[];
|
name: `${key} app`,
|
||||||
|
description: `This is a ${key} app`,
|
||||||
|
})) satisfies ApplicationMetadata[];
|
||||||
|
|
|
@ -10,6 +10,7 @@ const application = {
|
||||||
protected_app_metadata_is_required: 'Protected app metadata is required.',
|
protected_app_metadata_is_required: 'Protected app metadata is required.',
|
||||||
protected_app_not_configured:
|
protected_app_not_configured:
|
||||||
'Protected app provider is not configured. This feature is not available for open source version.',
|
'Protected app provider is not configured. This feature is not available for open source version.',
|
||||||
|
use_saml_app_api: 'Use `[METHOD] /saml-applications(/.*)?` API to operate SAML app.',
|
||||||
cloudflare_unknown_error: 'Got unknown error when requesting Cloudflare API',
|
cloudflare_unknown_error: 'Got unknown error when requesting Cloudflare API',
|
||||||
protected_application_only: 'The feature is only available for protected applications.',
|
protected_application_only: 'The feature is only available for protected applications.',
|
||||||
protected_application_misconfigured: 'Protected application is misconfigured.',
|
protected_application_misconfigured: 'Protected application is misconfigured.',
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
import { sql } from '@silverhand/slonik';
|
||||||
|
|
||||||
|
import type { AlterationScript } from '../lib/types/alteration.js';
|
||||||
|
|
||||||
|
const alteration: AlterationScript = {
|
||||||
|
up: async (pool) => {
|
||||||
|
await pool.query(sql`
|
||||||
|
alter type application_type add value 'SAML';
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
down: async (pool) => {
|
||||||
|
await pool.query(sql`
|
||||||
|
alter table organization_application_relations drop constraint application_type;
|
||||||
|
alter table application_secrets drop constraint application_type;
|
||||||
|
alter table sso_connector_idp_initiated_auth_configs drop constraint application_type;
|
||||||
|
|
||||||
|
drop function check_application_type;
|
||||||
|
|
||||||
|
create type application_type_new as enum ('Native', 'SPA', 'Traditional', 'MachineToMachine', 'Protected');
|
||||||
|
delete from applications where "type"='SAML';
|
||||||
|
alter table applications
|
||||||
|
alter column "type" type application_type_new
|
||||||
|
using ("type"::text::application_type_new);
|
||||||
|
drop type application_type;
|
||||||
|
alter type application_type_new rename to application_type;
|
||||||
|
|
||||||
|
create function check_application_type(
|
||||||
|
application_id varchar(21),
|
||||||
|
variadic target_type application_type[]
|
||||||
|
) returns boolean as
|
||||||
|
$$ begin
|
||||||
|
return (select type from applications where id = application_id) = any(target_type);
|
||||||
|
end; $$ language plpgsql set search_path = public;
|
||||||
|
|
||||||
|
alter table organization_application_relations
|
||||||
|
add constraint application_type
|
||||||
|
check (check_application_type(application_id, 'MachineToMachine'));
|
||||||
|
|
||||||
|
alter table application_secrets
|
||||||
|
add constraint application_type
|
||||||
|
check (check_application_type(application_id, 'MachineToMachine', 'Traditional', 'Protected'));
|
||||||
|
|
||||||
|
alter table sso_connector_idp_initiated_auth_configs
|
||||||
|
add constraint application_type
|
||||||
|
check (check_application_type(default_application_id, 'Traditional', 'SPA'));
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default alteration;
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { sql } from '@silverhand/slonik';
|
||||||
|
|
||||||
|
import type { AlterationScript } from '../lib/types/alteration.js';
|
||||||
|
|
||||||
|
const alteration: AlterationScript = {
|
||||||
|
up: async (pool) => {
|
||||||
|
await pool.query(sql`
|
||||||
|
alter table applications
|
||||||
|
add constraint check_saml_app_third_party_consistency
|
||||||
|
check (type != 'SAML' OR (type = 'SAML' AND is_third_party = true));
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
down: async (pool) => {
|
||||||
|
await pool.query(sql`
|
||||||
|
alter table applications drop constraint check_saml_app_third_party_consistency;
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default alteration;
|
|
@ -6,4 +6,6 @@ export const hasSecrets = (type: ApplicationType) =>
|
||||||
ApplicationType.MachineToMachine,
|
ApplicationType.MachineToMachine,
|
||||||
ApplicationType.Protected,
|
ApplicationType.Protected,
|
||||||
ApplicationType.Traditional,
|
ApplicationType.Traditional,
|
||||||
|
// SAML applications are used as traditional web applications.
|
||||||
|
ApplicationType.SAML,
|
||||||
].includes(type);
|
].includes(type);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/* init_order = 1 */
|
/* init_order = 1 */
|
||||||
|
|
||||||
create type application_type as enum ('Native', 'SPA', 'Traditional', 'MachineToMachine', 'Protected');
|
create type application_type as enum ('Native', 'SPA', 'Traditional', 'MachineToMachine', 'Protected', 'SAML');
|
||||||
|
|
||||||
create table applications (
|
create table applications (
|
||||||
tenant_id varchar(21) not null
|
tenant_id varchar(21) not null
|
||||||
|
@ -17,7 +17,10 @@ create table applications (
|
||||||
custom_data jsonb /* @use JsonObject */ not null default '{}'::jsonb,
|
custom_data jsonb /* @use JsonObject */ not null default '{}'::jsonb,
|
||||||
is_third_party boolean not null default false,
|
is_third_party boolean not null default false,
|
||||||
created_at timestamptz not null default(now()),
|
created_at timestamptz not null default(now()),
|
||||||
primary key (id)
|
primary key (id),
|
||||||
|
constraint check_saml_app_third_party_consistency check (
|
||||||
|
type != 'SAML' OR (type = 'SAML' AND is_third_party = true)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
create index applications__id
|
create index applications__id
|
||||||
|
|
Loading…
Reference in a new issue