Skip to content

Commit edc65e4

Browse files
authored
Merge pull request #149 from topcoder-platform/dev
Fix for wallet-admin switch for admin vs. engagement approver
2 parents 0ed0b02 + 100e9a9 commit edc65e4

2 files changed

Lines changed: 152 additions & 2 deletions

File tree

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { Role } from 'src/core/auth/auth.constants';
2+
import { AccessControlService } from './access-control.service';
3+
import { RoleAccessProvider } from './role-access.interface';
4+
5+
describe('AccessControlService', () => {
6+
let service: AccessControlService;
7+
8+
beforeEach(() => {
9+
service = new AccessControlService();
10+
});
11+
12+
it('skips engagement approver filters when payment admin role is also present', async () => {
13+
const paymentAdminApplyFilter = jest
14+
.fn()
15+
.mockImplementation((_userId, req: Record<string, unknown>) =>
16+
Promise.resolve({
17+
...req,
18+
admin: true,
19+
}),
20+
);
21+
const engagementApproverApplyFilter = jest
22+
.fn()
23+
.mockImplementation((_userId, req: Record<string, unknown>) =>
24+
Promise.resolve({
25+
...req,
26+
category: 'ENGAGEMENT_PAYMENT',
27+
}),
28+
);
29+
const paymentAdminProvider: RoleAccessProvider<Record<string, unknown>> = {
30+
roleName: Role.PaymentAdmin,
31+
applyFilter: paymentAdminApplyFilter,
32+
};
33+
const engagementApproverProvider: RoleAccessProvider<
34+
Record<string, unknown>
35+
> = {
36+
roleName: Role.EngagementPaymentApprover,
37+
applyFilter: engagementApproverApplyFilter,
38+
};
39+
40+
service.register(paymentAdminProvider);
41+
service.register(engagementApproverProvider);
42+
43+
const result = await service.applyFilters<Record<string, unknown>>(
44+
'88770025',
45+
[Role.PaymentAdmin, Role.EngagementPaymentApprover],
46+
{ type: 'PAYMENT' },
47+
);
48+
49+
expect(result).toEqual({
50+
admin: true,
51+
type: 'PAYMENT',
52+
});
53+
expect(paymentAdminApplyFilter).toHaveBeenCalledTimes(1);
54+
expect(engagementApproverApplyFilter).not.toHaveBeenCalled();
55+
});
56+
57+
it('applies engagement approver filters when payment admin role is absent', async () => {
58+
const engagementApproverApplyFilter = jest
59+
.fn()
60+
.mockImplementation((_userId, req: Record<string, unknown>) =>
61+
Promise.resolve({
62+
...req,
63+
category: 'ENGAGEMENT_PAYMENT',
64+
}),
65+
);
66+
const engagementApproverProvider: RoleAccessProvider<
67+
Record<string, unknown>
68+
> = {
69+
roleName: Role.EngagementPaymentApprover,
70+
applyFilter: engagementApproverApplyFilter,
71+
};
72+
73+
service.register(engagementApproverProvider);
74+
75+
const result = await service.applyFilters<Record<string, unknown>>(
76+
'88770025',
77+
[Role.EngagementPaymentApprover],
78+
{ type: 'PAYMENT' },
79+
);
80+
81+
expect(result).toEqual({
82+
category: 'ENGAGEMENT_PAYMENT',
83+
type: 'PAYMENT',
84+
});
85+
expect(engagementApproverApplyFilter).toHaveBeenCalledTimes(1);
86+
});
87+
88+
it('skips engagement approver resource checks when payment admin role is also present', async () => {
89+
const paymentAdminVerifyAccess = jest.fn().mockResolvedValue(undefined);
90+
const engagementApproverVerifyAccess = jest
91+
.fn()
92+
.mockRejectedValue(new Error('should not be called'));
93+
const paymentAdminProvider: RoleAccessProvider = {
94+
roleName: Role.PaymentAdmin,
95+
verifyAccessToResource: paymentAdminVerifyAccess,
96+
};
97+
const engagementApproverProvider: RoleAccessProvider = {
98+
roleName: Role.EngagementPaymentApprover,
99+
verifyAccessToResource: engagementApproverVerifyAccess,
100+
};
101+
102+
service.register(paymentAdminProvider);
103+
service.register(engagementApproverProvider);
104+
105+
await expect(
106+
service.verifyAccess('winning-id', '88770025', [
107+
Role.PaymentAdmin,
108+
Role.EngagementPaymentApprover,
109+
]),
110+
).resolves.toBeUndefined();
111+
112+
expect(paymentAdminVerifyAccess).toHaveBeenCalledTimes(1);
113+
expect(engagementApproverVerifyAccess).not.toHaveBeenCalled();
114+
});
115+
});

src/shared/access-control/access-control.service.ts

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Injectable } from '@nestjs/common';
2+
import { Role } from 'src/core/auth/auth.constants';
23
import { RoleAccessProvider } from './role-access.interface';
34

45
@Injectable()
@@ -9,14 +10,39 @@ export class AccessControlService {
910
this.providers.set(provider.roleName.trim().toLowerCase(), provider);
1011
}
1112

13+
private normalizeRoles(roles: string[] = []): Set<string> {
14+
return new Set(
15+
(roles || []).map((role) => role?.trim().toLowerCase()).filter(Boolean),
16+
);
17+
}
18+
19+
private shouldSkipProvider(
20+
providerRole: string,
21+
normalizedRoles: Set<string>,
22+
): boolean {
23+
return (
24+
providerRole === Role.EngagementPaymentApprover.trim().toLowerCase() &&
25+
normalizedRoles.has(Role.PaymentAdmin.trim().toLowerCase())
26+
);
27+
}
28+
1229
async applyFilters<T>(
1330
userId: string,
1431
roles: string[] = [],
1532
req: any,
1633
): Promise<T> {
1734
let out = { ...req };
35+
const normalizedRoles = this.normalizeRoles(roles);
1836
for (const r of roles || []) {
19-
const p = this.providers.get(r?.trim().toLowerCase());
37+
const normalizedRole = r?.trim().toLowerCase();
38+
if (
39+
!normalizedRole ||
40+
this.shouldSkipProvider(normalizedRole, normalizedRoles)
41+
) {
42+
continue;
43+
}
44+
45+
const p = this.providers.get(normalizedRole);
2046
if (p?.applyFilter) {
2147
out = await p.applyFilter(userId, out);
2248
}
@@ -25,8 +51,17 @@ export class AccessControlService {
2551
}
2652

2753
async verifyAccess(resourceId: string, userId: string, roles: string[] = []) {
54+
const normalizedRoles = this.normalizeRoles(roles);
2855
for (const r of roles || []) {
29-
const p = this.providers.get(r?.trim().toLowerCase());
56+
const normalizedRole = r?.trim().toLowerCase();
57+
if (
58+
!normalizedRole ||
59+
this.shouldSkipProvider(normalizedRole, normalizedRoles)
60+
) {
61+
continue;
62+
}
63+
64+
const p = this.providers.get(normalizedRole);
3065
if (p?.verifyAccessToResource) {
3166
await p.verifyAccessToResource(resourceId, userId);
3267
}

0 commit comments

Comments
 (0)