Skip to content
Open
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
21 changes: 21 additions & 0 deletions src/shared/services/permission.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,27 @@ describe('PermissionService', () => {
expect(allowed).toBe(true);
});

it.each([
Permission.CREATE_PROJECT_MEMBER_NOT_OWN,
Permission.UPDATE_PROJECT_MEMBER_NON_CUSTOMER,
Permission.DELETE_PROJECT_MEMBER_TOPCODER,
])(
'allows %s when raw M2M token scopes are broader than user.scopes',
(permission) => {
const allowed = service.hasNamedPermission(permission, {
scopes: [Scope.PROJECTS_READ],
isMachine: false,
tokenPayload: {
gty: 'client-credentials',
scope: `${Scope.PROJECTS_READ} ${Scope.PROJECT_MEMBERS_WRITE}`,
sub: 'svc-projects@clients',
},
});

expect(allowed).toBe(true);
},
);

it('allows reading other users project invites for machine token with invite read scope', () => {
const allowed = service.hasNamedPermission(
Permission.READ_PROJECT_INVITE_NOT_OWN,
Expand Down
18 changes: 14 additions & 4 deletions src/shared/services/permission.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,11 @@ export class PermissionService {
* Resolves machine-token status and effective scopes from the normalized user
* and the raw token payload so guard and permission checks stay aligned.
*
* Merges both scope sources because upstream auth middleware can populate
* `user.scopes` differently from the raw token payload used by
* `TokenRolesGuard`. Keeping the union here avoids false 403s when one source
* is stale or incomplete but the other still carries the granted M2M scopes.
*
* @param user authenticated JWT user context
* @returns machine classification and the scopes to evaluate
*/
Expand All @@ -643,13 +648,18 @@ export class PermissionService {
const payloadMachineContext = this.m2mService.validateMachineToken(
user.tokenPayload,
);
const userScopes = Array.isArray(user.scopes)
? user.scopes
.map((scope) => String(scope).trim())
.filter((scope) => scope.length > 0)
: [];
const mergedScopes = Array.from(
new Set([...userScopes, ...payloadMachineContext.scopes]),
);

return {
isMachine: Boolean(user.isMachine || payloadMachineContext.isMachine),
scopes:
Array.isArray(user.scopes) && user.scopes.length > 0
? user.scopes
: payloadMachineContext.scopes,
scopes: mergedScopes,
};
}

Expand Down
Loading