Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): update application organization role apis #6087

Merged
merged 1 commit into from
Jun 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
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 @@
}
);

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

router.get(
pathname,
koaPagination(),
koaPagination({ isOptional: true }),
koaGuard({
params: z.object(params),
response: OrganizationRoles.guard.array(),
Expand All @@ -49,10 +49,14 @@
{
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
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
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@

router.get(
pathname,
koaPagination(),
koaPagination({ isOptional: true }),
koaGuard({
params: z.object(params),
response: OrganizationRoles.guard.array(),
Expand All @@ -54,10 +54,12 @@
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));
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,40 @@ describe('organization user APIs', () => {
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', () => {
Expand Down
Loading