@flusys/nestjs-iam 6.0.1 → 6.1.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/cjs/controllers/action.controller.js +17 -9
- package/cjs/controllers/action.controller.spec.js +79 -0
- package/cjs/controllers/company-action-permission.controller.spec.js +65 -0
- package/cjs/controllers/my-permission.controller.spec.js +56 -0
- package/cjs/controllers/role-permission.controller.spec.js +140 -0
- package/cjs/controllers/role.controller.spec.js +86 -0
- package/cjs/controllers/user-action-permission.controller.js +0 -3
- package/cjs/controllers/user-action-permission.controller.spec.js +68 -0
- package/cjs/docs/iam-swagger.config.js +6 -0
- package/cjs/docs/iam-swagger.config.spec.js +99 -0
- package/cjs/dtos/action.dto.js +17 -0
- package/cjs/entities/index.spec.js +56 -0
- package/cjs/helpers/company-access.helper.spec.js +68 -0
- package/cjs/helpers/permission-mode.helper.spec.js +54 -0
- package/cjs/modules/iam.module.js +3 -1
- package/cjs/modules/iam.module.spec.js +293 -0
- package/cjs/services/action.service.js +90 -7
- package/cjs/services/action.service.spec.js +314 -0
- package/cjs/services/iam-config.service.spec.js +110 -0
- package/cjs/services/iam-datasource.service.spec.js +117 -0
- package/cjs/services/permission-cache.service.js +113 -110
- package/cjs/services/permission-cache.service.spec.js +252 -0
- package/cjs/services/permission.service.js +213 -92
- package/cjs/services/permission.service.spec.js +967 -0
- package/cjs/services/role.service.js +47 -3
- package/cjs/services/role.service.spec.js +312 -0
- package/controllers/action.controller.d.ts +5 -3
- package/dtos/action.dto.d.ts +3 -0
- package/entities/index.d.ts +2 -2
- package/fesm/controllers/action.controller.js +18 -10
- package/fesm/controllers/action.controller.spec.js +75 -0
- package/fesm/controllers/company-action-permission.controller.spec.js +61 -0
- package/fesm/controllers/my-permission.controller.spec.js +52 -0
- package/fesm/controllers/role-permission.controller.spec.js +136 -0
- package/fesm/controllers/role.controller.spec.js +82 -0
- package/fesm/controllers/user-action-permission.controller.js +0 -3
- package/fesm/controllers/user-action-permission.controller.spec.js +64 -0
- package/fesm/docs/iam-swagger.config.js +6 -0
- package/fesm/docs/iam-swagger.config.spec.js +95 -0
- package/fesm/dtos/action.dto.js +14 -0
- package/fesm/entities/index.spec.js +52 -0
- package/fesm/helpers/company-access.helper.spec.js +64 -0
- package/fesm/helpers/permission-mode.helper.spec.js +50 -0
- package/fesm/modules/iam.module.js +4 -2
- package/fesm/modules/iam.module.spec.js +289 -0
- package/fesm/services/action.service.js +90 -7
- package/fesm/services/action.service.spec.js +310 -0
- package/fesm/services/iam-config.service.spec.js +106 -0
- package/fesm/services/iam-datasource.service.spec.js +113 -0
- package/fesm/services/permission-cache.service.js +101 -108
- package/fesm/services/permission-cache.service.spec.js +248 -0
- package/fesm/services/permission.service.js +213 -92
- package/fesm/services/permission.service.spec.js +963 -0
- package/fesm/services/role.service.js +48 -4
- package/fesm/services/role.service.spec.js +308 -0
- package/package.json +3 -3
- package/services/action.service.d.ts +7 -3
- package/services/permission-cache.service.d.ts +13 -19
- package/services/permission.service.d.ts +6 -3
- package/services/role.service.d.ts +7 -3
|
@@ -14,8 +14,13 @@ const _common = require("@nestjs/common");
|
|
|
14
14
|
const _typeorm = require("typeorm");
|
|
15
15
|
const _config = require("../config");
|
|
16
16
|
const _actionentity = require("../entities/action.entity");
|
|
17
|
+
const _permissionwithcompanyentity = require("../entities/permission-with-company.entity");
|
|
18
|
+
const _useriampermissionentity = require("../entities/user-iam-permission.entity");
|
|
19
|
+
const _iamentitytypeenum = require("../enums/iam-entity-type.enum");
|
|
20
|
+
const _iampermissiontypeenum = require("../enums/iam-permission-type.enum");
|
|
17
21
|
const _iamconfigservice = require("./iam-config.service");
|
|
18
22
|
const _iamdatasourceservice = require("./iam-datasource.service");
|
|
23
|
+
const _permissioncacheservice = require("./permission-cache.service");
|
|
19
24
|
const _permissionservice = require("./permission.service");
|
|
20
25
|
function _define_property(obj, key, value) {
|
|
21
26
|
if (key in obj) {
|
|
@@ -45,6 +50,81 @@ function _ts_param(paramIndex, decorator) {
|
|
|
45
50
|
};
|
|
46
51
|
}
|
|
47
52
|
let ActionService = class ActionService extends _classes.ApiService {
|
|
53
|
+
async beforeDeleteOperation(dto, user, queryRunner) {
|
|
54
|
+
const actionIds = Array.isArray(dto.id) ? dto.id : [
|
|
55
|
+
dto.id
|
|
56
|
+
];
|
|
57
|
+
if (actionIds.length === 0) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const enableCompanyFeature = this.iamConfigService.isCompanyFeatureEnabled();
|
|
61
|
+
const permissionEntity = enableCompanyFeature ? _permissionwithcompanyentity.UserIamPermissionWithCompany : _useriampermissionentity.UserIamPermission;
|
|
62
|
+
const permissionRepo = queryRunner.manager.getRepository(permissionEntity);
|
|
63
|
+
const [roleActionRows, userActionRows, companyActionRows] = await Promise.all([
|
|
64
|
+
permissionRepo.find({
|
|
65
|
+
where: {
|
|
66
|
+
permissionType: _iampermissiontypeenum.IamPermissionType.ROLE_ACTION,
|
|
67
|
+
sourceType: _iamentitytypeenum.IamEntityType.ROLE,
|
|
68
|
+
targetType: _iamentitytypeenum.IamEntityType.ACTION,
|
|
69
|
+
targetId: (0, _typeorm.In)(actionIds)
|
|
70
|
+
}
|
|
71
|
+
}),
|
|
72
|
+
permissionRepo.find({
|
|
73
|
+
where: {
|
|
74
|
+
permissionType: _iampermissiontypeenum.IamPermissionType.USER_ACTION,
|
|
75
|
+
targetType: _iamentitytypeenum.IamEntityType.ACTION,
|
|
76
|
+
targetId: (0, _typeorm.In)(actionIds)
|
|
77
|
+
}
|
|
78
|
+
}),
|
|
79
|
+
enableCompanyFeature ? permissionRepo.find({
|
|
80
|
+
where: {
|
|
81
|
+
permissionType: _iampermissiontypeenum.IamPermissionType.COMPANY_ACTION,
|
|
82
|
+
sourceType: _iamentitytypeenum.IamEntityType.COMPANY,
|
|
83
|
+
targetType: _iamentitytypeenum.IamEntityType.ACTION,
|
|
84
|
+
targetId: (0, _typeorm.In)(actionIds)
|
|
85
|
+
}
|
|
86
|
+
}) : Promise.resolve([])
|
|
87
|
+
]);
|
|
88
|
+
const affectedUserIds = new Set(userActionRows.map((row)=>row.userId).filter((id)=>!!id));
|
|
89
|
+
const affectedCompanyIds = [
|
|
90
|
+
...new Set(companyActionRows.map((row)=>row.sourceId).filter((id)=>!!id))
|
|
91
|
+
];
|
|
92
|
+
const affectedRoleIds = [
|
|
93
|
+
...new Set(roleActionRows.map((row)=>row.sourceId))
|
|
94
|
+
];
|
|
95
|
+
if (affectedRoleIds.length > 0) {
|
|
96
|
+
const userRoleRows = await permissionRepo.find({
|
|
97
|
+
where: {
|
|
98
|
+
permissionType: _iampermissiontypeenum.IamPermissionType.USER_ROLE,
|
|
99
|
+
targetType: _iamentitytypeenum.IamEntityType.ROLE,
|
|
100
|
+
targetId: (0, _typeorm.In)(affectedRoleIds)
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
userRoleRows.forEach((row)=>row.userId && affectedUserIds.add(row.userId));
|
|
104
|
+
}
|
|
105
|
+
await permissionRepo.delete({
|
|
106
|
+
permissionType: _iampermissiontypeenum.IamPermissionType.ROLE_ACTION,
|
|
107
|
+
targetType: _iamentitytypeenum.IamEntityType.ACTION,
|
|
108
|
+
targetId: (0, _typeorm.In)(actionIds)
|
|
109
|
+
});
|
|
110
|
+
await permissionRepo.delete({
|
|
111
|
+
permissionType: _iampermissiontypeenum.IamPermissionType.USER_ACTION,
|
|
112
|
+
targetType: _iamentitytypeenum.IamEntityType.ACTION,
|
|
113
|
+
targetId: (0, _typeorm.In)(actionIds)
|
|
114
|
+
});
|
|
115
|
+
await permissionRepo.delete({
|
|
116
|
+
permissionType: _iampermissiontypeenum.IamPermissionType.COMPANY_ACTION,
|
|
117
|
+
sourceType: _iamentitytypeenum.IamEntityType.COMPANY,
|
|
118
|
+
targetType: _iamentitytypeenum.IamEntityType.ACTION,
|
|
119
|
+
targetId: (0, _typeorm.In)(actionIds)
|
|
120
|
+
});
|
|
121
|
+
await Promise.all([
|
|
122
|
+
affectedUserIds.size > 0 ? this.permissionCacheService.invalidateUsers([
|
|
123
|
+
...affectedUserIds
|
|
124
|
+
]) : Promise.resolve(),
|
|
125
|
+
...affectedCompanyIds.map((companyId)=>this.permissionService.invalidateCompanyMembersCache(companyId))
|
|
126
|
+
]);
|
|
127
|
+
}
|
|
48
128
|
// Query Customization
|
|
49
129
|
async getSelectQuery(query, _user, select) {
|
|
50
130
|
if (!select?.length) {
|
|
@@ -108,12 +188,13 @@ let ActionService = class ActionService extends _classes.ApiService {
|
|
|
108
188
|
});
|
|
109
189
|
}
|
|
110
190
|
}
|
|
111
|
-
/** Get actions available for permission assignment (filtered by company whitelist) */ async getActionsForPermission(user) {
|
|
191
|
+
/** Get actions available for permission assignment (filtered by company whitelist) */ async getActionsForPermission(user, companyId) {
|
|
112
192
|
await this.ensureDataSourceRepository();
|
|
113
193
|
this.requireUser(user, 'getActionsForPermission');
|
|
114
194
|
let whereClause = {};
|
|
115
|
-
|
|
116
|
-
|
|
195
|
+
const targetCompanyId = companyId || user.companyId;
|
|
196
|
+
if (this.iamConfigService.isCompanyFeatureEnabled() && targetCompanyId) {
|
|
197
|
+
const companyActionIds = await this.permissionService.getCompanyActionIds(targetCompanyId);
|
|
117
198
|
if (companyActionIds.length === 0) {
|
|
118
199
|
return [];
|
|
119
200
|
}
|
|
@@ -176,9 +257,9 @@ let ActionService = class ActionService extends _classes.ApiService {
|
|
|
176
257
|
}
|
|
177
258
|
return rootNodes;
|
|
178
259
|
}
|
|
179
|
-
constructor(cacheManager, utilsService, iamConfigService, dataSourceProvider, permissionService){
|
|
180
|
-
super('action', cacheManager, utilsService, ActionService.name, true, 'iam', _actionentity.Action, dataSourceProvider), _define_property(this, "cacheManager", void 0), _define_property(this, "utilsService", void 0), _define_property(this, "iamConfigService", void 0), _define_property(this, "permissionService", void 0), // Custom Methods
|
|
181
|
-
_define_property(this, "actionSelectFields", void 0), this.cacheManager = cacheManager, this.utilsService = utilsService, this.iamConfigService = iamConfigService, this.permissionService = permissionService, this.actionSelectFields = [
|
|
260
|
+
constructor(cacheManager, utilsService, iamConfigService, dataSourceProvider, permissionService, permissionCacheService){
|
|
261
|
+
super('action', cacheManager, utilsService, ActionService.name, true, 'iam', _actionentity.Action, dataSourceProvider), _define_property(this, "cacheManager", void 0), _define_property(this, "utilsService", void 0), _define_property(this, "iamConfigService", void 0), _define_property(this, "permissionService", void 0), _define_property(this, "permissionCacheService", void 0), // Custom Methods
|
|
262
|
+
_define_property(this, "actionSelectFields", void 0), this.cacheManager = cacheManager, this.utilsService = utilsService, this.iamConfigService = iamConfigService, this.permissionService = permissionService, this.permissionCacheService = permissionCacheService, this.actionSelectFields = [
|
|
182
263
|
'id',
|
|
183
264
|
'code',
|
|
184
265
|
'name',
|
|
@@ -200,12 +281,14 @@ ActionService = _ts_decorate([
|
|
|
200
281
|
_ts_param(2, (0, _common.Inject)(_iamconfigservice.IAMConfigService)),
|
|
201
282
|
_ts_param(3, (0, _common.Inject)(_iamdatasourceservice.IAMDataSourceService)),
|
|
202
283
|
_ts_param(4, (0, _common.Inject)(_permissionservice.PermissionService)),
|
|
284
|
+
_ts_param(5, (0, _common.Inject)(_permissioncacheservice.PermissionCacheService)),
|
|
203
285
|
_ts_metadata("design:type", Function),
|
|
204
286
|
_ts_metadata("design:paramtypes", [
|
|
205
287
|
typeof _classes.HybridCache === "undefined" ? Object : _classes.HybridCache,
|
|
206
288
|
typeof _modules.UtilsService === "undefined" ? Object : _modules.UtilsService,
|
|
207
289
|
typeof _iamconfigservice.IAMConfigService === "undefined" ? Object : _iamconfigservice.IAMConfigService,
|
|
208
290
|
typeof _iamdatasourceservice.IAMDataSourceService === "undefined" ? Object : _iamdatasourceservice.IAMDataSourceService,
|
|
209
|
-
typeof _permissionservice.PermissionService === "undefined" ? Object : _permissionservice.PermissionService
|
|
291
|
+
typeof _permissionservice.PermissionService === "undefined" ? Object : _permissionservice.PermissionService,
|
|
292
|
+
typeof _permissioncacheservice.PermissionCacheService === "undefined" ? Object : _permissioncacheservice.PermissionCacheService
|
|
210
293
|
])
|
|
211
294
|
], ActionService);
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
const _testing = require("@nestjs/testing");
|
|
6
|
+
const _modules = require("@flusys/nestjs-shared/modules");
|
|
7
|
+
const _repositorymock = require("@test-utils/mocks/repository.mock");
|
|
8
|
+
const _loggedusermock = require("@test-utils/mocks/logged-user.mock");
|
|
9
|
+
const _enums = require("../enums");
|
|
10
|
+
const _actionservice = require("./action.service");
|
|
11
|
+
const _iamconfigservice = require("./iam-config.service");
|
|
12
|
+
const _iamdatasourceservice = require("./iam-datasource.service");
|
|
13
|
+
const _permissioncacheservice = require("./permission-cache.service");
|
|
14
|
+
const _permissionservice = require("./permission.service");
|
|
15
|
+
function buildAction(overrides = {}) {
|
|
16
|
+
return {
|
|
17
|
+
id: 'action-uuid-1',
|
|
18
|
+
readOnly: false,
|
|
19
|
+
name: 'View Products',
|
|
20
|
+
description: null,
|
|
21
|
+
code: 'products.read',
|
|
22
|
+
actionType: _enums.ActionType.BACKEND,
|
|
23
|
+
permissionLogic: null,
|
|
24
|
+
serial: 1,
|
|
25
|
+
isActive: true,
|
|
26
|
+
parentId: null,
|
|
27
|
+
createdAt: new Date(),
|
|
28
|
+
updatedAt: new Date(),
|
|
29
|
+
deletedAt: null,
|
|
30
|
+
createdById: null,
|
|
31
|
+
updatedById: null,
|
|
32
|
+
deletedById: null,
|
|
33
|
+
...overrides
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
describe('ActionService', ()=>{
|
|
37
|
+
let service;
|
|
38
|
+
let mockRepo;
|
|
39
|
+
let mockIamConfigService;
|
|
40
|
+
let mockPermissionService;
|
|
41
|
+
let mockPermissionCacheService;
|
|
42
|
+
beforeEach(async ()=>{
|
|
43
|
+
mockRepo = (0, _repositorymock.createMockRepository)();
|
|
44
|
+
const mockDataSourceProvider = (0, _repositorymock.createMockDataSourceProvider)(mockRepo);
|
|
45
|
+
mockIamConfigService = {
|
|
46
|
+
isCompanyFeatureEnabled: jest.fn().mockReturnValue(false)
|
|
47
|
+
};
|
|
48
|
+
mockPermissionService = {
|
|
49
|
+
getCompanyActionIds: jest.fn(),
|
|
50
|
+
invalidateCompanyMembersCache: jest.fn()
|
|
51
|
+
};
|
|
52
|
+
mockPermissionCacheService = {
|
|
53
|
+
invalidateUsers: jest.fn()
|
|
54
|
+
};
|
|
55
|
+
const module = await _testing.Test.createTestingModule({
|
|
56
|
+
providers: [
|
|
57
|
+
_actionservice.ActionService,
|
|
58
|
+
{
|
|
59
|
+
provide: 'CACHE_INSTANCE',
|
|
60
|
+
useValue: {}
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
provide: _modules.UtilsService,
|
|
64
|
+
useValue: {}
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
provide: _iamconfigservice.IAMConfigService,
|
|
68
|
+
useValue: mockIamConfigService
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
provide: _iamdatasourceservice.IAMDataSourceService,
|
|
72
|
+
useValue: mockDataSourceProvider
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
provide: _permissionservice.PermissionService,
|
|
76
|
+
useValue: mockPermissionService
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
provide: _permissioncacheservice.PermissionCacheService,
|
|
80
|
+
useValue: mockPermissionCacheService
|
|
81
|
+
}
|
|
82
|
+
]
|
|
83
|
+
}).compile();
|
|
84
|
+
service = await module.resolve(_actionservice.ActionService);
|
|
85
|
+
});
|
|
86
|
+
describe('getActionsForPermission', ()=>{
|
|
87
|
+
it('returns all actions when the company feature is disabled', async ()=>{
|
|
88
|
+
mockRepo.find.mockResolvedValue([
|
|
89
|
+
buildAction()
|
|
90
|
+
]);
|
|
91
|
+
const result = await service.getActionsForPermission((0, _loggedusermock.buildMockUser)());
|
|
92
|
+
expect(mockPermissionService.getCompanyActionIds).not.toHaveBeenCalled();
|
|
93
|
+
expect(result).toHaveLength(1);
|
|
94
|
+
expect(result[0].code).toBe('products.read');
|
|
95
|
+
});
|
|
96
|
+
it('scopes actions to the company whitelist when the company feature is enabled', async ()=>{
|
|
97
|
+
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
98
|
+
mockPermissionService.getCompanyActionIds.mockResolvedValue([
|
|
99
|
+
'action-uuid-1'
|
|
100
|
+
]);
|
|
101
|
+
mockRepo.find.mockResolvedValue([
|
|
102
|
+
buildAction()
|
|
103
|
+
]);
|
|
104
|
+
await service.getActionsForPermission((0, _loggedusermock.buildMockUser)(), 'company-uuid-1');
|
|
105
|
+
expect(mockPermissionService.getCompanyActionIds).toHaveBeenCalledWith('company-uuid-1');
|
|
106
|
+
expect(mockRepo.find).toHaveBeenCalledWith(expect.objectContaining({
|
|
107
|
+
where: {
|
|
108
|
+
id: expect.anything()
|
|
109
|
+
}
|
|
110
|
+
}));
|
|
111
|
+
});
|
|
112
|
+
it('short-circuits without querying when the company has no whitelisted actions', async ()=>{
|
|
113
|
+
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
114
|
+
mockPermissionService.getCompanyActionIds.mockResolvedValue([]);
|
|
115
|
+
const result = await service.getActionsForPermission((0, _loggedusermock.buildMockUser)(), 'company-uuid-1');
|
|
116
|
+
expect(result).toEqual([]);
|
|
117
|
+
expect(mockRepo.find).not.toHaveBeenCalled();
|
|
118
|
+
});
|
|
119
|
+
it('throws when no user is provided', async ()=>{
|
|
120
|
+
await expect(service.getActionsForPermission(null)).rejects.toThrow();
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
describe('getActionTree', ()=>{
|
|
124
|
+
it('nests child actions under their parent', async ()=>{
|
|
125
|
+
const parent = buildAction({
|
|
126
|
+
id: 'parent-1',
|
|
127
|
+
parentId: null,
|
|
128
|
+
serial: 1
|
|
129
|
+
});
|
|
130
|
+
const child = buildAction({
|
|
131
|
+
id: 'child-1',
|
|
132
|
+
parentId: 'parent-1',
|
|
133
|
+
serial: 2,
|
|
134
|
+
code: 'products.create'
|
|
135
|
+
});
|
|
136
|
+
mockRepo.createQueryBuilder().getMany.mockResolvedValue([
|
|
137
|
+
parent,
|
|
138
|
+
child
|
|
139
|
+
]);
|
|
140
|
+
const tree = await service.getActionTree((0, _loggedusermock.buildMockUser)());
|
|
141
|
+
expect(tree).toHaveLength(1);
|
|
142
|
+
expect(tree[0].id).toBe('parent-1');
|
|
143
|
+
expect(tree[0].children).toHaveLength(1);
|
|
144
|
+
expect(tree[0].children?.[0].id).toBe('child-1');
|
|
145
|
+
});
|
|
146
|
+
it('throws when no user is provided', async ()=>{
|
|
147
|
+
await expect(service.getActionTree(null)).rejects.toThrow();
|
|
148
|
+
});
|
|
149
|
+
it('treats an action with a missing/unknown parentId as a root node', async ()=>{
|
|
150
|
+
const orphan = buildAction({
|
|
151
|
+
id: 'orphan-1',
|
|
152
|
+
parentId: 'does-not-exist'
|
|
153
|
+
});
|
|
154
|
+
mockRepo.createQueryBuilder().getMany.mockResolvedValue([
|
|
155
|
+
orphan
|
|
156
|
+
]);
|
|
157
|
+
const tree = await service.getActionTree((0, _loggedusermock.buildMockUser)());
|
|
158
|
+
expect(tree).toHaveLength(1);
|
|
159
|
+
expect(tree[0].id).toBe('orphan-1');
|
|
160
|
+
});
|
|
161
|
+
it('excludes soft-deleted actions by default and applies search/isActive filters', async ()=>{
|
|
162
|
+
const query = mockRepo.createQueryBuilder();
|
|
163
|
+
query.getMany.mockResolvedValue([]);
|
|
164
|
+
await service.getActionTree((0, _loggedusermock.buildMockUser)(), 'view', true, false);
|
|
165
|
+
expect(query.andWhere).toHaveBeenCalledWith('action.deletedAt IS NULL');
|
|
166
|
+
expect(query.andWhere).toHaveBeenCalledWith('action.isActive = :isActive', {
|
|
167
|
+
isActive: true
|
|
168
|
+
});
|
|
169
|
+
expect(query.andWhere).toHaveBeenCalledWith('(action.name LIKE :search OR action.code LIKE :search)', {
|
|
170
|
+
search: '%view%'
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
it('includes soft-deleted actions when withDeleted is true', async ()=>{
|
|
174
|
+
const query = mockRepo.createQueryBuilder();
|
|
175
|
+
query.getMany.mockResolvedValue([]);
|
|
176
|
+
await service.getActionTree((0, _loggedusermock.buildMockUser)(), undefined, undefined, true);
|
|
177
|
+
expect(query.andWhere).not.toHaveBeenCalledWith('action.deletedAt IS NULL');
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
describe('getSelectQuery', ()=>{
|
|
181
|
+
it('selects the default field set when none is provided', async ()=>{
|
|
182
|
+
const query = mockRepo.createQueryBuilder();
|
|
183
|
+
await service.getSelectQuery(query, (0, _loggedusermock.buildMockUser)());
|
|
184
|
+
expect(query.select).toHaveBeenCalledWith(expect.arrayContaining([
|
|
185
|
+
'action.code',
|
|
186
|
+
'action.actionType',
|
|
187
|
+
'action.permissionLogic'
|
|
188
|
+
]));
|
|
189
|
+
});
|
|
190
|
+
it('uses the explicitly provided select list as-is', async ()=>{
|
|
191
|
+
const query = mockRepo.createQueryBuilder();
|
|
192
|
+
await service.getSelectQuery(query, (0, _loggedusermock.buildMockUser)(), [
|
|
193
|
+
'id',
|
|
194
|
+
'code'
|
|
195
|
+
]);
|
|
196
|
+
expect(query.select).toHaveBeenCalledWith([
|
|
197
|
+
'action.id',
|
|
198
|
+
'action.code'
|
|
199
|
+
]);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
describe('getGlobalSearchQuery', ()=>{
|
|
203
|
+
it('filters by name, code, or description', async ()=>{
|
|
204
|
+
const query = mockRepo.createQueryBuilder();
|
|
205
|
+
await service.getGlobalSearchQuery(query, 'view');
|
|
206
|
+
expect(query.andWhere).toHaveBeenCalledWith('(action.name LIKE :search OR action.code LIKE :search OR action.description LIKE :search)', {
|
|
207
|
+
search: '%view%'
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
describe('convertEntityToResponseDto', ()=>{
|
|
212
|
+
it('maps every entity field onto the response shape', ()=>{
|
|
213
|
+
const entity = buildAction({
|
|
214
|
+
code: 'user.view'
|
|
215
|
+
});
|
|
216
|
+
const dto = service.convertEntityToResponseDto(entity, false);
|
|
217
|
+
expect(dto).toEqual(expect.objectContaining({
|
|
218
|
+
id: entity.id,
|
|
219
|
+
code: 'user.view',
|
|
220
|
+
actionType: _enums.ActionType.BACKEND
|
|
221
|
+
}));
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
describe('beforeDeleteOperation', ()=>{
|
|
225
|
+
function buildQueryRunner(permissionRepo) {
|
|
226
|
+
return {
|
|
227
|
+
manager: {
|
|
228
|
+
getRepository: jest.fn().mockReturnValue(permissionRepo)
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
it('does nothing when the delete DTO has no ids', async ()=>{
|
|
233
|
+
const permissionRepo = (0, _repositorymock.createMockRepository)();
|
|
234
|
+
const queryRunner = buildQueryRunner(permissionRepo);
|
|
235
|
+
await service.beforeDeleteOperation({
|
|
236
|
+
id: [],
|
|
237
|
+
type: 'delete'
|
|
238
|
+
}, (0, _loggedusermock.buildMockUser)(), queryRunner);
|
|
239
|
+
expect(permissionRepo.find).not.toHaveBeenCalled();
|
|
240
|
+
});
|
|
241
|
+
it('cascades role/user/company action-permission cleanup and invalidates affected caches', async ()=>{
|
|
242
|
+
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(false);
|
|
243
|
+
const permissionRepo = (0, _repositorymock.createMockRepository)();
|
|
244
|
+
// Company feature is disabled, so companyActionRows resolves via Promise.resolve([]) and
|
|
245
|
+
// never calls permissionRepo.find() — only 3 real find() calls happen: role-action,
|
|
246
|
+
// user-action, then the role -> user cascade lookup.
|
|
247
|
+
permissionRepo.find.mockResolvedValueOnce([
|
|
248
|
+
{
|
|
249
|
+
sourceId: 'role-1'
|
|
250
|
+
}
|
|
251
|
+
]) // role-action rows referencing the deleted action
|
|
252
|
+
.mockResolvedValueOnce([
|
|
253
|
+
{
|
|
254
|
+
userId: 'user-1'
|
|
255
|
+
}
|
|
256
|
+
]) // direct user-action rows
|
|
257
|
+
.mockResolvedValueOnce([
|
|
258
|
+
{
|
|
259
|
+
userId: 'user-2'
|
|
260
|
+
}
|
|
261
|
+
]); // users holding role-1 (cascaded lookup)
|
|
262
|
+
const queryRunner = buildQueryRunner(permissionRepo);
|
|
263
|
+
await service.beforeDeleteOperation({
|
|
264
|
+
id: 'action-uuid-1',
|
|
265
|
+
type: 'delete'
|
|
266
|
+
}, (0, _loggedusermock.buildMockUser)(), queryRunner);
|
|
267
|
+
expect(permissionRepo.delete).toHaveBeenCalledWith(expect.objectContaining({
|
|
268
|
+
permissionType: 'role_action'
|
|
269
|
+
}));
|
|
270
|
+
expect(permissionRepo.delete).toHaveBeenCalledWith(expect.objectContaining({
|
|
271
|
+
permissionType: 'user_action'
|
|
272
|
+
}));
|
|
273
|
+
expect(permissionRepo.delete).toHaveBeenCalledWith(expect.objectContaining({
|
|
274
|
+
permissionType: 'company_action'
|
|
275
|
+
}));
|
|
276
|
+
expect(mockPermissionCacheService.invalidateUsers).toHaveBeenCalledWith(expect.arrayContaining([
|
|
277
|
+
'user-1',
|
|
278
|
+
'user-2'
|
|
279
|
+
]));
|
|
280
|
+
expect(mockPermissionService.invalidateCompanyMembersCache).not.toHaveBeenCalled();
|
|
281
|
+
});
|
|
282
|
+
it('also queries and invalidates company whitelist entries when the company feature is enabled', async ()=>{
|
|
283
|
+
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
284
|
+
const permissionRepo = (0, _repositorymock.createMockRepository)();
|
|
285
|
+
permissionRepo.find.mockResolvedValueOnce([]) // role-action rows
|
|
286
|
+
.mockResolvedValueOnce([]) // direct user-action rows
|
|
287
|
+
.mockResolvedValueOnce([
|
|
288
|
+
{
|
|
289
|
+
sourceId: 'company-1'
|
|
290
|
+
}
|
|
291
|
+
]); // company-action rows
|
|
292
|
+
const queryRunner = buildQueryRunner(permissionRepo);
|
|
293
|
+
await service.beforeDeleteOperation({
|
|
294
|
+
id: [
|
|
295
|
+
'action-uuid-1'
|
|
296
|
+
],
|
|
297
|
+
type: 'delete'
|
|
298
|
+
}, (0, _loggedusermock.buildMockUser)(), queryRunner);
|
|
299
|
+
expect(mockPermissionService.invalidateCompanyMembersCache).toHaveBeenCalledWith('company-1');
|
|
300
|
+
});
|
|
301
|
+
it('does not invalidate any cache when nothing is affected', async ()=>{
|
|
302
|
+
const permissionRepo = (0, _repositorymock.createMockRepository)();
|
|
303
|
+
permissionRepo.find.mockResolvedValue([]);
|
|
304
|
+
const queryRunner = buildQueryRunner(permissionRepo);
|
|
305
|
+
await service.beforeDeleteOperation({
|
|
306
|
+
id: [
|
|
307
|
+
'action-uuid-1'
|
|
308
|
+
],
|
|
309
|
+
type: 'delete'
|
|
310
|
+
}, (0, _loggedusermock.buildMockUser)(), queryRunner);
|
|
311
|
+
expect(mockPermissionCacheService.invalidateUsers).not.toHaveBeenCalled();
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
const _permissiontypeenum = require("../enums/permission-type.enum");
|
|
6
|
+
const _iamconfigservice = require("./iam-config.service");
|
|
7
|
+
describe('IAMConfigService', ()=>{
|
|
8
|
+
describe('with no injected options (defaults)', ()=>{
|
|
9
|
+
let service;
|
|
10
|
+
beforeEach(()=>{
|
|
11
|
+
service = new _iamconfigservice.IAMConfigService();
|
|
12
|
+
});
|
|
13
|
+
it('defaults to single database mode', ()=>{
|
|
14
|
+
expect(service.getDatabaseMode()).toBe('single');
|
|
15
|
+
expect(service.isMultiTenant()).toBe(false);
|
|
16
|
+
});
|
|
17
|
+
it('defaults company feature to disabled', ()=>{
|
|
18
|
+
expect(service.isCompanyFeatureEnabled()).toBe(false);
|
|
19
|
+
});
|
|
20
|
+
it('defaults permission mode to FULL', ()=>{
|
|
21
|
+
expect(service.getPermissionMode()).toBe(_permissiontypeenum.IAMPermissionMode.FULL);
|
|
22
|
+
expect(service.isRbacEnabled()).toBe(true);
|
|
23
|
+
expect(service.isDirectPermissionEnabled()).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
it('returns the fallback options object from getOptions', ()=>{
|
|
26
|
+
expect(service.getOptions()).toEqual({
|
|
27
|
+
global: false,
|
|
28
|
+
includeController: false
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
describe('with injected options', ()=>{
|
|
33
|
+
function buildService(overrides) {
|
|
34
|
+
const options = {
|
|
35
|
+
global: false,
|
|
36
|
+
includeController: false,
|
|
37
|
+
bootstrapAppConfig: {
|
|
38
|
+
databaseMode: 'single',
|
|
39
|
+
enableCompanyFeature: false,
|
|
40
|
+
...overrides
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
return new _iamconfigservice.IAMConfigService(options);
|
|
44
|
+
}
|
|
45
|
+
it('reports multi-tenant mode when configured', ()=>{
|
|
46
|
+
const service = buildService({
|
|
47
|
+
databaseMode: 'multi-tenant'
|
|
48
|
+
});
|
|
49
|
+
expect(service.getDatabaseMode()).toBe('multi-tenant');
|
|
50
|
+
expect(service.isMultiTenant()).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
it('reports company feature enabled when configured', ()=>{
|
|
53
|
+
const service = buildService({
|
|
54
|
+
enableCompanyFeature: true
|
|
55
|
+
});
|
|
56
|
+
expect(service.isCompanyFeatureEnabled()).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
describe('permission mode: RBAC', ()=>{
|
|
59
|
+
let service;
|
|
60
|
+
beforeEach(()=>{
|
|
61
|
+
service = buildService({
|
|
62
|
+
permissionMode: 'RBAC'
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
it('enables RBAC but not direct permissions', ()=>{
|
|
66
|
+
expect(service.getPermissionMode()).toBe(_permissiontypeenum.IAMPermissionMode.RBAC);
|
|
67
|
+
expect(service.isRbacEnabled()).toBe(true);
|
|
68
|
+
expect(service.isDirectPermissionEnabled()).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
describe('permission mode: DIRECT', ()=>{
|
|
72
|
+
let service;
|
|
73
|
+
beforeEach(()=>{
|
|
74
|
+
service = buildService({
|
|
75
|
+
permissionMode: 'DIRECT'
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
it('enables direct permissions but not RBAC', ()=>{
|
|
79
|
+
expect(service.getPermissionMode()).toBe(_permissiontypeenum.IAMPermissionMode.DIRECT);
|
|
80
|
+
expect(service.isRbacEnabled()).toBe(false);
|
|
81
|
+
expect(service.isDirectPermissionEnabled()).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
describe('permission mode: FULL', ()=>{
|
|
85
|
+
let service;
|
|
86
|
+
beforeEach(()=>{
|
|
87
|
+
service = buildService({
|
|
88
|
+
permissionMode: 'FULL'
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
it('enables both RBAC and direct permissions', ()=>{
|
|
92
|
+
expect(service.getPermissionMode()).toBe(_permissiontypeenum.IAMPermissionMode.FULL);
|
|
93
|
+
expect(service.isRbacEnabled()).toBe(true);
|
|
94
|
+
expect(service.isDirectPermissionEnabled()).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
it('returns the exact injected options object from getOptions', ()=>{
|
|
98
|
+
const options = {
|
|
99
|
+
global: true,
|
|
100
|
+
includeController: true,
|
|
101
|
+
bootstrapAppConfig: {
|
|
102
|
+
databaseMode: 'single',
|
|
103
|
+
enableCompanyFeature: true
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
const service = new _iamconfigservice.IAMConfigService(options);
|
|
107
|
+
expect(service.getOptions()).toBe(options);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
});
|