@uniorganization/uni-lib 4.2.0 → 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/modules/common/jwt.strategy.js +1 -2
- package/dist/modules/common/jwt.strategy.spec.d.ts +1 -0
- package/dist/modules/common/jwt.strategy.spec.js +89 -0
- package/dist/modules/tenant/tenant-authorization.service.d.ts +1 -1
- package/dist/modules/tenant/tenant-authorization.service.js +3 -3
- package/dist/modules/tenant/tenant-authorization.service.spec.d.ts +1 -0
- package/dist/modules/tenant/tenant-authorization.service.spec.js +101 -0
- package/package.json +1 -1
|
@@ -62,7 +62,6 @@ let JwtStrategy = class JwtStrategy extends (0, passport_1.PassportStrategy)(pas
|
|
|
62
62
|
const impersonating = originalClientId !== null;
|
|
63
63
|
if ((impersonating &&
|
|
64
64
|
(originalClientId !== tenant_1.NEWTECH_CLIENT_ID ||
|
|
65
|
-
homeClientId !== tenant_1.NEWTECH_CLIENT_ID ||
|
|
66
65
|
effectiveClientId === tenant_1.NEWTECH_CLIENT_ID ||
|
|
67
66
|
!((_b = signedUser.roles) === null || _b === void 0 ? void 0 : _b.includes(tenant_1.TENANT_IMPERSONATOR_ROLE)) ||
|
|
68
67
|
!signedUser.impersonatedAt ||
|
|
@@ -71,7 +70,7 @@ let JwtStrategy = class JwtStrategy extends (0, passport_1.PassportStrategy)(pas
|
|
|
71
70
|
throw new common_1.UnauthorizedException();
|
|
72
71
|
}
|
|
73
72
|
const resolved = impersonating
|
|
74
|
-
? yield this.authorization.resolveImpersonation(signedUser.id, homeClientId, effectiveClientId)
|
|
73
|
+
? yield this.authorization.resolveImpersonation(signedUser.id, originalClientId, homeClientId, effectiveClientId)
|
|
75
74
|
: yield this.authorization.resolve(signedUser.id, effectiveClientId, homeClientId);
|
|
76
75
|
const { identity, snapshot } = resolved;
|
|
77
76
|
const isMasterTenantAdmin = !impersonating &&
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
const common_1 = require("@nestjs/common");
|
|
13
|
+
const tenant_1 = require("../tenant");
|
|
14
|
+
const jwt_strategy_1 = require("./jwt.strategy");
|
|
15
|
+
describe('JwtStrategy impersonated sessions', () => {
|
|
16
|
+
process.env.JWT_SECRET = 'test-secret';
|
|
17
|
+
const authorization = { resolveImpersonation: jest.fn() };
|
|
18
|
+
const securityState = { isAccessTokenRevoked: jest.fn() };
|
|
19
|
+
const strategy = new jwt_strategy_1.JwtStrategy(authorization, securityState);
|
|
20
|
+
const payload = (overrides = {}) => {
|
|
21
|
+
const now = Math.floor(Date.now() / 1000);
|
|
22
|
+
const expiresAt = now + 900;
|
|
23
|
+
const user = Object.assign({ id: 1, homeClientId: 2, clientId: 2, clientName: 'Altera', clientAlias: 'altera', jti: 'impersonated-jti', tokenExpiresAt: expiresAt, originalClientId: tenant_1.NEWTECH_CLIENT_ID, impersonatedAt: new Date().toISOString(), impersonationReason: 'Investigate issue', roles: [tenant_1.TENANT_IMPERSONATOR_ROLE], privileges: [], role: {
|
|
24
|
+
id: 0,
|
|
25
|
+
code: tenant_1.TENANT_IMPERSONATOR_ROLE,
|
|
26
|
+
name: 'Tenant impersonator',
|
|
27
|
+
scope: 'GLOBAL',
|
|
28
|
+
clientId: null,
|
|
29
|
+
}, permissions: [], authzVersion: 1, isMasterTenantAdmin: false }, overrides);
|
|
30
|
+
return {
|
|
31
|
+
type: 'access',
|
|
32
|
+
user,
|
|
33
|
+
sub: '1',
|
|
34
|
+
iat: now - 10,
|
|
35
|
+
exp: expiresAt,
|
|
36
|
+
jti: 'impersonated-jti',
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
jest.clearAllMocks();
|
|
41
|
+
securityState.isAccessTokenRevoked.mockResolvedValue(false);
|
|
42
|
+
authorization.resolveImpersonation.mockResolvedValue({
|
|
43
|
+
identity: { id: 1, homeClientId: 2 },
|
|
44
|
+
snapshot: {
|
|
45
|
+
userId: 1,
|
|
46
|
+
homeClientId: 2,
|
|
47
|
+
clientId: 2,
|
|
48
|
+
clientName: 'Altera',
|
|
49
|
+
clientAlias: 'altera',
|
|
50
|
+
roles: [
|
|
51
|
+
{
|
|
52
|
+
roleId: 0,
|
|
53
|
+
roleName: tenant_1.TENANT_IMPERSONATOR_ROLE,
|
|
54
|
+
moduleId: 0,
|
|
55
|
+
moduleName: tenant_1.TENANT_IMPERSONATOR_ROLE,
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
privileges: [],
|
|
59
|
+
accessRole: {
|
|
60
|
+
id: 0,
|
|
61
|
+
code: tenant_1.TENANT_IMPERSONATOR_ROLE,
|
|
62
|
+
name: 'Tenant impersonator',
|
|
63
|
+
scope: 'GLOBAL',
|
|
64
|
+
clientId: null,
|
|
65
|
+
},
|
|
66
|
+
permissions: [],
|
|
67
|
+
authzVersion: 1,
|
|
68
|
+
identity: { id: 1, homeClientId: 2 },
|
|
69
|
+
cachedAt: new Date().toISOString(),
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
it('revalidates an impersonated token with an Altera home client through Newtech', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
74
|
+
const validated = yield strategy.validate(payload());
|
|
75
|
+
expect(authorization.resolveImpersonation).toHaveBeenCalledWith(1, 1, 2, 2);
|
|
76
|
+
expect(validated.homeClientId).toBe(2);
|
|
77
|
+
expect(validated.clientId).toBe(2);
|
|
78
|
+
expect(validated.originalClientId).toBe(tenant_1.NEWTECH_CLIENT_ID);
|
|
79
|
+
}));
|
|
80
|
+
const invalidTokenCases = [
|
|
81
|
+
[{ originalClientId: 2 }, 'a non-Newtech origin'],
|
|
82
|
+
[{ roles: [] }, 'a token without the impersonator role'],
|
|
83
|
+
[{ impersonationReason: null }, 'a token without a support reason'],
|
|
84
|
+
];
|
|
85
|
+
it.each(invalidTokenCases)('rejects %s', (overrides) => __awaiter(void 0, void 0, void 0, function* () {
|
|
86
|
+
yield expect(strategy.validate(payload(overrides))).rejects.toBeInstanceOf(common_1.UnauthorizedException);
|
|
87
|
+
expect(authorization.resolveImpersonation).not.toHaveBeenCalled();
|
|
88
|
+
}));
|
|
89
|
+
});
|
|
@@ -13,7 +13,7 @@ export declare class TenantAuthorizationService {
|
|
|
13
13
|
private readonly logger;
|
|
14
14
|
constructor(store: TenantAuthorizationStore, userRepository: Repository<Users>, clientRepository: Repository<Client>, userRoleRepository: Repository<UserRoles>);
|
|
15
15
|
resolve(userId: number, clientId: number, expectedHomeClientId: number): Promise<ResolvedTenantAuthorization>;
|
|
16
|
-
resolveImpersonation(userId: number, homeClientId: number, targetClientId: number): Promise<ResolvedTenantAuthorization>;
|
|
16
|
+
resolveImpersonation(userId: number, originClientId: number, homeClientId: number, targetClientId: number): Promise<ResolvedTenantAuthorization>;
|
|
17
17
|
invalidate(userId: number, clientId: number): Promise<void>;
|
|
18
18
|
invalidateMany(clientId: number, userIds: readonly number[]): Promise<void>;
|
|
19
19
|
private loadFromDatabase;
|
|
@@ -64,13 +64,13 @@ let TenantAuthorizationService = TenantAuthorizationService_1 = class TenantAuth
|
|
|
64
64
|
return resolved;
|
|
65
65
|
});
|
|
66
66
|
}
|
|
67
|
-
resolveImpersonation(userId, homeClientId, targetClientId) {
|
|
67
|
+
resolveImpersonation(userId, originClientId, homeClientId, targetClientId) {
|
|
68
68
|
return __awaiter(this, void 0, void 0, function* () {
|
|
69
|
-
if (
|
|
69
|
+
if (originClientId !== tenant_constants_1.NEWTECH_CLIENT_ID ||
|
|
70
70
|
targetClientId === tenant_constants_1.NEWTECH_CLIENT_ID) {
|
|
71
71
|
throw new common_1.UnauthorizedException();
|
|
72
72
|
}
|
|
73
|
-
const origin = yield this.resolve(userId,
|
|
73
|
+
const origin = yield this.resolve(userId, originClientId, homeClientId);
|
|
74
74
|
const isNewtechMaster = origin.snapshot.permissions.includes('master.users.read') ||
|
|
75
75
|
origin.snapshot.roles.some((role) => role.moduleName.toLowerCase() === 'root' &&
|
|
76
76
|
role.roleName.toLowerCase() === 'sysadmin');
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
const common_1 = require("@nestjs/common");
|
|
13
|
+
const tenant_constants_1 = require("./tenant.constants");
|
|
14
|
+
const tenant_authorization_service_1 = require("./tenant-authorization.service");
|
|
15
|
+
describe('TenantAuthorizationService impersonation', () => {
|
|
16
|
+
const store = {
|
|
17
|
+
get: jest.fn(),
|
|
18
|
+
set: jest.fn(),
|
|
19
|
+
delete: jest.fn(),
|
|
20
|
+
deleteMany: jest.fn(),
|
|
21
|
+
};
|
|
22
|
+
const userRepository = {};
|
|
23
|
+
const clientRepository = { findOne: jest.fn() };
|
|
24
|
+
const userRoleRepository = {};
|
|
25
|
+
const service = new tenant_authorization_service_1.TenantAuthorizationService(store, userRepository, clientRepository, userRoleRepository);
|
|
26
|
+
const origin = (permissions) => ({
|
|
27
|
+
identity: {
|
|
28
|
+
id: 1,
|
|
29
|
+
homeClientId: 2,
|
|
30
|
+
firstName: 'Root',
|
|
31
|
+
lastName: 'User',
|
|
32
|
+
},
|
|
33
|
+
snapshot: {
|
|
34
|
+
userId: 1,
|
|
35
|
+
homeClientId: 2,
|
|
36
|
+
clientId: tenant_constants_1.NEWTECH_CLIENT_ID,
|
|
37
|
+
clientName: 'Newtech',
|
|
38
|
+
clientAlias: 'newtech',
|
|
39
|
+
roles: [
|
|
40
|
+
{
|
|
41
|
+
roleId: 8,
|
|
42
|
+
roleName: 'Master Root',
|
|
43
|
+
moduleId: 8,
|
|
44
|
+
moduleName: 'access-control',
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
privileges: [],
|
|
48
|
+
accessRole: {
|
|
49
|
+
id: 8,
|
|
50
|
+
code: 'master-root',
|
|
51
|
+
name: 'Master Root',
|
|
52
|
+
scope: 'CLIENT',
|
|
53
|
+
clientId: tenant_constants_1.NEWTECH_CLIENT_ID,
|
|
54
|
+
},
|
|
55
|
+
permissions,
|
|
56
|
+
authzVersion: 3,
|
|
57
|
+
identity: {
|
|
58
|
+
id: 1,
|
|
59
|
+
homeClientId: 2,
|
|
60
|
+
firstName: 'Root',
|
|
61
|
+
lastName: 'User',
|
|
62
|
+
},
|
|
63
|
+
cachedAt: new Date().toISOString(),
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
beforeEach(() => {
|
|
67
|
+
jest.clearAllMocks();
|
|
68
|
+
clientRepository.findOne.mockResolvedValue({
|
|
69
|
+
id: 2,
|
|
70
|
+
name: 'Altera',
|
|
71
|
+
alias: 'altera',
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
it('resolves support from active Newtech when the permanent home is Altera', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
75
|
+
jest
|
|
76
|
+
.spyOn(service, 'resolve')
|
|
77
|
+
.mockResolvedValue(origin(['master.users.read']));
|
|
78
|
+
const resolved = yield service.resolveImpersonation(1, tenant_constants_1.NEWTECH_CLIENT_ID, 2, 2);
|
|
79
|
+
expect(service.resolve).toHaveBeenCalledWith(1, tenant_constants_1.NEWTECH_CLIENT_ID, 2);
|
|
80
|
+
expect(resolved.snapshot.clientId).toBe(2);
|
|
81
|
+
expect(resolved.snapshot.homeClientId).toBe(2);
|
|
82
|
+
expect(resolved.snapshot.clientName).toBe('Altera');
|
|
83
|
+
}));
|
|
84
|
+
it('rejects an origin outside Newtech', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
85
|
+
yield expect(service.resolveImpersonation(1, 2, 2, 2)).rejects.toBeInstanceOf(common_1.UnauthorizedException);
|
|
86
|
+
}));
|
|
87
|
+
it('rejects Newtech as the target', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
88
|
+
yield expect(service.resolveImpersonation(1, tenant_constants_1.NEWTECH_CLIENT_ID, 2, tenant_constants_1.NEWTECH_CLIENT_ID)).rejects.toBeInstanceOf(common_1.UnauthorizedException);
|
|
89
|
+
}));
|
|
90
|
+
it('rejects an origin without master access', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
91
|
+
jest.spyOn(service, 'resolve').mockResolvedValue(origin([]));
|
|
92
|
+
yield expect(service.resolveImpersonation(1, tenant_constants_1.NEWTECH_CLIENT_ID, 2, 2)).rejects.toBeInstanceOf(common_1.UnauthorizedException);
|
|
93
|
+
}));
|
|
94
|
+
it('rejects an inactive or missing target client', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
95
|
+
jest
|
|
96
|
+
.spyOn(service, 'resolve')
|
|
97
|
+
.mockResolvedValue(origin(['master.users.read']));
|
|
98
|
+
clientRepository.findOne.mockResolvedValue(null);
|
|
99
|
+
yield expect(service.resolveImpersonation(1, tenant_constants_1.NEWTECH_CLIENT_ID, 2, 2)).rejects.toBeInstanceOf(common_1.UnauthorizedException);
|
|
100
|
+
}));
|
|
101
|
+
});
|