Skip to content

Commit 24631b4

Browse files
committed
fix(api): query scope for RolesService and OrganizationService
1 parent 3d8efb5 commit 24631b4

File tree

8 files changed

+81
-72
lines changed

8 files changed

+81
-72
lines changed

modules/services/api/src/auth/utils/user-permissions.util.ts

Lines changed: 24 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -15,58 +15,38 @@ export class UserPermissions {
1515
*/
1616
public check(
1717
permissions: domain.PermissionKey[],
18-
scope: { organizationId: string; schoolId?: string },
18+
scope?: { organizationId: string; schoolId?: string },
1919
) {
20-
// Check if user has permission on the organization level
21-
const permittedOnOrgLevel = this.getOrganizations(permissions).includes(
22-
scope.organizationId,
23-
);
24-
25-
// Check if user has permission on the school level
26-
const permittedOnSchoolLevel = scope.schoolId
27-
? this.getSchools(permissions).includes(scope.schoolId)
28-
: true;
29-
30-
// If user does not have permission on either the organization
31-
// or school level, throw an error indicating that the user
32-
// does not have permission
33-
if (!permittedOnOrgLevel || !permittedOnSchoolLevel) {
20+
const scopes = this.getScopes(permissions);
21+
if (!scopes.length) {
3422
throw new ForbiddenException('User does not have permission');
3523
}
36-
}
3724

38-
/**
39-
* Returns list of organization ids that user has access to
40-
* based on the provided permissions.
41-
* @param permissions List of permissions to check
42-
* @returns List of organization ids that user has access to
43-
*/
44-
public getOrganizations(permissions: domain.PermissionKey[]): string[] {
45-
return (
46-
this.userPermissions
47-
// organization level permissions
48-
.filter((p) => p.oid && !p.sid)
49-
// check if user has all required permissions
50-
.filter((p) =>
51-
permissions.every((permission) => p.p.includes(permission)),
52-
)
53-
.map((p) => p.oid)
25+
if (!scope) {
26+
return;
27+
}
28+
29+
const permitted = scopes.some(
30+
(s) =>
31+
s.organizationId == scope?.organizationId &&
32+
s.schoolId == scope?.schoolId,
5433
);
34+
35+
if (!permitted) {
36+
throw new ForbiddenException('User does not have permission');
37+
}
5538
}
5639

57-
/**
58-
* Returns list of school ids that user has access to
59-
* based on the provided permissions
60-
* @param permissions List of permissions to check
61-
* @returns List of school ids that user has access to
62-
*/
63-
public getSchools(permissions: domain.PermissionKey[]): string[] {
40+
public getScopes(
41+
permissions: domain.PermissionKey[],
42+
): { organizationId: string; schoolId?: string }[] {
6443
return this.userPermissions
65-
.filter(
66-
(userPermission) =>
67-
userPermission.sid &&
68-
permissions.every((p) => userPermission.p.includes(p)),
44+
.filter((p) =>
45+
permissions.every((permission) => p.p.includes(permission)),
6946
)
70-
.map((p) => p.sid);
47+
.map((p) => ({
48+
organizationId: p.oid,
49+
schoolId: p.sid,
50+
}));
7151
}
7252
}

modules/services/api/src/edu/controllers/organizations/organizations.controller.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export class OrganizationsController {
4646
@Param('id', new ParseUUIDPipe()) id: string,
4747
@UserWithPermissions() userPermissions: UserPermissions,
4848
): Promise<dto.GetOrganizationResponse> {
49+
userPermissions.check(['orgs:read']);
4950
const orgs = await this.organizationsService
5051
.scopedBy({ permissions: userPermissions })
5152
.findAll({ where: { id } });
@@ -67,6 +68,7 @@ export class OrganizationsController {
6768
async getMany(
6869
@UserWithPermissions() userPermissions: UserPermissions,
6970
): Promise<GetOrganizationsResponse> {
71+
userPermissions.check(['orgs:read']);
7072
const orgs = await this.organizationsService
7173
.scopedBy({ permissions: userPermissions })
7274
.findAll({});

modules/services/api/src/edu/controllers/organizations/specs/organizations.controller.e2e.get.spec.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,6 @@ describe('/edu/orgs', () => {
6767
return request(app.getHttpServer())
6868
.get(Routes().edu.org.find())
6969
.set('Authorization', `Bearer ${ctx.empty.tokens.noPermissions}`)
70-
.expect(200)
71-
.expect({ items: [] });
70+
.expect(403);
7271
});
7372
});

modules/services/api/src/edu/controllers/organizations/specs/organizations.controller.test.get.spec.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@ describe('OrganizationsController', () => {
2020

2121
describe('getOrganizations', () => {
2222
it('should return an empty array if no permissions granted', async () => {
23-
const res = await ctr.getMany(ctx.empty.permissions.no);
24-
expect(res.items).toEqual([]);
23+
await expect(
24+
async () => await ctr.getMany(ctx.empty.permissions.no),
25+
).rejects.toThrow();
2526
});
2627

2728
it('should return all permitted organizations', async () => {

modules/services/api/src/edu/controllers/roles/roles.controller.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export class RolesController {
4747
@Param('id', new ParseUUIDPipe()) id: string,
4848
@UserWithPermissions() permissions: UserPermissions,
4949
): Promise<dto.GetRoleResponse> {
50+
permissions.check(['roles:read']);
5051
const roles = await this.rolesService
5152
.scopedBy({ permissions })
5253
.findAll({ where: { id } });
@@ -65,7 +66,13 @@ export class RolesController {
6566
@Query() query: dto.GetRoleSummariesListQuery,
6667
@UserWithPermissions() permissions: UserPermissions,
6768
): Promise<dto.GetRolesResponse> {
68-
const roles = await this.rolesService.scopedBy({ permissions }).findAll({});
69+
permissions.check(['roles:read']);
70+
const roles = await this.rolesService.scopedBy({ permissions }).findAll({
71+
where: {
72+
organizationId: query.organizationId,
73+
schoolId: query.schoolId,
74+
},
75+
});
6976
return new dto.GetRolesResponse({
7077
items: this.mapper.mapArray(roles, entities.Role, dto.RoleSummary),
7178
});

modules/services/api/src/edu/controllers/roles/specs/roles.controller.e2e.get.spec.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,6 @@ describe('/edu/roles', () => {
101101
return request(app.getHttpServer())
102102
.get(Routes().edu.roles.find())
103103
.set('Authorization', `Bearer ${ctx.empty.tokens.noPermissions}`)
104-
.expect(200)
105-
.expect({ items: [] });
104+
.expect(403);
106105
});
107106
});

modules/services/api/src/edu/services/organizations.service.ts

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,23 @@ export class OrganizationsService extends ScopedEntitiesService<
1818
// work here, so we have to cast it to any. It doesn't contain
1919
// any fields like id, name, etc. But it should.
2020
const where = query?.where as any;
21-
let orgIds = scope.permissions.getOrganizations(['orgs:read']);
2221

23-
// If the query has an id, so check if it is in the list of
24-
// organization ids that the user has access to.
25-
if (where?.id) {
26-
orgIds = [where.id].filter((id) => orgIds.includes(id));
27-
}
22+
// Get all scopes that have the required permission and match the
23+
// organization id in the query if it is provided
24+
const scopes = scope.permissions
25+
.getScopes(['orgs:read'])
26+
.filter((s) => !where?.id || s.organizationId === where?.id);
2827

29-
return {
30-
where: {
31-
...query?.where,
32-
id: In(orgIds),
33-
},
34-
};
28+
// Return the query with the scopes applied or an empty query
29+
// if no scopes were found for the user permissions
30+
return scopes.length > 0
31+
? {
32+
where: scopes.map((s) => ({
33+
...query?.where,
34+
id: s.organizationId,
35+
})),
36+
}
37+
: { where: { id: In([]) } };
3538
});
3639
}
3740
}

modules/services/api/src/edu/services/roles.service.ts

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,33 @@ export class RolesService extends ScopedEntitiesService<Role, Scope> {
1212
@InjectRepository(UserRole) private userRolesRepo: Repository<UserRole>,
1313
) {
1414
super(repository, (query, scope) => {
15-
const orgIds = scope.permissions.getOrganizations(['roles:read']);
16-
const schoolIds = scope.permissions.getSchools(['roles:read']);
17-
return {
18-
where: {
19-
...query?.where,
20-
organizationId: schoolIds.length == 0 ? In(orgIds) : undefined,
21-
schoolId: schoolIds.length > 0 ? In(schoolIds) : undefined,
22-
},
23-
};
15+
// HACK: For some reason FindOptionsWhere<Organization> doesn't
16+
// work here, so we have to cast it to any. It doesn't contain
17+
// any fields like id, name, etc. But it should.
18+
const { organizationId, schoolId } = query?.where as any;
19+
20+
// Get all scopes that have the required permission
21+
// and match the organization and school ids in the query
22+
// if they are provided
23+
const scopes = scope.permissions
24+
.getScopes(['roles:read'])
25+
.filter(
26+
(s) =>
27+
(!organizationId || s.organizationId === organizationId) &&
28+
(!schoolId || s.schoolId === schoolId),
29+
);
30+
31+
// Return the query with the scopes applied or an empty query
32+
// if no scopes were found for the user permissions
33+
return scopes.length > 0
34+
? {
35+
where: scopes.map((s) => ({
36+
...query?.where,
37+
organizationId: s.organizationId,
38+
schoolId: s.schoolId,
39+
})),
40+
}
41+
: { where: { organizationId: In([]) } };
2442
});
2543
}
2644

0 commit comments

Comments
 (0)