mirror of
https://github.com/logto-io/logto.git
synced 2025-01-13 21:30:30 -05:00
feat(core): update application organization role apis
This commit is contained in:
parent
12cd49f903
commit
754d0e1340
13 changed files with 212 additions and 26 deletions
7
.changeset/fresh-gorillas-obey.md
Normal file
7
.changeset/fresh-gorillas-obey.md
Normal file
|
@ -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.
|
|
@ -60,9 +60,7 @@ export default function organizationRoleRoutes<T extends ManagementApiRouter>(
|
||||||
}),
|
}),
|
||||||
async (ctx, next) => {
|
async (ctx, next) => {
|
||||||
const { limit, offset } = ctx.pagination;
|
const { limit, offset } = ctx.pagination;
|
||||||
|
|
||||||
const search = parseSearchOptions(organizationRoleSearchKeys, ctx.guard.query);
|
const search = parseSearchOptions(organizationRoleSearchKeys, ctx.guard.query);
|
||||||
|
|
||||||
const [count, entities] = await roles.findAll(limit, offset, search);
|
const [count, entities] = await roles.findAll(limit, offset, search);
|
||||||
|
|
||||||
ctx.pagination.totalCount = count;
|
ctx.pagination.totalCount = count;
|
||||||
|
|
|
@ -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": {
|
"/api/organizations/{id}/applications/{applicationId}/roles": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": ["Dev feature"],
|
"tags": ["Dev feature"],
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// MARK: Organization - application role relation routes
|
// MARK: Organization - application role relation routes
|
||||||
applicationRoleRelationRoutes(router, organizations);
|
applicationRoleRelationRoutes(router, organizations);
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,7 @@ export default function applicationRoleRelationRoutes(
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
pathname,
|
pathname,
|
||||||
koaPagination(),
|
koaPagination({ isOptional: true }),
|
||||||
koaGuard({
|
koaGuard({
|
||||||
params: z.object(params),
|
params: z.object(params),
|
||||||
response: OrganizationRoles.guard.array(),
|
response: OrganizationRoles.guard.array(),
|
||||||
|
@ -49,10 +49,14 @@ export default function applicationRoleRelationRoutes(
|
||||||
{
|
{
|
||||||
organizationId: id,
|
organizationId: id,
|
||||||
applicationId,
|
applicationId,
|
||||||
}
|
},
|
||||||
|
ctx.pagination.disabled ? undefined : ctx.pagination
|
||||||
);
|
);
|
||||||
|
|
||||||
ctx.pagination.totalCount = totalCount;
|
if (!ctx.pagination.disabled) {
|
||||||
|
ctx.pagination.totalCount = totalCount;
|
||||||
|
}
|
||||||
|
|
||||||
ctx.body = entities;
|
ctx.body = entities;
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
|
@ -91,7 +91,7 @@
|
||||||
"/api/organizations/{id}/users/roles": {
|
"/api/organizations/{id}/users/roles": {
|
||||||
"post": {
|
"post": {
|
||||||
"summary": "Assign roles to organization user members",
|
"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": {
|
"requestBody": {
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
|
@ -113,7 +113,7 @@
|
||||||
"description": "Roles were assigned to organization users successfully."
|
"description": "Roles were assigned to organization users successfully."
|
||||||
},
|
},
|
||||||
"422": {
|
"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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,7 +39,7 @@ export default function userRoleRelationRoutes(
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
pathname,
|
pathname,
|
||||||
koaPagination(),
|
koaPagination({ isOptional: true }),
|
||||||
koaGuard({
|
koaGuard({
|
||||||
params: z.object(params),
|
params: z.object(params),
|
||||||
response: OrganizationRoles.guard.array(),
|
response: OrganizationRoles.guard.array(),
|
||||||
|
@ -54,10 +54,12 @@ export default function userRoleRelationRoutes(
|
||||||
organizationId: id,
|
organizationId: id,
|
||||||
userId,
|
userId,
|
||||||
},
|
},
|
||||||
ctx.pagination
|
ctx.pagination.disabled ? undefined : ctx.pagination
|
||||||
);
|
);
|
||||||
|
|
||||||
ctx.pagination.totalCount = totalCount;
|
if (!ctx.pagination.disabled) {
|
||||||
|
ctx.pagination.totalCount = totalCount;
|
||||||
|
}
|
||||||
ctx.body = entities;
|
ctx.body = entities;
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,16 @@ export class OrganizationApi extends ApiFactory<Organization, Omit<CreateOrganiz
|
||||||
return super.getList(query) as Promise<OrganizationWithFeatured[]>;
|
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> {
|
async addUsers(id: string, userIds: string[]): Promise<void> {
|
||||||
await authedAdminApi.post(`${this.path}/${id}/users`, { json: { userIds } });
|
await authedAdminApi.post(`${this.path}/${id}/users`, { json: { userIds } });
|
||||||
}
|
}
|
||||||
|
@ -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
|
return authedAdminApi
|
||||||
.get(`${this.path}/${id}/users/${userId}/roles`)
|
.get(`${this.path}/${id}/users/${userId}/roles`, { searchParams: query })
|
||||||
.json<OrganizationRole[]>();
|
.json<OrganizationRole[]>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
return authedAdminApi
|
||||||
.get(`${this.path}/${id}/applications/${applicationId}/roles`)
|
.get(`${this.path}/${id}/applications/${applicationId}/roles`, { searchParams: search })
|
||||||
.json<OrganizationRole[]>();
|
.json<OrganizationRole[]>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -137,10 +137,12 @@ export class OrganizationApiTest extends OrganizationApi {
|
||||||
* when they are deleted by other tests.
|
* when they are deleted by other tests.
|
||||||
*/
|
*/
|
||||||
async cleanUp(): Promise<void> {
|
async cleanUp(): Promise<void> {
|
||||||
await Promise.all(
|
await Promise.all([
|
||||||
// Use `trySafe` to avoid error when organization is deleted by other tests.
|
// 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 = [];
|
this.#organizations = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -340,7 +340,7 @@ describe('organization role data hook events', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await organizationScopeApi.cleanUp();
|
await Promise.all([organizationScopeApi.cleanUp(), roleApi.cleanUp()]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each(organizationRoleDataHookTestCases)(
|
it.each(organizationRoleDataHookTestCases)(
|
||||||
|
|
|
@ -152,11 +152,9 @@ describe('consent api', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('get consent info with organization resource scopes', () => {
|
describe('get consent info with organization resource scopes', () => {
|
||||||
const roleApi = new OrganizationRoleApiTest();
|
|
||||||
const organizationApi = new OrganizationApiTest();
|
const organizationApi = new OrganizationApiTest();
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
await roleApi.cleanUp();
|
|
||||||
await organizationApi.cleanUp();
|
await organizationApi.cleanUp();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -167,7 +165,7 @@ describe('consent api', () => {
|
||||||
const resource = await createResource(generateResourceName(), generateResourceIndicator());
|
const resource = await createResource(generateResourceName(), generateResourceIndicator());
|
||||||
const scope = await createScope(resource.id, generateScopeName());
|
const scope = await createScope(resource.id, generateScopeName());
|
||||||
const scope2 = 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(),
|
name: generateRoleName(),
|
||||||
resourceScopeIds: [scope.id],
|
resourceScopeIds: [scope.id],
|
||||||
});
|
});
|
||||||
|
@ -219,7 +217,7 @@ describe('consent api', () => {
|
||||||
|
|
||||||
const resource = await createResource(generateResourceName(), generateResourceIndicator());
|
const resource = await createResource(generateResourceName(), generateResourceIndicator());
|
||||||
const scope = await createScope(resource.id, generateScopeName());
|
const scope = await createScope(resource.id, generateScopeName());
|
||||||
const role = await roleApi.create({
|
const role = await organizationApi.roleApi.create({
|
||||||
name: generateRoleName(),
|
name: generateRoleName(),
|
||||||
resourceScopeIds: [scope.id],
|
resourceScopeIds: [scope.id],
|
||||||
});
|
});
|
||||||
|
@ -397,10 +395,12 @@ describe('consent api', () => {
|
||||||
// Scope2 is removed because organization2 is not consented
|
// Scope2 is removed because organization2 is not consented
|
||||||
expect(getAccessTokenPayload(accessToken)).toHaveProperty('scope', scope.name);
|
expect(getAccessTokenPayload(accessToken)).toHaveProperty('scope', scope.name);
|
||||||
|
|
||||||
await roleApi.cleanUp();
|
await Promise.all([
|
||||||
await organizationApi.cleanUp();
|
roleApi.cleanUp(),
|
||||||
await deleteResource(resource.id);
|
organizationApi.cleanUp(),
|
||||||
await deleteUser(user.id);
|
deleteResource(resource.id),
|
||||||
|
deleteUser(user.id),
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -259,5 +259,59 @@ devFeatureTest.describe('organization application APIs', () => {
|
||||||
expect.objectContaining({ code: 'entity.db_constraint_violated' })
|
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));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -307,6 +307,40 @@ describe('organization user APIs', () => {
|
||||||
expect.objectContaining({ code: 'entity.db_constraint_violated' })
|
expect.objectContaining({ code: 'entity.db_constraint_violated' })
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should be able to get organization roles for a user with or without pagination', async () => {
|
||||||
|
const organization = await organizationApi.create({ name: 'test' });
|
||||||
|
const user = await userApi.create({ username: generateTestName() });
|
||||||
|
const roles = await Promise.all(
|
||||||
|
Array.from({ length: 30 }).map(async () => roleApi.create({ name: generateTestName() }))
|
||||||
|
);
|
||||||
|
|
||||||
|
await organizationApi.addUsers(organization.id, [user.id]);
|
||||||
|
await organizationApi.addUserRoles(
|
||||||
|
organization.id,
|
||||||
|
user.id,
|
||||||
|
roles.map(({ id }) => id)
|
||||||
|
);
|
||||||
|
|
||||||
|
const roles1 = await organizationApi.getUserRoles(organization.id, user.id, {
|
||||||
|
page: 1,
|
||||||
|
page_size: 20,
|
||||||
|
});
|
||||||
|
const roles2 = await organizationApi.getUserRoles(organization.id, user.id, {
|
||||||
|
page: 2,
|
||||||
|
page_size: 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(roles1).toHaveLength(20);
|
||||||
|
expect(roles2).toHaveLength(10);
|
||||||
|
expect(roles2[0]?.id).toBe(roles1[10]?.id);
|
||||||
|
expect(roles).toEqual(expect.arrayContaining(roles1));
|
||||||
|
expect(roles).toEqual(expect.arrayContaining(roles2));
|
||||||
|
|
||||||
|
const allRoles = await organizationApi.getUserRoles(organization.id, user.id);
|
||||||
|
expect(allRoles).toHaveLength(30);
|
||||||
|
expect(allRoles).toEqual(expect.arrayContaining(roles));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('organization - user - organization role - organization scopes relation', () => {
|
describe('organization - user - organization role - organization scopes relation', () => {
|
||||||
|
|
Loading…
Add table
Reference in a new issue