diff --git a/packages/console/src/pages/OrganizationDetails/Settings/index.module.scss b/packages/console/src/pages/OrganizationDetails/Settings/index.module.scss index de27779b9..41873ee06 100644 --- a/packages/console/src/pages/OrganizationDetails/Settings/index.module.scss +++ b/packages/console/src/pages/OrganizationDetails/Settings/index.module.scss @@ -4,16 +4,6 @@ margin-top: _.unit(3); } -.membershipDescription { - font: var(--font-body-2); - color: var(--color-text-secondary); - margin-top: _.unit(1.5); -} - -.emailDomains { - margin-top: _.unit(1); -} - .warning { margin-top: _.unit(3); } diff --git a/packages/console/src/pages/OrganizationDetails/Settings/index.tsx b/packages/console/src/pages/OrganizationDetails/Settings/index.tsx index 351ee65ed..2645c3e92 100644 --- a/packages/console/src/pages/OrganizationDetails/Settings/index.tsx +++ b/packages/console/src/pages/OrganizationDetails/Settings/index.tsx @@ -1,5 +1,6 @@ import { type SignInExperience, type Organization } from '@logto/schemas'; import { trySafe } from '@silverhand/essentials'; +import { useState } from 'react'; import { Controller, useForm } from 'react-hook-form'; import { toast } from 'react-hot-toast'; import { useTranslation } from 'react-i18next'; @@ -9,11 +10,13 @@ import useSWR from 'swr'; import DetailsForm from '@/components/DetailsForm'; import FormCard from '@/components/FormCard'; import MultiOptionInput from '@/components/MultiOptionInput'; +import OrganizationRolesSelect from '@/components/OrganizationRolesSelect'; import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal'; import { isDevFeaturesEnabled } from '@/consts/env'; import CodeEditor from '@/ds-components/CodeEditor'; import FormField from '@/ds-components/FormField'; import InlineNotification from '@/ds-components/InlineNotification'; +import { type Option } from '@/ds-components/Select/MultiSelect'; import Switch from '@/ds-components/Switch'; import TextInput from '@/ds-components/TextInput'; import useApi, { type RequestError } from '@/hooks/use-api'; @@ -27,6 +30,7 @@ import * as styles from './index.module.scss'; type FormData = Partial & { customData: string }> & { isJitEnabled: boolean; jitEmailDomains: string[]; + jitRoles: Array>; }; const isJsonObject = (value: string) => { @@ -34,10 +38,14 @@ const isJsonObject = (value: string) => { return Boolean(parsed && typeof parsed === 'object'); }; -const normalizeData = (data: Organization, emailDomains: string[]): FormData => ({ +const normalizeData = ( + data: Organization, + jit: { emailDomains: string[]; roles: Array> } +): FormData => ({ ...data, - isJitEnabled: emailDomains.length > 0, - jitEmailDomains: emailDomains, + isJitEnabled: jit.emailDomains.length > 0 || jit.roles.length > 0, + jitEmailDomains: jit.emailDomains, + jitRoles: jit.roles, customData: JSON.stringify(data.customData, undefined, 2), }); @@ -53,8 +61,7 @@ const assembleData = ({ }); function Settings() { - const { isDeleting, data, emailDomains, onUpdated } = - useOutletContext(); + const { isDeleting, data, jit, onUpdated } = useOutletContext(); const { data: signInExperience } = useSWR('api/sign-in-exp'); const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const { @@ -67,13 +74,14 @@ function Settings() { clearErrors, watch, } = useForm({ - defaultValues: normalizeData( - data, - emailDomains.map(({ emailDomain }) => emailDomain) - ), + defaultValues: normalizeData(data, { + emailDomains: jit.emailDomains.map(({ emailDomain }) => emailDomain), + roles: jit.roles.map(({ id, name }) => ({ value: id, title: name })), + }), }); const [isJitEnabled, isMfaRequired] = watch(['isJitEnabled', 'isMfaRequired']); const api = useApi(); + const [keyword, setKeyword] = useState(''); const onSubmit = handleSubmit( trySubmitSafe(async (data) => { @@ -82,17 +90,23 @@ function Settings() { } const emailDomains = data.isJitEnabled ? data.jitEmailDomains : []; + const roles = data.isJitEnabled ? data.jitRoles : []; const updatedData = await api .patch(`api/organizations/${data.id}`, { json: assembleData(data), }) .json(); - await api.put(`api/organizations/${data.id}/email-domains`, { - json: { emailDomains }, - }); + await Promise.all([ + api.put(`api/organizations/${data.id}/jit/email-domains`, { + json: { emailDomains }, + }), + api.put(`api/organizations/${data.id}/jit/roles`, { + json: { organizationRoleIds: roles.map(({ value }) => value) }, + }), + ]); - reset(normalizeData(updatedData, emailDomains)); + reset(normalizeData(updatedData, { emailDomains, roles })); toast.success(t('general.saved')); onUpdated(updatedData); }) @@ -139,57 +153,73 @@ function Settings() { /> - - -
- -
-
- {isJitEnabled && ( - -

- {t('organization_details.jit.membership_description')} -

- ( - value} - validateInput={(input) => { - if (!domainRegExp.test(input)) { - return t('organization_details.jit.invalid_domain'); - } - - if (value.includes(input)) { - return t('organization_details.jit.domain_already_added'); - } - - return { value: input }; - }} - placeholder={t('organization_details.jit.email_domains_placeholder')} - error={errors.jitEmailDomains?.message} - onChange={onChange} - onError={(error) => { - setError('jitEmailDomains', { type: 'custom', message: error }); - }} - onClearError={() => { - clearErrors('jitEmailDomains'); - }} - /> - )} - /> + {isDevFeaturesEnabled && ( + + +
+ +
- )} - {isDevFeaturesEnabled && ( + {isJitEnabled && ( + + ( + value} + validateInput={(input) => { + if (!domainRegExp.test(input)) { + return t('organization_details.jit.invalid_domain'); + } + + if (value.includes(input)) { + return t('organization_details.jit.domain_already_added'); + } + + return { value: input }; + }} + placeholder={t('organization_details.jit.email_domains_placeholder')} + error={errors.jitEmailDomains?.message} + onChange={onChange} + onError={(error) => { + setError('jitEmailDomains', { type: 'custom', message: error }); + }} + onClearError={() => { + clearErrors('jitEmailDomains'); + }} + /> + )} + /> + + )} + {isJitEnabled && ( + + ( + + )} + /> + + )} )} - )} -
+
+ )} ); diff --git a/packages/console/src/pages/OrganizationDetails/index.tsx b/packages/console/src/pages/OrganizationDetails/index.tsx index 57e93cf7d..fb4ccfd2e 100644 --- a/packages/console/src/pages/OrganizationDetails/index.tsx +++ b/packages/console/src/pages/OrganizationDetails/index.tsx @@ -1,4 +1,8 @@ -import { type OrganizationEmailDomain, type Organization } from '@logto/schemas'; +import { + type OrganizationJitEmailDomain, + type Organization, + type OrganizationRole, +} from '@logto/schemas'; import { useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Outlet, useParams } from 'react-router-dom'; @@ -31,8 +35,11 @@ function OrganizationDetails() { const { navigate } = useTenantPathname(); const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const organization = useSWR(id && `api/organizations/${id}`); - const emailDomains = useSWR( - id && `api/organizations/${id}/email-domains` + const jitEmailDomains = useSWR( + id && `api/organizations/${id}/jit/email-domains` + ); + const jitRoles = useSWR( + id && `api/organizations/${id}/jit/roles` ); const [isDeleting, setIsDeleting] = useState(false); const [isGuideDrawerOpen, setIsGuideDrawerOpen] = useState(false); @@ -54,15 +61,17 @@ function OrganizationDetails() { }, [api, id, isDeleting, navigate]); const isLoading = - (!organization.data && !organization.error) || (!emailDomains.data && !emailDomains.error); - const error = organization.error ?? emailDomains.error; + (!organization.data && !organization.error) || + (!jitEmailDomains.data && !jitEmailDomains.error) || + (!jitRoles.data && !jitRoles.error); + const error = organization.error ?? jitEmailDomains.error ?? jitRoles.error; return ( {isLoading && } {error && } - {id && organization.data && emailDomains.data && ( + {id && organization.data && jitEmailDomains.data && jitRoles.data && ( <> } @@ -118,7 +127,10 @@ function OrganizationDetails() { context={ { data: organization.data, - emailDomains: emailDomains.data, + jit: { + emailDomains: jitEmailDomains.data, + roles: jitRoles.data, + }, isDeleting, onUpdated: async (data) => organization.mutate(data), } satisfies OrganizationDetailsOutletContext diff --git a/packages/console/src/pages/OrganizationDetails/types.ts b/packages/console/src/pages/OrganizationDetails/types.ts index f9f8c2340..c6702ce51 100644 --- a/packages/console/src/pages/OrganizationDetails/types.ts +++ b/packages/console/src/pages/OrganizationDetails/types.ts @@ -1,8 +1,15 @@ -import { type OrganizationEmailDomain, type Organization } from '@logto/schemas'; +import { + type OrganizationJitEmailDomain, + type Organization, + type OrganizationRole, +} from '@logto/schemas'; export type OrganizationDetailsOutletContext = { data: Organization; - emailDomains: OrganizationEmailDomain[]; + jit: { + emailDomains: OrganizationJitEmailDomain[]; + roles: OrganizationRole[]; + }; /** * Whether the organization is being deleted, this is used to disable the unsaved * changes alert modal. diff --git a/packages/core/src/libraries/user.ts b/packages/core/src/libraries/user.ts index 5a766e686..15cf30095 100644 --- a/packages/core/src/libraries/user.ts +++ b/packages/core/src/libraries/user.ts @@ -148,9 +148,8 @@ export const createUserLibrary = (queries: Queries) => { const userEmailDomain = data.primaryEmail?.split('@')[1]; if (userEmailDomain) { const organizationQueries = new OrganizationQueries(connection); - const organizationIds = await organizationQueries.emailDomains.getOrganizationIdsByDomain( - userEmailDomain - ); + const organizationIds = + await organizationQueries.jit.emailDomains.getOrganizationIdsByDomain(userEmailDomain); if (organizationIds.length > 0) { await organizationQueries.relations.users.insert( diff --git a/packages/core/src/queries/organization/email-domains.ts b/packages/core/src/queries/organization/email-domains.ts index f772662b3..4411145cb 100644 --- a/packages/core/src/queries/organization/email-domains.ts +++ b/packages/core/src/queries/organization/email-domains.ts @@ -1,7 +1,7 @@ import { - type OrganizationEmailDomain, - OrganizationEmailDomains, - type CreateOrganizationEmailDomain, + type OrganizationJitEmailDomain, + OrganizationJitEmailDomains, + type CreateOrganizationJitEmailDomain, } from '@logto/schemas'; import { type CommonQueryMethods, sql } from '@silverhand/slonik'; @@ -10,15 +10,15 @@ import { DeletionError } from '#src/errors/SlonikError/index.js'; import { type GetEntitiesOptions } from '#src/utils/RelationQueries.js'; import { type OmitAutoSetFields, conditionalSql, convertToIdentifiers } from '#src/utils/sql.js'; -const { table, fields } = convertToIdentifiers(OrganizationEmailDomains); +const { table, fields } = convertToIdentifiers(OrganizationJitEmailDomains); export class EmailDomainQueries { readonly #insert: ( - data: OmitAutoSetFields - ) => Promise>; + data: OmitAutoSetFields + ) => Promise>; constructor(protected pool: CommonQueryMethods) { - this.#insert = buildInsertIntoWithPool(this.pool)(OrganizationEmailDomains, { + this.#insert = buildInsertIntoWithPool(this.pool)(OrganizationJitEmailDomains, { returning: true, }); } @@ -26,7 +26,7 @@ export class EmailDomainQueries { async getEntities( organizationId: string, options: GetEntitiesOptions - ): Promise<[number, readonly OrganizationEmailDomain[]]> { + ): Promise<[number, readonly OrganizationJitEmailDomain[]]> { const { limit, offset } = options; const mainSql = sql` from ${table} @@ -38,7 +38,7 @@ export class EmailDomainQueries { select count(*) ${mainSql} `), - this.pool.any(sql` + this.pool.any(sql` select ${sql.join(Object.values(fields), sql`, `)} ${mainSql} ${conditionalSql(limit, (limit) => sql`limit ${limit}`)} @@ -50,7 +50,7 @@ export class EmailDomainQueries { } async getOrganizationIdsByDomain(emailDomain: string): Promise { - const rows = await this.pool.any>(sql` + const rows = await this.pool.any>(sql` select ${fields.organizationId} from ${table} where ${fields.emailDomain} = ${emailDomain} @@ -58,7 +58,7 @@ export class EmailDomainQueries { return rows.map((row) => row.organizationId); } - async insert(organizationId: string, emailDomain: string): Promise { + async insert(organizationId: string, emailDomain: string): Promise { return this.#insert({ organizationId, emailDomain, @@ -73,7 +73,7 @@ export class EmailDomainQueries { `); if (rowCount < 1) { - throw new DeletionError(OrganizationEmailDomains.table); + throw new DeletionError(OrganizationJitEmailDomains.table); } } diff --git a/packages/core/src/queries/organization/index.ts b/packages/core/src/queries/organization/index.ts index a789c228b..32959af2b 100644 --- a/packages/core/src/queries/organization/index.ts +++ b/packages/core/src/queries/organization/index.ts @@ -21,6 +21,7 @@ import { Scopes, Resources, Users, + OrganizationJitRoles, } from '@logto/schemas'; import { sql, type CommonQueryMethods } from '@silverhand/slonik'; @@ -290,8 +291,16 @@ export default class OrganizationQueries extends SchemaQueries< ), }; - /** Queries for email domains that will be automatically provisioned. */ - emailDomains = new EmailDomainQueries(this.pool); + jit = { + /** Queries for email domains that are used for just-in-time provisioning. */ + emailDomains: new EmailDomainQueries(this.pool), + roles: new TwoRelationsQueries( + this.pool, + OrganizationJitRoles.table, + Organizations, + OrganizationRoles + ), + }; constructor(pool: CommonQueryMethods) { super(pool, Organizations); diff --git a/packages/core/src/routes/organization/index.email-domain.openapi.json b/packages/core/src/routes/organization/index.jit.email-domains.openapi.json similarity index 86% rename from packages/core/src/routes/organization/index.email-domain.openapi.json rename to packages/core/src/routes/organization/index.jit.email-domains.openapi.json index b48e1e6e2..aee509ad4 100644 --- a/packages/core/src/routes/organization/index.email-domain.openapi.json +++ b/packages/core/src/routes/organization/index.jit.email-domains.openapi.json @@ -5,9 +5,9 @@ } ], "paths": { - "/api/organizations/{id}/email-domains": { + "/api/organizations/{id}/jit/email-domains": { "get": { - "summary": "Get organization email domains", + "summary": "Get organization JIT email domains", "description": "Get email domains for just-in-time provisioning of users in the organization.", "responses": { "200": { @@ -16,7 +16,7 @@ } }, "post": { - "summary": "Add organization email domain", + "summary": "Add organization JIT email domain", "description": "Add a new email domain for just-in-time provisioning of users in the organization.", "requestBody": { "content": { @@ -41,7 +41,7 @@ } }, "put": { - "summary": "Replace organization email domains", + "summary": "Replace organization JIT email domains", "description": "Replace all just-in-time provisioning email domains for the organization with the given data.", "requestBody": { "content": { @@ -63,9 +63,9 @@ } } }, - "/api/organizations/{id}/email-domains/{emailDomain}": { + "/api/organizations/{id}/jit/email-domains/{emailDomain}": { "delete": { - "summary": "Remove organization email domain", + "summary": "Remove organization JIT email domain", "description": "Remove an email domain for just-in-time provisioning of users in the organization.", "parameters": [ { diff --git a/packages/core/src/routes/organization/index.email-domains.ts b/packages/core/src/routes/organization/index.jit.email-domains.ts similarity index 77% rename from packages/core/src/routes/organization/index.email-domains.ts rename to packages/core/src/routes/organization/index.jit.email-domains.ts index e60bf0b14..37e79f65d 100644 --- a/packages/core/src/routes/organization/index.email-domains.ts +++ b/packages/core/src/routes/organization/index.jit.email-domains.ts @@ -1,4 +1,4 @@ -import { OrganizationEmailDomains } from '@logto/schemas'; +import { OrganizationJitEmailDomains } from '@logto/schemas'; import type Router from 'koa-router'; import { z } from 'zod'; @@ -12,21 +12,21 @@ export default function emailDomainRoutes( organizations: OrganizationQueries ) { const params = Object.freeze({ id: z.string().min(1) }); - const pathname = '/:id/email-domains'; + const pathname = '/:id/jit/email-domains'; router.get( pathname, koaPagination(), koaGuard({ params: z.object(params), - response: OrganizationEmailDomains.guard.array(), + response: OrganizationJitEmailDomains.guard.array(), status: [200], }), async (ctx, next) => { const { id } = ctx.guard.params; const { limit, offset } = ctx.pagination; - const [count, rows] = await organizations.emailDomains.getEntities(id, { limit, offset }); + const [count, rows] = await organizations.jit.emailDomains.getEntities(id, { limit, offset }); ctx.pagination.totalCount = count; ctx.body = rows; return next(); @@ -38,14 +38,14 @@ export default function emailDomainRoutes( koaGuard({ params: z.object(params), body: z.object({ emailDomain: z.string().min(1) }), - response: OrganizationEmailDomains.guard, + response: OrganizationJitEmailDomains.guard, status: [201], }), async (ctx, next) => { const { id } = ctx.guard.params; const { emailDomain } = ctx.guard.body; - ctx.body = await organizations.emailDomains.insert(id, emailDomain); + ctx.body = await organizations.jit.emailDomains.insert(id, emailDomain); ctx.status = 201; return next(); } @@ -62,7 +62,7 @@ export default function emailDomainRoutes( const { id } = ctx.guard.params; const { emailDomains } = ctx.guard.body; - await organizations.emailDomains.replace(id, emailDomains); + await organizations.jit.emailDomains.replace(id, emailDomains); ctx.status = 204; return next(); } @@ -77,7 +77,7 @@ export default function emailDomainRoutes( async (ctx, next) => { const { id, emailDomain } = ctx.guard.params; - await organizations.emailDomains.delete(id, emailDomain); + await organizations.jit.emailDomains.delete(id, emailDomain); ctx.status = 204; return next(); } diff --git a/packages/core/src/routes/organization/index.jit.roles.openapi.json b/packages/core/src/routes/organization/index.jit.roles.openapi.json new file mode 100644 index 000000000..bbd4333dc --- /dev/null +++ b/packages/core/src/routes/organization/index.jit.roles.openapi.json @@ -0,0 +1,78 @@ +{ + "tags": [ + { + "name": "Organizations" + } + ], + "paths": { + "/api/organizations/{id}/jit/roles": { + "get": { + "summary": "Get organization JIT roles", + "description": "Get organization roles that will be assigned to users during just-in-time provisioning.", + "responses": { + "200": { + "description": "A list of organization roles." + } + } + }, + "post": { + "summary": "Add organization JIT role", + "description": "Add a new organization role that will be assigned to users during just-in-time provisioning.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "properties": { + "organizationRoleId": { + "description": "The organization role ID to add." + } + } + } + } + } + }, + "responses": { + "201": { + "description": "The organization role was added successfully." + }, + "422": { + "description": "The organization role is already in use." + } + } + }, + "put": { + "summary": "Replace organization JIT roles", + "description": "Replace all organization roles that will be assigned to users during just-in-time provisioning with the given data.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "properties": { + "organizationRoleIds": { + "description": "An array of organization role IDs to replace existing organization roles." + } + } + } + } + } + }, + "responses": { + "204": { + "description": "The organization roles were replaced successfully." + } + } + } + }, + "/api/organizations/{id}/jit/roles/{organizationRoleId}": { + "delete": { + "summary": "Remove organization JIT role", + "description": "Remove an organization role that will be assigned to users during just-in-time provisioning.", + "responses": { + "204": { + "description": "The organization role was removed successfully." + } + } + } + } + } +} diff --git a/packages/core/src/routes/organization/index.ts b/packages/core/src/routes/organization/index.ts index e15d2fcdc..c7b37a0f0 100644 --- a/packages/core/src/routes/organization/index.ts +++ b/packages/core/src/routes/organization/index.ts @@ -16,7 +16,7 @@ import { parseSearchOptions } from '#src/utils/search.js'; import { type ManagementApiRouter, type RouterInitArgs } from '../types.js'; -import emailDomainRoutes from './index.email-domains.js'; +import emailDomainRoutes from './index.jit.email-domains.js'; import userRoleRelationRoutes from './index.user-role-relations.js'; import organizationInvitationRoutes from './invitations.js'; import organizationRoleRoutes from './roles.js'; @@ -138,7 +138,10 @@ export default function organizationRoutes( ); userRoleRelationRoutes(router, organizations); + + // MARK: Just-in-time provisioning emailDomainRoutes(router, organizations); + router.addRelationRoutes(organizations.jit.roles, 'jit/roles'); // MARK: Mount sub-routes organizationRoleRoutes(...args); diff --git a/packages/integration-tests/src/api/organization.ts b/packages/integration-tests/src/api/organization.ts index 9fc1682fd..49551f5fc 100644 --- a/packages/integration-tests/src/api/organization.ts +++ b/packages/integration-tests/src/api/organization.ts @@ -5,7 +5,7 @@ import { type UserWithOrganizationRoles, type OrganizationWithFeatured, type OrganizationScope, - type OrganizationEmailDomain, + type OrganizationJitEmailDomain, type CreateOrganization, } from '@logto/schemas'; @@ -82,7 +82,7 @@ export class OrganizationApi extends ApiFactory { + ): Promise { const searchParams = new URLSearchParams(); if (page) { @@ -94,19 +94,19 @@ export class OrganizationApi extends ApiFactory(); + .get(`${this.path}/${id}/jit/email-domains`, { searchParams }) + .json(); } async addEmailDomain(id: string, emailDomain: string): Promise { - await authedAdminApi.post(`${this.path}/${id}/email-domains`, { json: { emailDomain } }); + await authedAdminApi.post(`${this.path}/${id}/jit/email-domains`, { json: { emailDomain } }); } async deleteEmailDomain(id: string, emailDomain: string): Promise { - await authedAdminApi.delete(`${this.path}/${id}/email-domains/${emailDomain}`); + await authedAdminApi.delete(`${this.path}/${id}/jit/email-domains/${emailDomain}`); } async replaceEmailDomains(id: string, emailDomains: string[]): Promise { - await authedAdminApi.put(`${this.path}/${id}/email-domains`, { json: { emailDomains } }); + await authedAdminApi.put(`${this.path}/${id}/jit/email-domains`, { json: { emailDomains } }); } } diff --git a/packages/phrases/src/locales/en/translation/admin-console/organization-details.ts b/packages/phrases/src/locales/en/translation/admin-console/organization-details.ts index f4b48fe17..728c81765 100644 --- a/packages/phrases/src/locales/en/translation/admin-console/organization-details.ts +++ b/packages/phrases/src/locales/en/translation/admin-console/organization-details.ts @@ -30,15 +30,16 @@ const organization_details = { membership_policies_description: 'Define how users can join this organization and what requirements they must meet for access.', jit: { + title: 'Enable just-in-time provisioning', description: - 'Enable automatic membership assignment based on verified email domains and default roles assignment.', - membership_description: - 'Automatically assign users into this organization when they sign up or are added through the Management API, provided their verified email addresses match the specified domains.', - is_enabled_title: 'Enable just-in-time provisioning', - email_domain_provisioning: 'Email domain provisioning', + 'Users can automatically join the organization and receive role assignments if their email matches specific domains, either during sign-up or when added via the Management API.', + email_domains: 'JIT provisioning email domains', email_domains_placeholder: 'Enter email domains for just-in-time provisioning', invalid_domain: 'Invalid domain', domain_already_added: 'Domain already added', + organization_roles: 'Default organization roles', + organization_roles_description: + 'Assign roles to users upon joining the organization through just-in-time provisioning.', }, mfa: { title: 'Multi-factor authentication (MFA)', diff --git a/packages/schemas/alterations/next-1718340884-rename-org-email-domains-and-add-jit-roles-table.ts b/packages/schemas/alterations/next-1718340884-rename-org-email-domains-and-add-jit-roles-table.ts new file mode 100644 index 000000000..1e22f30de --- /dev/null +++ b/packages/schemas/alterations/next-1718340884-rename-org-email-domains-and-add-jit-roles-table.ts @@ -0,0 +1,56 @@ +import { sql } from '@silverhand/slonik'; + +import type { AlterationScript } from '../lib/types/alteration.js'; + +import { applyTableRls, dropTableRls } from './utils/1704934999-tables.js'; + +const alteration: AlterationScript = { + up: async (pool) => { + await pool.query(sql` + alter table organization_email_domains rename to organization_jit_email_domains; + alter table organization_jit_email_domains + rename constraint organization_email_domains_organization_id_fkey to organization_jit_email_domains_organization_id_fkey; + alter table organization_jit_email_domains + rename constraint organization_email_domains_pkey to organization_jit_email_domains_pkey; + alter table organization_jit_email_domains + rename constraint organization_email_domains_tenant_id_fkey to organization_jit_email_domains_tenant_id_fkey; + alter policy organization_email_domains_modification + on organization_jit_email_domains rename to organization_jit_email_domains_modification; + alter policy organization_email_domains_tenant_id + on organization_jit_email_domains rename to organization_jit_email_domains_tenant_id; + create table organization_jit_roles ( + tenant_id varchar(21) not null + references tenants (id) on update cascade on delete cascade, + /** The ID of the organization. */ + organization_id varchar(21) not null + references organizations (id) on update cascade on delete cascade, + /** The organization role ID that will be automatically provisioned. */ + organization_role_id varchar(21) not null + references organization_roles (id) on update cascade on delete cascade, + primary key (tenant_id, organization_id, organization_role_id) + ); + `); + await applyTableRls(pool, 'organization_jit_roles'); + }, + down: async (pool) => { + await dropTableRls(pool, 'organization_jit_roles'); + await pool.query(sql` + drop table organization_jit_roles + `); + await pool.query(sql` + alter table organization_jit_email_domains rename to organization_email_domains; + alter table organization_email_domains + rename constraint organization_jit_email_domains_organization_id_fkey to organization_email_domains_organization_id_fkey; + alter table organization_email_domains + rename constraint organization_jit_email_domains_pkey to organization_email_domains_pkey; + alter table organization_email_domains + rename constraint organization_jit_email_domains_tenant_id_fkey to organization_email_domains_tenant_id_fkey; + alter policy organization_jit_email_domains_modification + on organization_email_domains rename to organization_email_domains_modification; + alter policy organization_jit_email_domains_tenant_id + on organization_email_domains rename to organization_email_domains_tenant_id; + `); + }, +}; + +export default alteration; diff --git a/packages/schemas/tables/organization_email_domains.sql b/packages/schemas/tables/organization_jit_email_domains.sql similarity index 69% rename from packages/schemas/tables/organization_email_domains.sql rename to packages/schemas/tables/organization_jit_email_domains.sql index 63c37332f..94b3e154d 100644 --- a/packages/schemas/tables/organization_email_domains.sql +++ b/packages/schemas/tables/organization_jit_email_domains.sql @@ -1,7 +1,7 @@ /* init_order = 2 */ -/** The email domains that will be automatically provisioned for an organization. */ -create table organization_email_domains ( +/** The email domains that will automatically assign users into an organization when they sign up or are added through the Management API. */ +create table organization_jit_email_domains ( tenant_id varchar(21) not null references tenants (id) on update cascade on delete cascade, /** The ID of the organization. */ diff --git a/packages/schemas/tables/organization_jit_roles.sql b/packages/schemas/tables/organization_jit_roles.sql new file mode 100644 index 000000000..c5ac603f9 --- /dev/null +++ b/packages/schemas/tables/organization_jit_roles.sql @@ -0,0 +1,14 @@ +/* init_order = 2 */ + +/** The organization roles that will be automatically provisioned to users when they join an organization through JIT. */ +create table organization_jit_roles ( + tenant_id varchar(21) not null + references tenants (id) on update cascade on delete cascade, + /** The ID of the organization. */ + organization_id varchar(21) not null + references organizations (id) on update cascade on delete cascade, + /** The organization role ID that will be automatically provisioned. */ + organization_role_id varchar(21) not null + references organization_roles (id) on update cascade on delete cascade, + primary key (tenant_id, organization_id, organization_role_id) +);