0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-17 22:31:28 -05:00

feat(core,console): organization jit roles

This commit is contained in:
Gao Sun 2024-06-14 14:50:33 +08:00
parent 847a7c413a
commit 3ea37c5275
No known key found for this signature in database
GPG key ID: 13EBE123E4773688
16 changed files with 329 additions and 130 deletions

View file

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

View file

@ -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<Omit<Organization, 'customData'> & { customData: string }> & {
isJitEnabled: boolean;
jitEmailDomains: string[];
jitRoles: Array<Option<string>>;
};
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<Option<string>> }
): 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<OrganizationDetailsOutletContext>();
const { isDeleting, data, jit, onUpdated } = useOutletContext<OrganizationDetailsOutletContext>();
const { data: signInExperience } = useSWR<SignInExperience, RequestError>('api/sign-in-exp');
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const {
@ -67,13 +74,14 @@ function Settings() {
clearErrors,
watch,
} = useForm<FormData>({
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<Organization>();
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() {
/>
</FormField>
</FormCard>
<FormCard
title="organization_details.membership_policies"
description="organization_details.membership_policies_description"
>
<FormField title="organization_details.jit.is_enabled_title">
<div className={styles.jitContent}>
<Switch
label={t('organization_details.jit.description')}
{...register('isJitEnabled')}
/>
</div>
</FormField>
{isJitEnabled && (
<FormField title="organization_details.jit.email_domain_provisioning">
<p className={styles.membershipDescription}>
{t('organization_details.jit.membership_description')}
</p>
<Controller
name="jitEmailDomains"
control={control}
render={({ field: { onChange, value } }) => (
<MultiOptionInput
className={styles.emailDomains}
values={value}
renderValue={(value) => 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 && (
<FormCard
title="organization_details.membership_policies"
description="organization_details.membership_policies_description"
>
<FormField title="organization_details.jit.title">
<div className={styles.jitContent}>
<Switch
label={t('organization_details.jit.description')}
{...register('isJitEnabled')}
/>
</div>
</FormField>
)}
{isDevFeaturesEnabled && (
{isJitEnabled && (
<FormField title="organization_details.jit.email_domains">
<Controller
name="jitEmailDomains"
control={control}
render={({ field: { onChange, value } }) => (
<MultiOptionInput
values={value}
renderValue={(value) => 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');
}}
/>
)}
/>
</FormField>
)}
{isJitEnabled && (
<FormField
title="organization_details.jit.organization_roles"
description="organization_details.jit.organization_roles_description"
descriptionPosition="top"
>
<Controller
name="jitRoles"
control={control}
render={({ field: { onChange, value } }) => (
<OrganizationRolesSelect
keyword={keyword}
setKeyword={setKeyword}
value={value}
onChange={onChange}
/>
)}
/>
</FormField>
)}
<FormField title="organization_details.mfa.title" tip={t('organization_details.mfa.tip')}>
<Switch
label={t('organization_details.mfa.description')}
@ -201,8 +231,8 @@ function Settings() {
</InlineNotification>
)}
</FormField>
)}
</FormCard>
</FormCard>
)}
<UnsavedChangesAlertModal hasUnsavedChanges={!isDeleting && isDirty} />
</DetailsForm>
);

View file

@ -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<Organization, RequestError>(id && `api/organizations/${id}`);
const emailDomains = useSWR<OrganizationEmailDomain[], RequestError>(
id && `api/organizations/${id}/email-domains`
const jitEmailDomains = useSWR<OrganizationJitEmailDomain[], RequestError>(
id && `api/organizations/${id}/jit/email-domains`
);
const jitRoles = useSWR<OrganizationRole[], RequestError>(
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 (
<DetailsPage backLink={pathname} backLinkTitle="organizations.title" className={styles.page}>
<PageMeta titleKey="organization_details.page_title" />
{isLoading && <Skeleton />}
{error && <AppError errorCode={error.body?.code} errorMessage={error.body?.message} />}
{id && organization.data && emailDomains.data && (
{id && organization.data && jitEmailDomains.data && jitRoles.data && (
<>
<DetailsPageHeader
icon={<ThemedIcon for={OrganizationIcon} size={60} />}
@ -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

View file

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

View file

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

View file

@ -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<CreateOrganizationEmailDomain>
) => Promise<Readonly<OrganizationEmailDomain>>;
data: OmitAutoSetFields<CreateOrganizationJitEmailDomain>
) => Promise<Readonly<OrganizationJitEmailDomain>>;
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<OrganizationEmailDomain>(sql`
this.pool.any<OrganizationJitEmailDomain>(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<readonly string[]> {
const rows = await this.pool.any<Pick<OrganizationEmailDomain, 'organizationId'>>(sql`
const rows = await this.pool.any<Pick<OrganizationJitEmailDomain, 'organizationId'>>(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<OrganizationEmailDomain> {
async insert(organizationId: string, emailDomain: string): Promise<OrganizationJitEmailDomain> {
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);
}
}

View file

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

View file

@ -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": [
{

View file

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

View file

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

View file

@ -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<T extends ManagementApiRouter>(
);
userRoleRelationRoutes(router, organizations);
// MARK: Just-in-time provisioning
emailDomainRoutes(router, organizations);
router.addRelationRoutes(organizations.jit.roles, 'jit/roles');
// MARK: Mount sub-routes
organizationRoleRoutes(...args);

View file

@ -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<Organization, Omit<CreateOrganiz
id: string,
page?: number,
pageSize?: number
): Promise<OrganizationEmailDomain[]> {
): Promise<OrganizationJitEmailDomain[]> {
const searchParams = new URLSearchParams();
if (page) {
@ -94,19 +94,19 @@ export class OrganizationApi extends ApiFactory<Organization, Omit<CreateOrganiz
}
return authedAdminApi
.get(`${this.path}/${id}/email-domains`, { searchParams })
.json<OrganizationEmailDomain[]>();
.get(`${this.path}/${id}/jit/email-domains`, { searchParams })
.json<OrganizationJitEmailDomain[]>();
}
async addEmailDomain(id: string, emailDomain: string): Promise<void> {
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<void> {
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<void> {
await authedAdminApi.put(`${this.path}/${id}/email-domains`, { json: { emailDomains } });
await authedAdminApi.put(`${this.path}/${id}/jit/email-domains`, { json: { emailDomains } });
}
}

View file

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

View file

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

View file

@ -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. */

View file

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