0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

feat(core): add api to fetch organization scopes for a user (#5701)

* feat(core): add api to fetch user organization scopes

* chore: add openapi.json

* fix: integration test

* chore: turn off max-lines lint rules for openapi json files

* chore: add changeset

* refactor: return all scope information instead of just the name
This commit is contained in:
Charles Zhao 2024-04-15 15:04:42 +08:00 committed by GitHub
parent 1b4c106603
commit aacbebcbc8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 98 additions and 7 deletions

View file

@ -0,0 +1,7 @@
---
"@logto/core": patch
---
Provide management API to fetch user organization scopes based on user organization roles
- GET `organizations/:id/users/:userId/scopes`

View file

@ -148,6 +148,14 @@
"import/no-unused-modules": "off"
}
},
{
"files": [
"*.openapi.json"
],
"rules": {
"max-lines": "off"
}
},
{
"files": [
"src/include.d/oidc-provider/**/*"

View file

@ -307,9 +307,9 @@ describe('organization token grant', () => {
Sinon.stub(tenant.queries.organizations.relations.users, 'exists').resolves(true);
Sinon.stub(tenant.queries.applications, 'findApplicationById').resolves(mockApplication);
Sinon.stub(tenant.queries.organizations.relations.rolesUsers, 'getUserScopes').resolves([
{ id: 'foo', name: 'foo' },
{ id: 'bar', name: 'bar' },
{ id: 'baz', name: 'baz' },
{ tenantId: 'default', id: 'foo', name: 'foo', description: 'foo' },
{ tenantId: 'default', id: 'bar', name: 'bar', description: 'bar' },
{ tenantId: 'default', id: 'baz', name: 'baz', description: 'baz' },
]);
const entityStub = Sinon.stub(ctx.oidc, 'entity');

View file

@ -9,7 +9,7 @@ import {
type OrganizationWithRoles,
type UserWithOrganizationRoles,
type FeaturedUser,
type OrganizationScopeEntity,
type OrganizationScope,
} from '@logto/schemas';
import { sql, type CommonQueryMethods } from '@silverhand/slonik';
@ -178,14 +178,14 @@ export class RoleUserRelationQueries extends RelationQueries<
async getUserScopes(
organizationId: string,
userId: string
): Promise<readonly OrganizationScopeEntity[]> {
): Promise<readonly OrganizationScope[]> {
const { fields } = convertToIdentifiers(OrganizationRoleUserRelations, true);
const roleScopeRelations = convertToIdentifiers(OrganizationRoleScopeRelations, true);
const scopes = convertToIdentifiers(OrganizationScopes, true);
return this.pool.any<OrganizationScopeEntity>(sql`
return this.pool.any<OrganizationScope>(sql`
select distinct on (${scopes.fields.id})
${scopes.fields.id}, ${scopes.fields.name}
${sql.join(Object.values(scopes.fields), sql`, `)}
from ${this.table}
join ${roleScopeRelations.table}
on ${roleScopeRelations.fields.organizationRoleId} = ${fields.organizationRoleId}

View file

@ -288,6 +288,20 @@
}
}
}
},
"/api/organizations/{id}/users/{userId}/scopes": {
"get": {
"summary": "Get scopes for a user in an organization tailored by the organization roles",
"description": "Get scopes assigned to a user in the specified organization tailored by the organization roles. The scopes are derived from the organization roles assigned to the user.",
"responses": {
"200": {
"description": "A list of scopes assigned to the user."
},
"422": {
"description": "The user is not a member of the organization."
}
}
}
}
}
}

View file

@ -4,6 +4,7 @@ import {
Organizations,
featuredUserGuard,
userWithOrganizationRolesGuard,
OrganizationScopes,
} from '@logto/schemas';
import { yes } from '@silverhand/essentials';
import { z } from 'zod';
@ -235,6 +236,23 @@ export default function organizationRoutes<T extends AuthedRouter>(...args: Rout
}
);
router.get(
'/:id/users/:userId/scopes',
koaGuard({
params: z.object(params),
response: z.array(OrganizationScopes.guard),
status: [200, 422],
}),
async (ctx, next) => {
const { id, userId } = ctx.guard.params;
const scopes = await organizations.relations.rolesUsers.getUserScopes(id, userId);
ctx.body = scopes;
return next();
}
);
// MARK: Mount sub-routes
organizationRoleRoutes(...args);
organizationScopeRoutes(...args);

View file

@ -4,6 +4,7 @@ import {
type OrganizationWithRoles,
type UserWithOrganizationRoles,
type OrganizationWithFeatured,
type OrganizationScope,
} from '@logto/schemas';
import { authedAdminApi } from './api.js';
@ -67,4 +68,10 @@ export class OrganizationApi extends ApiFactory<
async getUserOrganizations(userId: string): Promise<OrganizationWithRoles[]> {
return authedAdminApi.get(`users/${userId}/organizations`).json<OrganizationWithRoles[]>();
}
async getUserOrganizationScopes(id: string, userId: string): Promise<OrganizationScope[]> {
return authedAdminApi
.get(`${this.path}/${id}/users/${userId}/scopes`)
.json<OrganizationScope[]>();
}
}

View file

@ -247,4 +247,41 @@ describe('organization user APIs', () => {
expect(response instanceof HTTPError && response.response.status).toBe(422); // Require membership
});
});
describe('organization - user - organization role - organization scopes relation', () => {
it('should be able to get organization scopes for a user with a specific role', async () => {
const organizationApi = new OrganizationApiTest();
const { roleApi, scopeApi } = organizationApi;
const userApi = new UserApiTest();
const organization = await organizationApi.create({ name: 'test' });
const user = await userApi.create({ username: generateTestName() });
await organizationApi.addUsers(organization.id, [user.id]);
const [role1, role2] = await Promise.all([
roleApi.create({ name: generateTestName() }),
roleApi.create({ name: generateTestName() }),
]);
const [scope1, scope2] = await Promise.all([
scopeApi.create({ name: generateTestName() }),
scopeApi.create({ name: generateTestName() }),
]);
// Assign scope1 and scope2 to role1
await roleApi.addScopes(role1.id, [scope1.id, scope2.id]);
// Assign scope1 to role2
await roleApi.addScopes(role2.id, [scope1.id]);
// Assign role1 to user
await organizationApi.addUserRoles(organization.id, user.id, [role1.id]);
const scopes = await organizationApi.getUserOrganizationScopes(organization.id, user.id);
expect(scopes.map(({ name }) => name)).toMatchObject([scope1.name, scope2.name]);
// Remove role1 and assign role2 to user
await organizationApi.deleteUserRole(organization.id, user.id, role1.id);
await organizationApi.addUserRoles(organization.id, user.id, [role2.id]);
const newScopes = await organizationApi.getUserOrganizationScopes(organization.id, user.id);
expect(newScopes.map(({ name }) => name)).toEqual([scope1.name]);
});
});
});