mirror of
https://github.com/logto-io/logto.git
synced 2025-03-24 22:41:28 -05:00
Merge pull request #6064 from logto-io/gao-init-org-app-apis
feat(core): init organization app apis
This commit is contained in:
commit
5362772f6d
19 changed files with 405 additions and 40 deletions
|
@ -22,6 +22,8 @@ import {
|
|||
Resources,
|
||||
Users,
|
||||
OrganizationJitRoles,
|
||||
OrganizationApplicationRelations,
|
||||
Applications,
|
||||
} from '@logto/schemas';
|
||||
import { sql, type CommonQueryMethods } from '@silverhand/slonik';
|
||||
|
||||
|
@ -283,6 +285,13 @@ export default class OrganizationQueries extends SchemaQueries<
|
|||
users: new UserRelationQueries(this.pool),
|
||||
/** Queries for organization - organization role - user relations. */
|
||||
rolesUsers: new RoleUserRelationQueries(this.pool),
|
||||
/** Queries for organization - application relations. */
|
||||
apps: new TwoRelationsQueries(
|
||||
this.pool,
|
||||
OrganizationApplicationRelations.table,
|
||||
Organizations,
|
||||
Applications
|
||||
),
|
||||
invitationsRoles: new TwoRelationsQueries(
|
||||
this.pool,
|
||||
OrganizationInvitationRoleRelations.table,
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
{
|
||||
"tags": [
|
||||
{
|
||||
"name": "Organization applications",
|
||||
"description": "Manage organization - application relationships. An application can be associated with one or more organizations in order to grant organization access to the application.\n\nCurrently, only machine-to-machine applications can be associated with organizations."
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"/api/organizations/{id}/applications": {
|
||||
"get": {
|
||||
"summary": "Get organization applications",
|
||||
"description": "Get applications associated with the organization.",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "A list of applications."
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"summary": "Add organization application",
|
||||
"description": "Add an application to the organization.",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"applicationIds": {
|
||||
"description": "The application IDs to add."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "The application was added successfully."
|
||||
},
|
||||
"422": {
|
||||
"description": "The application could not be added. Some of the applications may not exist."
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"summary": "Replace organization applications",
|
||||
"description": "Replace all applications associated with the organization with the given data.",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"applicationIds": {
|
||||
"description": "An array of application IDs to replace existing applications."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "The applications were replaced successfully."
|
||||
},
|
||||
"422": {
|
||||
"description": "The applications could not be replaced. Some of the applications may not exist."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/organizations/{id}/applications/{applicationId}": {
|
||||
"delete": {
|
||||
"summary": "Remove organization application",
|
||||
"description": "Remove an application from the organization.",
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "The application was removed from the organization successfully."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -59,6 +59,9 @@
|
|||
"responses": {
|
||||
"204": {
|
||||
"description": "The organization roles were replaced successfully."
|
||||
},
|
||||
"422": {
|
||||
"description": "The organization roles could not be replaced. Some of the organization roles may not exist."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
import { yes } from '@silverhand/essentials';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { EnvSet } from '#src/env-set/index.js';
|
||||
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';
|
||||
|
@ -139,6 +140,13 @@ export default function organizationRoutes<T extends ManagementApiRouter>(
|
|||
|
||||
userRoleRelationRoutes(router, organizations);
|
||||
|
||||
// MARK: Organization - application relation routes
|
||||
if (EnvSet.values.isDevFeaturesEnabled) {
|
||||
router.addRelationRoutes(organizations.relations.apps, undefined, {
|
||||
hookEvent: 'Organization.Membership.Updated',
|
||||
});
|
||||
}
|
||||
|
||||
// MARK: Just-in-time provisioning
|
||||
emailDomainRoutes(router, organizations);
|
||||
router.addRelationRoutes(organizations.jit.roles, 'jit/roles', { isPaginationOptional: true });
|
||||
|
|
|
@ -112,7 +112,7 @@ const isManagementApiRouter = ({ stack }: Router) =>
|
|||
|
||||
// Add more components here to cover more ID parameters in paths. For example, if there is a
|
||||
// path `/foo/:barBazId`, then add `bar-baz` to the array.
|
||||
const identifiableEntityNames = [
|
||||
const identifiableEntityNames = Object.freeze([
|
||||
'key',
|
||||
'connector-factory',
|
||||
'factory',
|
||||
|
@ -131,7 +131,10 @@ const identifiableEntityNames = [
|
|||
'organization-role',
|
||||
'organization-scope',
|
||||
'organization-invitation',
|
||||
];
|
||||
]);
|
||||
|
||||
/** Additional tags that cannot be inferred from the path. */
|
||||
const additionalTags = Object.freeze(['Organization applications']);
|
||||
|
||||
/**
|
||||
* Attach the `/swagger.json` route which returns the generated OpenAPI document for the
|
||||
|
@ -229,7 +232,7 @@ export default function swaggerRoutes<T extends AnonymousRouter, R extends Route
|
|||
{}
|
||||
),
|
||||
},
|
||||
tags: [...tags].map((tag) => ({ name: tag })),
|
||||
tags: [...tags, ...additionalTags].map((tag) => ({ name: tag })),
|
||||
};
|
||||
|
||||
const data = supplementDocuments.reduce<OpenAPIV3.Document>(
|
||||
|
|
|
@ -142,7 +142,9 @@ export const validateSupplement = (
|
|||
|
||||
for (const { name } of supplementTags) {
|
||||
if (!originalTags.has(name)) {
|
||||
throw new TypeError(`Supplement document contains extra tag \`${name}\`.`);
|
||||
throw new TypeError(
|
||||
`Supplement document contains extra tag \`${name}\`. If you want to add a new tag, please add it to the \`additionalTags\` array in the main swagger route file.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -159,6 +159,7 @@ export default class SchemaRouter<
|
|||
*
|
||||
* - `GET /:id/[pathname]`: Get the entities of the relation with pagination.
|
||||
* - `POST /:id/[pathname]`: Add entities to the relation.
|
||||
* - `PUT /:id/[pathname]`: Replace the entities in the relation.
|
||||
* - `DELETE /:id/[pathname]/:relationSchemaId`: Remove an entity from the relation set.
|
||||
* The `:relationSchemaId` is the entity ID in the relation schema.
|
||||
*
|
||||
|
|
|
@ -49,3 +49,74 @@ export class ApiFactory<
|
|||
await authedAdminApi.delete(this.path + '/' + id);
|
||||
}
|
||||
}
|
||||
|
||||
type RelationApiFactoryConfig = {
|
||||
/** The base path of the API. */
|
||||
basePath: string;
|
||||
/**
|
||||
* The path of the relation. It will to be appended to the base path and the ID of the parent.
|
||||
*
|
||||
* @example
|
||||
* If the base path is `organizations` and the relation path is `applications`, the final paths
|
||||
* will be `organizations/:id/applications` and `organizations/:id/applications/:applicationId`.
|
||||
*/
|
||||
relationPath: string;
|
||||
/**
|
||||
* The key name of the relation IDs. It will be used in the request body.
|
||||
*
|
||||
* @example
|
||||
* If the key name is `applicationIds`, the request body will be
|
||||
* `{ applicationIds: ['id1', 'id2'] }`.
|
||||
*/
|
||||
relationKey: string;
|
||||
};
|
||||
|
||||
export class RelationApiFactory<RelationSchema extends Record<string, unknown>> {
|
||||
constructor(public readonly config: RelationApiFactoryConfig) {}
|
||||
|
||||
get basePath(): string {
|
||||
return this.config.basePath;
|
||||
}
|
||||
|
||||
get relationPath(): string {
|
||||
return this.config.relationPath;
|
||||
}
|
||||
|
||||
get relationKey(): string {
|
||||
return this.config.relationKey;
|
||||
}
|
||||
|
||||
async getList(id: string, page?: number, pageSize?: number): Promise<RelationSchema[]> {
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (page) {
|
||||
searchParams.append('page', String(page));
|
||||
}
|
||||
|
||||
if (pageSize) {
|
||||
searchParams.append('page_size', String(pageSize));
|
||||
}
|
||||
|
||||
return transform(
|
||||
await authedAdminApi
|
||||
.get(`${this.basePath}/${id}/${this.relationPath}`, { searchParams })
|
||||
.json<RelationSchema[]>()
|
||||
);
|
||||
}
|
||||
|
||||
async add(id: string, relationIds: string[]): Promise<void> {
|
||||
await authedAdminApi.post(`${this.basePath}/${id}/${this.relationPath}`, {
|
||||
json: { [this.relationKey]: relationIds },
|
||||
});
|
||||
}
|
||||
|
||||
async delete(id: string, relationId: string): Promise<void> {
|
||||
await authedAdminApi.delete(`${this.basePath}/${id}/${this.relationPath}/${relationId}`);
|
||||
}
|
||||
|
||||
async replace(id: string, relationIds: string[]): Promise<void> {
|
||||
await authedAdminApi.put(`${this.basePath}/${id}/${this.relationPath}`, {
|
||||
json: { [this.relationKey]: relationIds },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,15 @@
|
|||
import { type OrganizationRole, type OrganizationJitEmailDomain } from '@logto/schemas';
|
||||
|
||||
import { authedAdminApi } from './api.js';
|
||||
import { RelationApiFactory } from './factory.js';
|
||||
|
||||
export class OrganizationJitApi {
|
||||
roles = new RelationApiFactory<OrganizationRole>({
|
||||
basePath: 'organizations',
|
||||
relationPath: 'jit/roles',
|
||||
relationKey: 'organizationRoleIds',
|
||||
});
|
||||
|
||||
constructor(public path: string) {}
|
||||
|
||||
async getEmailDomains(
|
||||
|
@ -36,20 +43,4 @@ export class OrganizationJitApi {
|
|||
async replaceEmailDomains(id: string, emailDomains: string[]): Promise<void> {
|
||||
await authedAdminApi.put(`${this.path}/${id}/jit/email-domains`, { json: { emailDomains } });
|
||||
}
|
||||
|
||||
async getRoles(id: string): Promise<OrganizationRole[]> {
|
||||
return authedAdminApi.get(`${this.path}/${id}/jit/roles`).json<OrganizationRole[]>();
|
||||
}
|
||||
|
||||
async addRole(id: string, organizationRoleIds: string[]): Promise<void> {
|
||||
await authedAdminApi.post(`${this.path}/${id}/jit/roles`, { json: { organizationRoleIds } });
|
||||
}
|
||||
|
||||
async deleteRole(id: string, organizationRoleId: string): Promise<void> {
|
||||
await authedAdminApi.delete(`${this.path}/${id}/jit/roles/${organizationRoleId}`);
|
||||
}
|
||||
|
||||
async replaceRoles(id: string, organizationRoleIds: string[]): Promise<void> {
|
||||
await authedAdminApi.put(`${this.path}/${id}/jit/roles`, { json: { organizationRoleIds } });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,10 +6,11 @@ import {
|
|||
type OrganizationWithFeatured,
|
||||
type OrganizationScope,
|
||||
type CreateOrganization,
|
||||
type Application,
|
||||
} from '@logto/schemas';
|
||||
|
||||
import { authedAdminApi } from './api.js';
|
||||
import { ApiFactory } from './factory.js';
|
||||
import { ApiFactory, RelationApiFactory } from './factory.js';
|
||||
import { OrganizationJitApi } from './organization-jit.js';
|
||||
|
||||
type Query = {
|
||||
|
@ -20,6 +21,11 @@ type Query = {
|
|||
|
||||
export class OrganizationApi extends ApiFactory<Organization, Omit<CreateOrganization, 'id'>> {
|
||||
jit = new OrganizationJitApi(this.path);
|
||||
applications = new RelationApiFactory<Application>({
|
||||
basePath: 'organizations',
|
||||
relationPath: 'applications',
|
||||
relationKey: 'applicationIds',
|
||||
});
|
||||
|
||||
constructor() {
|
||||
super('organizations');
|
||||
|
|
|
@ -28,8 +28,8 @@ describe('organization just-in-time provisioning', () => {
|
|||
)
|
||||
);
|
||||
await Promise.all([
|
||||
organizationApi.jit.addRole(organizations[0].id, [roles[0].id, roles[1].id]),
|
||||
organizationApi.jit.addRole(organizations[1].id, [roles[0].id]),
|
||||
organizationApi.jit.roles.add(organizations[0].id, [roles[0].id, roles[1].id]),
|
||||
organizationApi.jit.roles.add(organizations[1].id, [roles[0].id]),
|
||||
]);
|
||||
|
||||
const email = randomString() + '@' + emailDomain;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import {
|
||||
ApplicationType,
|
||||
RoleType,
|
||||
hookEventGuard,
|
||||
hookEvents,
|
||||
|
@ -10,8 +11,10 @@ import { assert } from '@silverhand/essentials';
|
|||
import { z } from 'zod';
|
||||
|
||||
import { authedAdminApi } from '#src/api/api.js';
|
||||
import { createApplication, deleteApplication } from '#src/api/application.js';
|
||||
import { createResource } from '#src/api/resource.js';
|
||||
import { createScope } from '#src/api/scope.js';
|
||||
import { isDevFeaturesEnabled } from '#src/constants.js';
|
||||
import { WebHookApiTest } from '#src/helpers/hook.js';
|
||||
import {
|
||||
OrganizationApiTest,
|
||||
|
@ -214,6 +217,7 @@ describe('organization data hook events', () => {
|
|||
/* eslint-disable @silverhand/fp/no-let */
|
||||
let organizationId: string;
|
||||
let userId: string;
|
||||
let applicationId: string;
|
||||
/* eslint-enable @silverhand/fp/no-let */
|
||||
|
||||
const organizationApi = new OrganizationApiTest();
|
||||
|
@ -226,10 +230,12 @@ describe('organization data hook events', () => {
|
|||
});
|
||||
|
||||
const user = await userApi.create({ name: generateName() });
|
||||
const application = await createApplication(generateName(), ApplicationType.MachineToMachine);
|
||||
|
||||
/* eslint-disable @silverhand/fp/no-mutation */
|
||||
organizationId = organization.id;
|
||||
userId = user.id;
|
||||
applicationId = application.id;
|
||||
/* eslint-enable @silverhand/fp/no-mutation */
|
||||
|
||||
const organizationCreateHook = await getWebhookResult('POST /organizations');
|
||||
|
@ -237,15 +243,30 @@ describe('organization data hook events', () => {
|
|||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await userApi.cleanUp();
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
await Promise.all([userApi.cleanUp(), deleteApplication(applicationId).catch(() => {})]);
|
||||
});
|
||||
|
||||
it.each(organizationDataHookTestCases)(
|
||||
'test case %#: %p',
|
||||
async ({ route, event, method, endpoint, payload, hookPayload }) => {
|
||||
// TODO: Remove this check
|
||||
if (route.includes('applications') && !isDevFeaturesEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
await authedAdminApi[method](
|
||||
endpoint.replace('{organizationId}', organizationId).replace('{userId}', userId),
|
||||
{ json: JSON.parse(JSON.stringify(payload).replace('{userId}', userId)) }
|
||||
endpoint
|
||||
.replace('{organizationId}', organizationId)
|
||||
.replace('{userId}', userId)
|
||||
.replace('{applicationId}', applicationId),
|
||||
{
|
||||
json: JSON.parse(
|
||||
JSON.stringify(payload)
|
||||
.replace('{userId}', userId)
|
||||
.replace('{applicationId}', applicationId)
|
||||
),
|
||||
}
|
||||
);
|
||||
const hook = await getWebhookResult(route);
|
||||
expect(hook?.payload.event).toBe(event);
|
||||
|
|
|
@ -129,6 +129,30 @@ export const organizationDataHookTestCases: TestCase[] = [
|
|||
payload: {},
|
||||
hookPayload: { organizationId: expect.any(String) },
|
||||
},
|
||||
{
|
||||
route: 'POST /organizations/:id/applications',
|
||||
event: 'Organization.Membership.Updated',
|
||||
method: 'post',
|
||||
endpoint: `organizations/{organizationId}/applications`,
|
||||
payload: { applicationIds: ['{applicationId}'] },
|
||||
hookPayload: { organizationId: expect.any(String) },
|
||||
},
|
||||
{
|
||||
route: 'PUT /organizations/:id/applications',
|
||||
event: 'Organization.Membership.Updated',
|
||||
method: 'put',
|
||||
endpoint: `organizations/{organizationId}/applications`,
|
||||
payload: { applicationIds: ['{applicationId}'] },
|
||||
hookPayload: { organizationId: expect.any(String) },
|
||||
},
|
||||
{
|
||||
route: 'DELETE /organizations/:id/applications/:applicationId',
|
||||
event: 'Organization.Membership.Updated',
|
||||
method: 'delete',
|
||||
endpoint: `organizations/{organizationId}/applications/{applicationId}`,
|
||||
payload: {},
|
||||
hookPayload: { organizationId: expect.any(String) },
|
||||
},
|
||||
{
|
||||
route: 'DELETE /organizations/:id',
|
||||
event: 'Organization.Deleted',
|
||||
|
|
|
@ -62,8 +62,8 @@ describe('organization just-in-time provisioning', () => {
|
|||
)
|
||||
);
|
||||
await Promise.all([
|
||||
organizationApi.jit.addRole(organizations[0].id, [roles[0].id, roles[1].id]),
|
||||
organizationApi.jit.addRole(organizations[1].id, [roles[0].id]),
|
||||
organizationApi.jit.roles.add(organizations[0].id, [roles[0].id, roles[1].id]),
|
||||
organizationApi.jit.roles.add(organizations[1].id, [roles[0].id]),
|
||||
]);
|
||||
|
||||
const email = randomString() + '@' + emailDomain;
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
import assert from 'node:assert';
|
||||
|
||||
import { ApplicationType, type Application } from '@logto/schemas';
|
||||
import { HTTPError } from 'ky';
|
||||
|
||||
import {
|
||||
createApplication as createApplicationApi,
|
||||
deleteApplication,
|
||||
} from '#src/api/application.js';
|
||||
import { OrganizationApiTest } from '#src/helpers/organization.js';
|
||||
import { devFeatureTest, generateTestName } from '#src/utils.js';
|
||||
|
||||
// TODO: Remove this prefix
|
||||
devFeatureTest.describe('organization application APIs', () => {
|
||||
describe('organization - application relations', () => {
|
||||
const organizationApi = new OrganizationApiTest();
|
||||
const applications: Application[] = [];
|
||||
const createApplication = async (...args: Parameters<typeof createApplicationApi>) => {
|
||||
const created = await createApplicationApi(...args);
|
||||
// eslint-disable-next-line @silverhand/fp/no-mutating-methods
|
||||
applications.push(created);
|
||||
return created;
|
||||
};
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all([
|
||||
organizationApi.cleanUp(),
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
...applications.map(async ({ id }) => deleteApplication(id).catch(() => {})),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should fail when try to add empty application list', async () => {
|
||||
const organization = await organizationApi.create({ name: 'test' });
|
||||
const response = await organizationApi.applications
|
||||
.add(organization.id, [])
|
||||
.catch((error: unknown) => error);
|
||||
expect(response instanceof HTTPError && response.response.status).toBe(400);
|
||||
});
|
||||
|
||||
it('should fail when try to add application to an organization that does not exist', async () => {
|
||||
const response = await organizationApi.applications
|
||||
.add('0', ['0'])
|
||||
.catch((error: unknown) => error);
|
||||
assert(response instanceof HTTPError);
|
||||
expect(response.response.status).toBe(422);
|
||||
expect(await response.response.json()).toMatchObject(
|
||||
expect.objectContaining({ code: 'entity.relation_foreign_key_not_found' })
|
||||
);
|
||||
});
|
||||
|
||||
it('should be able to add and delete organization application', async () => {
|
||||
const organization = await organizationApi.create({ name: 'test' });
|
||||
const application = await createApplication(
|
||||
generateTestName(),
|
||||
ApplicationType.MachineToMachine
|
||||
);
|
||||
|
||||
await organizationApi.applications.add(organization.id, [application.id]);
|
||||
expect(await organizationApi.applications.getList(organization.id)).toContainEqual(
|
||||
application
|
||||
);
|
||||
|
||||
await organizationApi.applications.delete(organization.id, application.id);
|
||||
expect(await organizationApi.applications.getList(organization.id)).not.toContainEqual(
|
||||
application
|
||||
);
|
||||
});
|
||||
|
||||
it('should fail when try to delete application from an organization that does not exist', async () => {
|
||||
const response = await organizationApi.applications
|
||||
.delete('0', '0')
|
||||
.catch((error: unknown) => error);
|
||||
assert(response instanceof HTTPError);
|
||||
expect(response.response.status).toBe(404);
|
||||
});
|
||||
|
||||
it('should fail when try to add application that is not machine-to-machine', async () => {
|
||||
const organization = await organizationApi.create({ name: 'test' });
|
||||
const application = await createApplication(generateTestName(), ApplicationType.Native);
|
||||
|
||||
const response = await organizationApi.applications
|
||||
.add(organization.id, [application.id])
|
||||
.catch((error: unknown) => error);
|
||||
expect(response instanceof HTTPError && response.response.status).toBe(422);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -88,13 +88,13 @@ describe('organization just-in-time provisioning', () => {
|
|||
name: `jit-role:${randomString()}`,
|
||||
});
|
||||
|
||||
await organizationApi.jit.addRole(organization.id, [organizationRoleId]);
|
||||
await expect(organizationApi.jit.getRoles(organization.id)).resolves.toMatchObject([
|
||||
await organizationApi.jit.roles.add(organization.id, [organizationRoleId]);
|
||||
await expect(organizationApi.jit.roles.getList(organization.id)).resolves.toMatchObject([
|
||||
{ id: organizationRoleId },
|
||||
]);
|
||||
|
||||
await organizationApi.jit.deleteRole(organization.id, organizationRoleId);
|
||||
await expect(organizationApi.jit.getRoles(organization.id)).resolves.toEqual([]);
|
||||
await organizationApi.jit.roles.delete(organization.id, organizationRoleId);
|
||||
await expect(organizationApi.jit.roles.getList(organization.id)).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('should have no pagination', async () => {
|
||||
|
@ -107,12 +107,12 @@ describe('organization just-in-time provisioning', () => {
|
|||
)
|
||||
);
|
||||
|
||||
await organizationApi.jit.replaceRoles(
|
||||
await organizationApi.jit.roles.replace(
|
||||
organization.id,
|
||||
organizationRoles.map(({ id }) => id)
|
||||
);
|
||||
|
||||
await expect(organizationApi.jit.getRoles(organization.id)).resolves.toEqual(
|
||||
await expect(organizationApi.jit.roles.getList(organization.id)).resolves.toEqual(
|
||||
expect.arrayContaining(organizationRoles.map(({ id }) => expect.objectContaining({ id })))
|
||||
);
|
||||
});
|
||||
|
@ -122,7 +122,7 @@ describe('organization just-in-time provisioning', () => {
|
|||
const organizationRoleId = randomId();
|
||||
|
||||
await expect(
|
||||
organizationApi.jit.deleteRole(organization.id, organizationRoleId)
|
||||
organizationApi.jit.roles.delete(organization.id, organizationRoleId)
|
||||
).rejects.toMatchInlineSnapshot('[HTTPError: Request failed with status code 404 Not Found]');
|
||||
});
|
||||
|
||||
|
@ -131,7 +131,7 @@ describe('organization just-in-time provisioning', () => {
|
|||
const organizationRoleId = randomId();
|
||||
|
||||
await expect(
|
||||
organizationApi.jit.addRole(organization.id, [organizationRoleId])
|
||||
organizationApi.jit.roles.add(organization.id, [organizationRoleId])
|
||||
).rejects.toMatchInlineSnapshot(
|
||||
'[HTTPError: Request failed with status code 422 Unprocessable Entity]'
|
||||
);
|
||||
|
@ -148,9 +148,9 @@ describe('organization just-in-time provisioning', () => {
|
|||
}),
|
||||
]);
|
||||
|
||||
await organizationApi.jit.addRole(organization.id, [organizationRoles[0].id]);
|
||||
await organizationApi.jit.roles.add(organization.id, [organizationRoles[0].id]);
|
||||
await expect(
|
||||
organizationApi.jit.addRole(organization.id, [
|
||||
organizationApi.jit.roles.add(organization.id, [
|
||||
organizationRoles[0].id,
|
||||
organizationRoles[1].id,
|
||||
])
|
||||
|
@ -167,11 +167,11 @@ describe('organization just-in-time provisioning', () => {
|
|||
)
|
||||
);
|
||||
|
||||
await organizationApi.jit.replaceRoles(
|
||||
await organizationApi.jit.roles.replace(
|
||||
organization.id,
|
||||
organizationRoles.map(({ id }) => id)
|
||||
);
|
||||
await expect(organizationApi.jit.getRoles(organization.id)).resolves.toEqual(
|
||||
await expect(organizationApi.jit.roles.getList(organization.id)).resolves.toEqual(
|
||||
expect.arrayContaining(organizationRoles.map(({ id }) => expect.objectContaining({ id })))
|
||||
);
|
||||
});
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
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`
|
||||
create function check_application_type(application_id varchar(21), target_type application_type) returns boolean as
|
||||
$$ begin
|
||||
return (select type from applications where id = application_id) = target_type;
|
||||
end; $$ language plpgsql;
|
||||
create table organization_application_relations (
|
||||
tenant_id varchar(21) not null
|
||||
references tenants (id) on update cascade on delete cascade,
|
||||
organization_id varchar(21) not null
|
||||
references organizations (id) on update cascade on delete cascade,
|
||||
application_id varchar(21) not null
|
||||
references applications (id) on update cascade on delete cascade,
|
||||
primary key (tenant_id, organization_id, application_id),
|
||||
constraint application_type
|
||||
check (check_application_type(application_id, 'MachineToMachine'))
|
||||
);
|
||||
`);
|
||||
await applyTableRls(pool, 'organization_application_relations');
|
||||
},
|
||||
down: async (pool) => {
|
||||
await dropTableRls(pool, 'organization_application_relations');
|
||||
await pool.query(sql`
|
||||
drop table organization_application_relations;
|
||||
drop function check_application_type;
|
||||
`);
|
||||
},
|
||||
};
|
||||
|
||||
export default alteration;
|
|
@ -33,3 +33,8 @@ create unique index applications__protected_app_metadata_custom_domain
|
|||
on applications (
|
||||
(protected_app_metadata->'customDomains'->0->>'domain')
|
||||
);
|
||||
|
||||
create function check_application_type(application_id varchar(21), target_type application_type) returns boolean as
|
||||
$$ begin
|
||||
return (select type from applications where id = application_id) = target_type;
|
||||
end; $$ language plpgsql;
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
/* init_order = 2 */
|
||||
|
||||
/** The relations between organizations and applications. It indicates membership of applications in organizations. For now only machine-to-machine applications are supported. */
|
||||
create table organization_application_relations (
|
||||
tenant_id varchar(21) not null
|
||||
references tenants (id) on update cascade on delete cascade,
|
||||
organization_id varchar(21) not null
|
||||
references organizations (id) on update cascade on delete cascade,
|
||||
application_id varchar(21) not null
|
||||
references applications (id) on update cascade on delete cascade,
|
||||
primary key (tenant_id, organization_id, application_id),
|
||||
constraint application_type
|
||||
check (check_application_type(application_id, 'MachineToMachine'))
|
||||
);
|
Loading…
Add table
Reference in a new issue