Skip to content

Commit

Permalink
feat(core): update application organizaiton role apis
Browse files Browse the repository at this point in the history
  • Loading branch information
gao-sun committed Jun 22, 2024
1 parent 7f6ef27 commit ce4fc97
Show file tree
Hide file tree
Showing 14 changed files with 216 additions and 28 deletions.
7 changes: 7 additions & 0 deletions .changeset/fresh-gorillas-obey.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@logto/core": minor
---

pagination is now optional for `GET /api/organizations/:id/users/:userId/roles`

The default pagination is now removed. This isn't considered a breaking change, but we marked it as minor to get your attention.
6 changes: 4 additions & 2 deletions packages/core/src/routes/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type TenantContext from '#src/tenants/TenantContext.js';
import koaAuth from '../middleware/koa-auth/index.js';

import adminUserRoutes from './admin-user/index.js';
import applicationOrganizationRoutes from './applications/application-organization.js';
import applicationProtectedAppMetadataRoutes from './applications/application-protected-app-metadata.js';
import applicationRoleRoutes from './applications/application-role.js';
import applicationSignInExperienceRoutes from './applications/application-sign-in-experience.js';
Expand Down Expand Up @@ -52,16 +53,17 @@ const createRouters = (tenant: TenantContext) => {
managementRouter.use(koaTenantGuard(tenant.id, tenant.queries));
managementRouter.use(koaManagementApiHooks(tenant.libraries.hooks));

// TODO: FIXME @sijie @darcy mount these routes in `applicationRoutes` instead
applicationRoutes(managementRouter, tenant);
applicationRoleRoutes(managementRouter, tenant);
applicationProtectedAppMetadataRoutes(managementRouter, tenant);
applicationOrganizationRoutes(managementRouter, tenant);

// Third-party application related routes
applicationUserConsentScopeRoutes(managementRouter, tenant);
applicationSignInExperienceRoutes(managementRouter, tenant);
applicationUserConsentOrganizationRoutes(managementRouter, tenant);

applicationProtectedAppMetadataRoutes(managementRouter, tenant);

logtoConfigRoutes(managementRouter, tenant);
connectorRoutes(managementRouter, tenant);
resourceRoutes(managementRouter, tenant);
Expand Down
2 changes: 0 additions & 2 deletions packages/core/src/routes/organization-role/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,7 @@ export default function organizationRoleRoutes<T extends ManagementApiRouter>(
}),
async (ctx, next) => {
const { limit, offset } = ctx.pagination;

const search = parseSearchOptions(organizationRoleSearchKeys, ctx.guard.query);

const [count, entities] = await roles.findAll(limit, offset, search);

ctx.pagination.totalCount = count;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,37 @@
}
}
},
"/api/organizations/{id}/applications/roles": {
"post": {
"tags": ["Dev feature"],
"summary": "Assign roles to applications in an organization",
"description": "Assign roles to applications in the specified organization.",
"requestBody": {
"content": {
"application/json": {
"schema": {
"properties": {
"applicationIds": {
"description": "An array of application IDs to assign roles to."
},
"organizationRoleIds": {
"description": "An array of organization role IDs to assign to the applications."
}
}
}
}
}
},
"responses": {
"201": {
"description": "Roles were assigned to the applications successfully."
},
"422": {
"description": "At least one of the IDs provided is not valid. For example, the organization ID, application ID, or organization role ID does not exist; the application is not a member of the organization; or the role type is not assignable to the application."
}
}
}
},
"/api/organizations/{id}/applications/{applicationId}/roles": {
"get": {
"tags": ["Dev feature"],
Expand Down
29 changes: 29 additions & 0 deletions packages/core/src/routes/organization/application/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,35 @@ export default function applicationRoutes(
}
);

router.post(
'/:id/applications/roles',
koaGuard({
params: z.object({ id: z.string().min(1) }),
body: z.object({
applicationIds: z.string().min(1).array().nonempty(),
organizationRoleIds: z.string().min(1).array().nonempty(),
}),
status: [201, 422],
}),
async (ctx, next) => {
const { id } = ctx.guard.params;
const { applicationIds, organizationRoleIds } = ctx.guard.body;

await organizations.relations.appsRoles.insert(
...organizationRoleIds.flatMap((organizationRoleId) =>
applicationIds.map((applicationId) => ({
organizationId: id,
applicationId,
organizationRoleId,
}))
)
);

ctx.status = 201;
return next();
}

Check warning on line 83 in packages/core/src/routes/organization/application/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/organization/application/index.ts#L68-L83

Added lines #L68 - L83 were not covered by tests
);

// MARK: Organization - application role relation routes
applicationRoleRelationRoutes(router, organizations);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export default function applicationRoleRelationRoutes(

router.get(
pathname,
koaPagination(),
koaPagination({ isOptional: true }),
koaGuard({
params: z.object(params),
response: OrganizationRoles.guard.array(),
Expand All @@ -49,10 +49,14 @@ export default function applicationRoleRelationRoutes(
{
organizationId: id,
applicationId,
}
},
ctx.pagination.disabled ? undefined : ctx.pagination

Check warning on line 53 in packages/core/src/routes/organization/application/role-relations.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/organization/application/role-relations.ts#L52-L53

Added lines #L52 - L53 were not covered by tests
);

ctx.pagination.totalCount = totalCount;
if (!ctx.pagination.disabled) {
ctx.pagination.totalCount = totalCount;
}

Check warning on line 59 in packages/core/src/routes/organization/application/role-relations.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/organization/application/role-relations.ts#L56-L59

Added lines #L56 - L59 were not covered by tests
ctx.body = entities;
return next();
}
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/routes/organization/user/index.openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@
"/api/organizations/{id}/users/roles": {
"post": {
"summary": "Assign roles to organization user members",
"description": "Assign roles to user members of the specified organization with the given data.",
"description": "Assign roles to user members of the specified organization.",
"requestBody": {
"content": {
"application/json": {
Expand All @@ -113,7 +113,7 @@
"description": "Roles were assigned to organization users successfully."
},
"422": {
"description": "At least one of the IDs provided is not valid. For example, the organization ID, user ID, or organization role ID does not exist; the user is not a member of the organization."
"description": "At least one of the IDs provided is not valid. For example, the organization ID, user ID, or organization role ID does not exist; the user is not a member of the organization; or the role type is not assignable to the user."
}
}
}
Expand Down
8 changes: 5 additions & 3 deletions packages/core/src/routes/organization/user/role-relations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export default function userRoleRelationRoutes(

router.get(
pathname,
koaPagination(),
koaPagination({ isOptional: true }),
koaGuard({
params: z.object(params),
response: OrganizationRoles.guard.array(),
Expand All @@ -54,10 +54,12 @@ export default function userRoleRelationRoutes(
organizationId: id,
userId,
},
ctx.pagination
ctx.pagination.disabled ? undefined : ctx.pagination

Check warning on line 57 in packages/core/src/routes/organization/user/role-relations.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/organization/user/role-relations.ts#L57

Added line #L57 was not covered by tests
);

ctx.pagination.totalCount = totalCount;
if (!ctx.pagination.disabled) {
ctx.pagination.totalCount = totalCount;
}

Check warning on line 62 in packages/core/src/routes/organization/user/role-relations.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/organization/user/role-relations.ts#L60-L62

Added lines #L60 - L62 were not covered by tests
ctx.body = entities;
return next();
}
Expand Down
33 changes: 29 additions & 4 deletions packages/integration-tests/src/api/organization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@ export class OrganizationApi extends ApiFactory<Organization, Omit<CreateOrganiz
return super.getList(query) as Promise<OrganizationWithFeatured[]>;
}

async addApplicationsRoles(
id: string,
applicationIds: string[],
organizationRoleIds: string[]
): Promise<void> {
await authedAdminApi.post(`${this.path}/${id}/applications/roles`, {
json: { applicationIds, organizationRoleIds },
});
}

async addUsers(id: string, userIds: string[]): Promise<void> {
await authedAdminApi.post(`${this.path}/${id}/users`, { json: { userIds } });
}
Expand Down Expand Up @@ -68,9 +78,9 @@ export class OrganizationApi extends ApiFactory<Organization, Omit<CreateOrganiz
});
}

async getUserRoles(id: string, userId: string): Promise<OrganizationRole[]> {
async getUserRoles(id: string, userId: string, query?: Query): Promise<OrganizationRole[]> {
return authedAdminApi
.get(`${this.path}/${id}/users/${userId}/roles`)
.get(`${this.path}/${id}/users/${userId}/roles`, { searchParams: query })
.json<OrganizationRole[]>();
}

Expand Down Expand Up @@ -98,9 +108,24 @@ export class OrganizationApi extends ApiFactory<Organization, Omit<CreateOrganiz
});
}

async getApplicationRoles(id: string, applicationId: string): Promise<OrganizationRole[]> {
async getApplicationRoles(
id: string,
applicationId: string,
page?: number,
pageSize?: number
): Promise<OrganizationRole[]> {
const search = new URLSearchParams();

if (page) {
search.set('page', String(page));
}

if (pageSize) {
search.set('page_size', String(pageSize));
}

return authedAdminApi
.get(`${this.path}/${id}/applications/${applicationId}/roles`)
.get(`${this.path}/${id}/applications/${applicationId}/roles`, { searchParams: search })
.json<OrganizationRole[]>();
}

Expand Down
8 changes: 5 additions & 3 deletions packages/integration-tests/src/helpers/organization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,10 +137,12 @@ export class OrganizationApiTest extends OrganizationApi {
* when they are deleted by other tests.
*/
async cleanUp(): Promise<void> {
await Promise.all(
await Promise.all([
// Use `trySafe` to avoid error when organization is deleted by other tests.
this.organizations.map(async (organization) => trySafe(this.delete(organization.id)))
);
...this.organizations.map(async (organization) => trySafe(this.delete(organization.id))),
this.roleApi.cleanUp(),
this.scopeApi.cleanUp(),
]);
this.#organizations = [];
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ describe('organization role data hook events', () => {
});

afterAll(async () => {
await organizationScopeApi.cleanUp();
await Promise.all([organizationScopeApi.cleanUp(), roleApi.cleanUp()]);
});

it.each(organizationRoleDataHookTestCases)(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,11 +152,9 @@ describe('consent api', () => {
});

describe('get consent info with organization resource scopes', () => {
const roleApi = new OrganizationRoleApiTest();
const organizationApi = new OrganizationApiTest();

afterEach(async () => {
await roleApi.cleanUp();
await organizationApi.cleanUp();
});

Expand All @@ -167,7 +165,7 @@ describe('consent api', () => {
const resource = await createResource(generateResourceName(), generateResourceIndicator());
const scope = await createScope(resource.id, generateScopeName());
const scope2 = await createScope(resource.id, generateScopeName());
const role = await roleApi.create({
const role = await organizationApi.roleApi.create({
name: generateRoleName(),
resourceScopeIds: [scope.id],
});
Expand Down Expand Up @@ -219,7 +217,7 @@ describe('consent api', () => {

const resource = await createResource(generateResourceName(), generateResourceIndicator());
const scope = await createScope(resource.id, generateScopeName());
const role = await roleApi.create({
const role = await organizationApi.roleApi.create({
name: generateRoleName(),
resourceScopeIds: [scope.id],
});
Expand Down Expand Up @@ -397,10 +395,12 @@ describe('consent api', () => {
// Scope2 is removed because organization2 is not consented
expect(getAccessTokenPayload(accessToken)).toHaveProperty('scope', scope.name);

await roleApi.cleanUp();
await organizationApi.cleanUp();
await deleteResource(resource.id);
await deleteUser(user.id);
await Promise.all([
roleApi.cleanUp(),
organizationApi.cleanUp(),
deleteResource(resource.id),
deleteUser(user.id),
]);
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -259,5 +259,59 @@ devFeatureTest.describe('organization application APIs', () => {
expect.objectContaining({ code: 'entity.db_constraint_violated' })
);
});

it('should be able to assign multiple roles to multiple applications', async () => {
const organization = await organizationApi.create({ name: 'test' });
const roles = await Promise.all(
Array.from({ length: 30 }).map(async () =>
organizationApi.roleApi.create({
name: `test-${generateTestName()}`,
type: RoleType.MachineToMachine,
})
)
);
const applications = await Promise.all(
Array.from({ length: 3 }).map(async () =>
createApplication(generateTestName(), ApplicationType.MachineToMachine)
)
);

await organizationApi.applications.add(
organization.id,
applications.map(({ id }) => id)
);
await organizationApi.addApplicationsRoles(
organization.id,
applications.map(({ id }) => id),
roles.map(({ id }) => id)
);
const fetchedRoles = await Promise.all(
applications.map(async ({ id }) => organizationApi.getApplicationRoles(organization.id, id))
);

expect(fetchedRoles).toEqual(
Array.from({ length: 3 }).map(() =>
expect.arrayContaining(roles.map((role) => expect.objectContaining(role)))
)
);

// Test pagination
const fetchedRoles1 = await organizationApi.getApplicationRoles(
organization.id,
applications[0]!.id,
1,
20
);
const fetchedRoles2 = await organizationApi.getApplicationRoles(
organization.id,
applications[0]!.id,
2,
10
);
expect(fetchedRoles1).toHaveLength(20);
expect(fetchedRoles2).toHaveLength(10);
expect(roles).toEqual(expect.arrayContaining(fetchedRoles1));
expect(roles).toEqual(expect.arrayContaining(fetchedRoles2));
});
});
});
Loading

0 comments on commit ce4fc97

Please sign in to comment.