@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
|
@@ -12,10 +12,16 @@ const _classes = require("@flusys/nestjs-shared/classes");
|
|
|
12
12
|
const _modules = require("@flusys/nestjs-shared/modules");
|
|
13
13
|
const _utils = require("@flusys/nestjs-shared/utils");
|
|
14
14
|
const _common = require("@nestjs/common");
|
|
15
|
+
const _typeorm = require("typeorm");
|
|
16
|
+
const _permissionwithcompanyentity = require("../entities/permission-with-company.entity");
|
|
15
17
|
const _rolewithcompanyentity = require("../entities/role-with-company.entity");
|
|
16
18
|
const _roleentity = require("../entities/role.entity");
|
|
19
|
+
const _useriampermissionentity = require("../entities/user-iam-permission.entity");
|
|
20
|
+
const _iamentitytypeenum = require("../enums/iam-entity-type.enum");
|
|
21
|
+
const _iampermissiontypeenum = require("../enums/iam-permission-type.enum");
|
|
17
22
|
const _iamconfigservice = require("./iam-config.service");
|
|
18
23
|
const _iamdatasourceservice = require("./iam-datasource.service");
|
|
24
|
+
const _permissioncacheservice = require("./permission-cache.service");
|
|
19
25
|
function _define_property(obj, key, value) {
|
|
20
26
|
if (key in obj) {
|
|
21
27
|
Object.defineProperty(obj, key, {
|
|
@@ -44,6 +50,42 @@ function _ts_param(paramIndex, decorator) {
|
|
|
44
50
|
};
|
|
45
51
|
}
|
|
46
52
|
let RoleService = class RoleService extends _classes.ApiService {
|
|
53
|
+
async beforeDeleteOperation(dto, user, queryRunner) {
|
|
54
|
+
await super.beforeDeleteOperation(dto, user, queryRunner);
|
|
55
|
+
const roleIds = Array.isArray(dto.id) ? dto.id : [
|
|
56
|
+
dto.id
|
|
57
|
+
];
|
|
58
|
+
if (roleIds.length === 0) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const enableCompanyFeature = this.iamConfigService.isCompanyFeatureEnabled();
|
|
62
|
+
const permissionEntity = enableCompanyFeature ? _permissionwithcompanyentity.UserIamPermissionWithCompany : _useriampermissionentity.UserIamPermission;
|
|
63
|
+
const permissionRepo = queryRunner.manager.getRepository(permissionEntity);
|
|
64
|
+
const userRoleRows = await permissionRepo.find({
|
|
65
|
+
where: {
|
|
66
|
+
permissionType: _iampermissiontypeenum.IamPermissionType.USER_ROLE,
|
|
67
|
+
sourceType: _iamentitytypeenum.IamEntityType.USER,
|
|
68
|
+
targetType: _iamentitytypeenum.IamEntityType.ROLE,
|
|
69
|
+
targetId: (0, _typeorm.In)(roleIds)
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
const affectedUserIds = [
|
|
73
|
+
...new Set(userRoleRows.map((row)=>row.userId).filter((id)=>!!id))
|
|
74
|
+
];
|
|
75
|
+
await permissionRepo.delete({
|
|
76
|
+
permissionType: _iampermissiontypeenum.IamPermissionType.ROLE_ACTION,
|
|
77
|
+
sourceType: _iamentitytypeenum.IamEntityType.ROLE,
|
|
78
|
+
sourceId: (0, _typeorm.In)(roleIds)
|
|
79
|
+
});
|
|
80
|
+
await permissionRepo.delete({
|
|
81
|
+
permissionType: _iampermissiontypeenum.IamPermissionType.USER_ROLE,
|
|
82
|
+
targetType: _iamentitytypeenum.IamEntityType.ROLE,
|
|
83
|
+
targetId: (0, _typeorm.In)(roleIds)
|
|
84
|
+
});
|
|
85
|
+
if (affectedUserIds.length > 0) {
|
|
86
|
+
await this.permissionCacheService.invalidateUsers(affectedUserIds);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
47
89
|
resolveEntity() {
|
|
48
90
|
return this.iamConfigService.isCompanyFeatureEnabled() ? _rolewithcompanyentity.RoleWithCompany : _roleentity.Role;
|
|
49
91
|
}
|
|
@@ -117,8 +159,8 @@ let RoleService = class RoleService extends _classes.ApiService {
|
|
|
117
159
|
deletedById: entity.deletedById
|
|
118
160
|
};
|
|
119
161
|
}
|
|
120
|
-
constructor(cacheManager, utilsService, iamConfigService, dataSourceProvider){
|
|
121
|
-
super('role', cacheManager, utilsService, RoleService.name, true, 'iam', undefined, dataSourceProvider), _define_property(this, "cacheManager", void 0), _define_property(this, "utilsService", void 0), _define_property(this, "iamConfigService", void 0), this.cacheManager = cacheManager, this.utilsService = utilsService, this.iamConfigService = iamConfigService;
|
|
162
|
+
constructor(cacheManager, utilsService, iamConfigService, dataSourceProvider, permissionCacheService){
|
|
163
|
+
super('role', cacheManager, utilsService, RoleService.name, true, 'iam', undefined, dataSourceProvider), _define_property(this, "cacheManager", void 0), _define_property(this, "utilsService", void 0), _define_property(this, "iamConfigService", void 0), _define_property(this, "permissionCacheService", void 0), this.cacheManager = cacheManager, this.utilsService = utilsService, this.iamConfigService = iamConfigService, this.permissionCacheService = permissionCacheService;
|
|
122
164
|
}
|
|
123
165
|
};
|
|
124
166
|
RoleService = _ts_decorate([
|
|
@@ -129,11 +171,13 @@ RoleService = _ts_decorate([
|
|
|
129
171
|
_ts_param(1, (0, _common.Inject)(_modules.UtilsService)),
|
|
130
172
|
_ts_param(2, (0, _common.Inject)(_iamconfigservice.IAMConfigService)),
|
|
131
173
|
_ts_param(3, (0, _common.Inject)(_iamdatasourceservice.IAMDataSourceService)),
|
|
174
|
+
_ts_param(4, (0, _common.Inject)(_permissioncacheservice.PermissionCacheService)),
|
|
132
175
|
_ts_metadata("design:type", Function),
|
|
133
176
|
_ts_metadata("design:paramtypes", [
|
|
134
177
|
typeof _classes.HybridCache === "undefined" ? Object : _classes.HybridCache,
|
|
135
178
|
typeof _modules.UtilsService === "undefined" ? Object : _modules.UtilsService,
|
|
136
179
|
typeof _iamconfigservice.IAMConfigService === "undefined" ? Object : _iamconfigservice.IAMConfigService,
|
|
137
|
-
typeof _iamdatasourceservice.IAMDataSourceService === "undefined" ? Object : _iamdatasourceservice.IAMDataSourceService
|
|
180
|
+
typeof _iamdatasourceservice.IAMDataSourceService === "undefined" ? Object : _iamdatasourceservice.IAMDataSourceService,
|
|
181
|
+
typeof _permissioncacheservice.PermissionCacheService === "undefined" ? Object : _permissioncacheservice.PermissionCacheService
|
|
138
182
|
])
|
|
139
183
|
], RoleService);
|
|
@@ -0,0 +1,312 @@
|
|
|
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 _roleentity = require("../entities/role.entity");
|
|
10
|
+
const _rolewithcompanyentity = require("../entities/role-with-company.entity");
|
|
11
|
+
const _iamconfigservice = require("./iam-config.service");
|
|
12
|
+
const _iamdatasourceservice = require("./iam-datasource.service");
|
|
13
|
+
const _permissioncacheservice = require("./permission-cache.service");
|
|
14
|
+
const _roleservice = require("./role.service");
|
|
15
|
+
function buildRole(overrides = {}) {
|
|
16
|
+
return {
|
|
17
|
+
id: 'role-uuid-1',
|
|
18
|
+
readOnly: false,
|
|
19
|
+
name: 'Manager',
|
|
20
|
+
description: null,
|
|
21
|
+
isActive: true,
|
|
22
|
+
serial: 1,
|
|
23
|
+
createdAt: new Date(),
|
|
24
|
+
updatedAt: new Date(),
|
|
25
|
+
deletedAt: null,
|
|
26
|
+
createdById: null,
|
|
27
|
+
updatedById: null,
|
|
28
|
+
deletedById: null,
|
|
29
|
+
...overrides
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
describe('RoleService', ()=>{
|
|
33
|
+
let service;
|
|
34
|
+
let mockRepo;
|
|
35
|
+
let mockIamConfigService;
|
|
36
|
+
let mockPermissionCacheService;
|
|
37
|
+
beforeEach(async ()=>{
|
|
38
|
+
mockRepo = (0, _repositorymock.createMockRepository)();
|
|
39
|
+
const mockDataSourceProvider = (0, _repositorymock.createMockDataSourceProvider)(mockRepo);
|
|
40
|
+
mockIamConfigService = {
|
|
41
|
+
isCompanyFeatureEnabled: jest.fn().mockReturnValue(false)
|
|
42
|
+
};
|
|
43
|
+
mockPermissionCacheService = {
|
|
44
|
+
invalidateUsers: jest.fn()
|
|
45
|
+
};
|
|
46
|
+
const module = await _testing.Test.createTestingModule({
|
|
47
|
+
providers: [
|
|
48
|
+
_roleservice.RoleService,
|
|
49
|
+
{
|
|
50
|
+
provide: 'CACHE_INSTANCE',
|
|
51
|
+
useValue: {}
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
provide: _modules.UtilsService,
|
|
55
|
+
useValue: {}
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
provide: _iamconfigservice.IAMConfigService,
|
|
59
|
+
useValue: mockIamConfigService
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
provide: _iamdatasourceservice.IAMDataSourceService,
|
|
63
|
+
useValue: mockDataSourceProvider
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
provide: _permissioncacheservice.PermissionCacheService,
|
|
67
|
+
useValue: mockPermissionCacheService
|
|
68
|
+
}
|
|
69
|
+
]
|
|
70
|
+
}).compile();
|
|
71
|
+
service = await module.resolve(_roleservice.RoleService);
|
|
72
|
+
});
|
|
73
|
+
describe('resolveEntity', ()=>{
|
|
74
|
+
it('resolves to Role when the company feature is disabled', ()=>{
|
|
75
|
+
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(false);
|
|
76
|
+
expect(service.resolveEntity()).toBe(_roleentity.Role);
|
|
77
|
+
});
|
|
78
|
+
it('resolves to RoleWithCompany when the company feature is enabled', ()=>{
|
|
79
|
+
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
80
|
+
expect(service.resolveEntity()).toBe(_rolewithcompanyentity.RoleWithCompany);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
describe('convertSingleDtoToEntity', ()=>{
|
|
84
|
+
it('creates a plain entity without companyId when the company feature is disabled', async ()=>{
|
|
85
|
+
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(false);
|
|
86
|
+
await service.ensureDataSourceRepository();
|
|
87
|
+
mockRepo.create.mockReturnValue({});
|
|
88
|
+
const entity = await service.convertSingleDtoToEntity({
|
|
89
|
+
name: 'Manager'
|
|
90
|
+
}, (0, _loggedusermock.buildMockUser)());
|
|
91
|
+
expect(entity.companyId).toBeUndefined();
|
|
92
|
+
});
|
|
93
|
+
it('stamps companyId from the DTO on create when the company feature is enabled', async ()=>{
|
|
94
|
+
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
95
|
+
await service.ensureDataSourceRepository();
|
|
96
|
+
mockRepo.create.mockReturnValue({});
|
|
97
|
+
const entity = await service.convertSingleDtoToEntity({
|
|
98
|
+
name: 'Manager',
|
|
99
|
+
companyId: 'company-explicit'
|
|
100
|
+
}, (0, _loggedusermock.buildMockUser)({
|
|
101
|
+
companyId: 'company-from-user'
|
|
102
|
+
}));
|
|
103
|
+
expect(entity.companyId).toBe('company-explicit');
|
|
104
|
+
});
|
|
105
|
+
it('falls back to the user companyId on create when the DTO omits it', async ()=>{
|
|
106
|
+
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
107
|
+
await service.ensureDataSourceRepository();
|
|
108
|
+
mockRepo.create.mockReturnValue({});
|
|
109
|
+
const entity = await service.convertSingleDtoToEntity({
|
|
110
|
+
name: 'Manager'
|
|
111
|
+
}, (0, _loggedusermock.buildMockUser)({
|
|
112
|
+
companyId: 'company-from-user'
|
|
113
|
+
}));
|
|
114
|
+
expect(entity.companyId).toBe('company-from-user');
|
|
115
|
+
});
|
|
116
|
+
it('does not override companyId on update when the DTO omits it', async ()=>{
|
|
117
|
+
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
118
|
+
await service.ensureDataSourceRepository();
|
|
119
|
+
const existing = buildRole({
|
|
120
|
+
id: 'role-uuid-1',
|
|
121
|
+
companyId: 'existing-company'
|
|
122
|
+
});
|
|
123
|
+
mockRepo.findOne.mockResolvedValue(existing);
|
|
124
|
+
const entity = await service.convertSingleDtoToEntity({
|
|
125
|
+
id: 'role-uuid-1',
|
|
126
|
+
name: 'Manager Updated'
|
|
127
|
+
}, (0, _loggedusermock.buildMockUser)({
|
|
128
|
+
companyId: 'company-from-user'
|
|
129
|
+
}));
|
|
130
|
+
expect(entity.companyId).toBe('existing-company');
|
|
131
|
+
});
|
|
132
|
+
it('overrides companyId on update when explicitly provided in the DTO', async ()=>{
|
|
133
|
+
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
134
|
+
await service.ensureDataSourceRepository();
|
|
135
|
+
const existing = buildRole({
|
|
136
|
+
id: 'role-uuid-1',
|
|
137
|
+
companyId: 'existing-company'
|
|
138
|
+
});
|
|
139
|
+
mockRepo.findOne.mockResolvedValue(existing);
|
|
140
|
+
const entity = await service.convertSingleDtoToEntity({
|
|
141
|
+
id: 'role-uuid-1',
|
|
142
|
+
name: 'Manager Updated',
|
|
143
|
+
companyId: 'new-company'
|
|
144
|
+
}, (0, _loggedusermock.buildMockUser)());
|
|
145
|
+
expect(entity.companyId).toBe('new-company');
|
|
146
|
+
});
|
|
147
|
+
it('throws NotFoundException when updating a role that does not exist', async ()=>{
|
|
148
|
+
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(false);
|
|
149
|
+
await service.ensureDataSourceRepository();
|
|
150
|
+
mockRepo.findOne.mockResolvedValue(null);
|
|
151
|
+
await expect(service.convertSingleDtoToEntity({
|
|
152
|
+
id: 'missing-role'
|
|
153
|
+
}, (0, _loggedusermock.buildMockUser)())).rejects.toThrow();
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
describe('getSelectQuery', ()=>{
|
|
157
|
+
it('selects the default field set without companyId when the company feature is disabled', async ()=>{
|
|
158
|
+
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(false);
|
|
159
|
+
const query = mockRepo.createQueryBuilder();
|
|
160
|
+
await service.getSelectQuery(query, (0, _loggedusermock.buildMockUser)());
|
|
161
|
+
expect(query.select).toHaveBeenCalledWith(expect.not.arrayContaining([
|
|
162
|
+
'role.companyId'
|
|
163
|
+
]));
|
|
164
|
+
});
|
|
165
|
+
it('includes companyId in the default field set when the company feature is enabled', async ()=>{
|
|
166
|
+
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
167
|
+
const query = mockRepo.createQueryBuilder();
|
|
168
|
+
await service.getSelectQuery(query, (0, _loggedusermock.buildMockUser)());
|
|
169
|
+
expect(query.select).toHaveBeenCalledWith(expect.arrayContaining([
|
|
170
|
+
'role.companyId'
|
|
171
|
+
]));
|
|
172
|
+
});
|
|
173
|
+
it('uses the explicitly provided select list as-is', async ()=>{
|
|
174
|
+
const query = mockRepo.createQueryBuilder();
|
|
175
|
+
await service.getSelectQuery(query, (0, _loggedusermock.buildMockUser)(), [
|
|
176
|
+
'id',
|
|
177
|
+
'name'
|
|
178
|
+
]);
|
|
179
|
+
expect(query.select).toHaveBeenCalledWith([
|
|
180
|
+
'role.id',
|
|
181
|
+
'role.name'
|
|
182
|
+
]);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
describe('getGlobalSearchQuery', ()=>{
|
|
186
|
+
it('filters by name or description', async ()=>{
|
|
187
|
+
const query = mockRepo.createQueryBuilder();
|
|
188
|
+
await service.getGlobalSearchQuery(query, 'manager');
|
|
189
|
+
expect(query.andWhere).toHaveBeenCalledWith('(role.name LIKE :search OR role.description LIKE :search)', {
|
|
190
|
+
search: '%manager%'
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
describe('getExtraManipulateQuery', ()=>{
|
|
195
|
+
it('applies a company filter clause when the company feature is enabled and the user has a company', async ()=>{
|
|
196
|
+
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
197
|
+
const query = mockRepo.createQueryBuilder();
|
|
198
|
+
await service.getExtraManipulateQuery(query, {}, (0, _loggedusermock.buildMockUser)({
|
|
199
|
+
companyId: 'company-1'
|
|
200
|
+
}));
|
|
201
|
+
expect(query.andWhere).toHaveBeenCalledWith('role.companyId = :companyId', {
|
|
202
|
+
companyId: 'company-1'
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
it('does not apply a company filter when the company feature is disabled', async ()=>{
|
|
206
|
+
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(false);
|
|
207
|
+
const query = mockRepo.createQueryBuilder();
|
|
208
|
+
await service.getExtraManipulateQuery(query, {}, (0, _loggedusermock.buildMockUser)({
|
|
209
|
+
companyId: 'company-1'
|
|
210
|
+
}));
|
|
211
|
+
expect(query.andWhere).not.toHaveBeenCalled();
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
describe('convertEntityToResponseDto', ()=>{
|
|
215
|
+
it('defaults companyId to null when the entity has no companyId field', ()=>{
|
|
216
|
+
const entity = buildRole();
|
|
217
|
+
const dto = service.convertEntityToResponseDto(entity, false);
|
|
218
|
+
expect(dto.companyId).toBeNull();
|
|
219
|
+
});
|
|
220
|
+
it('surfaces companyId when present on the entity', ()=>{
|
|
221
|
+
const entity = buildRole({
|
|
222
|
+
companyId: 'company-1'
|
|
223
|
+
});
|
|
224
|
+
const dto = service.convertEntityToResponseDto(entity, false);
|
|
225
|
+
expect(dto.companyId).toBe('company-1');
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
describe('beforeDeleteOperation', ()=>{
|
|
229
|
+
function buildQueryRunner(permissionRepo) {
|
|
230
|
+
return {
|
|
231
|
+
manager: {
|
|
232
|
+
getRepository: jest.fn().mockReturnValue(permissionRepo)
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
it('does nothing when the delete DTO has no ids', async ()=>{
|
|
237
|
+
const permissionRepo = (0, _repositorymock.createMockRepository)();
|
|
238
|
+
const queryRunner = buildQueryRunner(permissionRepo);
|
|
239
|
+
await service.beforeDeleteOperation({
|
|
240
|
+
id: [],
|
|
241
|
+
type: 'delete'
|
|
242
|
+
}, (0, _loggedusermock.buildMockUser)(), queryRunner);
|
|
243
|
+
expect(permissionRepo.find).not.toHaveBeenCalled();
|
|
244
|
+
});
|
|
245
|
+
it('cascades ROLE_ACTION and USER_ROLE cleanup and invalidates affected users cache', async ()=>{
|
|
246
|
+
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(false);
|
|
247
|
+
const permissionRepo = (0, _repositorymock.createMockRepository)();
|
|
248
|
+
permissionRepo.find.mockResolvedValue([
|
|
249
|
+
{
|
|
250
|
+
userId: 'user-1'
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
userId: 'user-2'
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
userId: null
|
|
257
|
+
}
|
|
258
|
+
]);
|
|
259
|
+
const queryRunner = buildQueryRunner(permissionRepo);
|
|
260
|
+
await service.beforeDeleteOperation({
|
|
261
|
+
id: 'role-uuid-1',
|
|
262
|
+
type: 'delete'
|
|
263
|
+
}, (0, _loggedusermock.buildMockUser)(), queryRunner);
|
|
264
|
+
expect(permissionRepo.delete).toHaveBeenCalledWith(expect.objectContaining({
|
|
265
|
+
permissionType: 'role_action',
|
|
266
|
+
sourceType: 'role'
|
|
267
|
+
}));
|
|
268
|
+
expect(permissionRepo.delete).toHaveBeenCalledWith(expect.objectContaining({
|
|
269
|
+
permissionType: 'user_role',
|
|
270
|
+
targetType: 'role'
|
|
271
|
+
}));
|
|
272
|
+
expect(mockPermissionCacheService.invalidateUsers).toHaveBeenCalledWith([
|
|
273
|
+
'user-1',
|
|
274
|
+
'user-2'
|
|
275
|
+
]);
|
|
276
|
+
});
|
|
277
|
+
it('does not invalidate any cache when no users are affected', async ()=>{
|
|
278
|
+
const permissionRepo = (0, _repositorymock.createMockRepository)();
|
|
279
|
+
permissionRepo.find.mockResolvedValue([]);
|
|
280
|
+
const queryRunner = buildQueryRunner(permissionRepo);
|
|
281
|
+
await service.beforeDeleteOperation({
|
|
282
|
+
id: [
|
|
283
|
+
'role-uuid-1'
|
|
284
|
+
],
|
|
285
|
+
type: 'delete'
|
|
286
|
+
}, (0, _loggedusermock.buildMockUser)(), queryRunner);
|
|
287
|
+
expect(mockPermissionCacheService.invalidateUsers).not.toHaveBeenCalled();
|
|
288
|
+
});
|
|
289
|
+
it('supports array ids and dedupes affected user ids', async ()=>{
|
|
290
|
+
const permissionRepo = (0, _repositorymock.createMockRepository)();
|
|
291
|
+
permissionRepo.find.mockResolvedValue([
|
|
292
|
+
{
|
|
293
|
+
userId: 'user-1'
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
userId: 'user-1'
|
|
297
|
+
}
|
|
298
|
+
]);
|
|
299
|
+
const queryRunner = buildQueryRunner(permissionRepo);
|
|
300
|
+
await service.beforeDeleteOperation({
|
|
301
|
+
id: [
|
|
302
|
+
'role-uuid-1',
|
|
303
|
+
'role-uuid-2'
|
|
304
|
+
],
|
|
305
|
+
type: 'delete'
|
|
306
|
+
}, (0, _loggedusermock.buildMockUser)(), queryRunner);
|
|
307
|
+
expect(mockPermissionCacheService.invalidateUsers).toHaveBeenCalledWith([
|
|
308
|
+
'user-1'
|
|
309
|
+
]);
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ILoggedUserInfo, SingleResponseDto } from '@flusys/nestjs-shared';
|
|
2
|
-
import { ActionResponseDto, ActionTreeQueryDto, CreateActionDto, UpdateActionDto } from '../dtos/action.dto';
|
|
2
|
+
import { ActionResponseDto, ActionTreeForPermissionDto, ActionTreeQueryDto, CreateActionDto, UpdateActionDto } from '../dtos/action.dto';
|
|
3
3
|
import { ActionService } from '../services/action.service';
|
|
4
|
+
import { IAMConfigService } from '../services/iam-config.service';
|
|
4
5
|
declare const ActionController_base: abstract new (service: ActionService) => {
|
|
5
6
|
readonly enabledEndpoints: import("@flusys/nestjs-shared").ApiEndpoint[] | "all";
|
|
6
7
|
service: ActionService;
|
|
@@ -18,8 +19,9 @@ declare const ActionController_base: abstract new (service: ActionService) => {
|
|
|
18
19
|
};
|
|
19
20
|
export declare class ActionController extends ActionController_base {
|
|
20
21
|
actionService: ActionService;
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
private readonly iamConfigService;
|
|
23
|
+
constructor(actionService: ActionService, iamConfigService: IAMConfigService);
|
|
24
|
+
getActionsForPermission(dto: ActionTreeForPermissionDto, user: ILoggedUserInfo): Promise<SingleResponseDto<ActionResponseDto[]>>;
|
|
23
25
|
getActionTree(query: ActionTreeQueryDto, user: ILoggedUserInfo): Promise<SingleResponseDto<ActionResponseDto[]>>;
|
|
24
26
|
}
|
|
25
27
|
export {};
|
package/dtos/action.dto.d.ts
CHANGED
|
@@ -29,6 +29,9 @@ export declare class ActionResponseDto extends IdentityResponseDto {
|
|
|
29
29
|
export declare class ActionTreeDto extends ActionResponseDto {
|
|
30
30
|
children: ActionTreeDto[];
|
|
31
31
|
}
|
|
32
|
+
export declare class ActionTreeForPermissionDto {
|
|
33
|
+
companyId?: string;
|
|
34
|
+
}
|
|
32
35
|
export declare class ActionTreeQueryDto {
|
|
33
36
|
search?: string;
|
|
34
37
|
isActive?: boolean;
|
package/entities/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { UserIamPermissionWithCompany } from './permission-with-company.entity';
|
|
|
9
9
|
import { RoleWithCompany } from './role-with-company.entity';
|
|
10
10
|
import { Role } from './role.entity';
|
|
11
11
|
import { UserIamPermission } from './user-iam-permission.entity';
|
|
12
|
-
export declare const IAMCoreEntities: (typeof
|
|
12
|
+
export declare const IAMCoreEntities: (typeof UserIamPermission | typeof Role)[];
|
|
13
13
|
export declare const IAMCompanyEntities: (typeof UserIamPermissionWithCompany | typeof RoleWithCompany)[];
|
|
14
|
-
export declare const IAMAllEntities: (typeof
|
|
14
|
+
export declare const IAMAllEntities: (typeof UserIamPermission | typeof Role)[];
|
|
15
15
|
export declare function getIAMEntitiesByConfig(enableCompanyFeature: boolean, permissionMode?: 'FULL' | 'RBAC' | 'DIRECT'): any[];
|
|
@@ -25,13 +25,14 @@ function _ts_param(paramIndex, decorator) {
|
|
|
25
25
|
decorator(target, key, paramIndex);
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
|
+
import { ACTION_PERMISSIONS, createApiController, CurrentUser, ILoggedUserInfo, SingleResponseDto } from '@flusys/nestjs-shared';
|
|
28
29
|
import { JwtAuthGuard } from '@flusys/nestjs-shared/guards';
|
|
29
|
-
import { createApiController, CurrentUser, ILoggedUserInfo, SingleResponseDto, ACTION_PERMISSIONS } from '@flusys/nestjs-shared';
|
|
30
|
-
import { ACTION_MESSAGES } from '../config';
|
|
31
30
|
import { Body, Controller, Inject, Post, UseGuards } from '@nestjs/common';
|
|
32
31
|
import { ApiBearerAuth, ApiBody, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
|
33
|
-
import {
|
|
32
|
+
import { ACTION_MESSAGES } from '../config';
|
|
33
|
+
import { ActionResponseDto, ActionTreeForPermissionDto, ActionTreeQueryDto, CreateActionDto, UpdateActionDto } from '../dtos/action.dto';
|
|
34
34
|
import { ActionService } from '../services/action.service';
|
|
35
|
+
import { IAMConfigService } from '../services/iam-config.service';
|
|
35
36
|
export class ActionController extends createApiController(CreateActionDto, UpdateActionDto, ActionResponseDto, {
|
|
36
37
|
entityName: 'action',
|
|
37
38
|
security: {
|
|
@@ -79,8 +80,8 @@ export class ActionController extends createApiController(CreateActionDto, Updat
|
|
|
79
80
|
}
|
|
80
81
|
}
|
|
81
82
|
}) {
|
|
82
|
-
async getActionsForPermission(user) {
|
|
83
|
-
const actions = await this.actionService.getActionsForPermission(user);
|
|
83
|
+
async getActionsForPermission(dto, user) {
|
|
84
|
+
const actions = await this.actionService.getActionsForPermission(user, dto?.companyId);
|
|
84
85
|
return {
|
|
85
86
|
success: true,
|
|
86
87
|
message: 'Actions retrieved successfully',
|
|
@@ -97,8 +98,8 @@ export class ActionController extends createApiController(CreateActionDto, Updat
|
|
|
97
98
|
data: tree
|
|
98
99
|
};
|
|
99
100
|
}
|
|
100
|
-
constructor(actionService){
|
|
101
|
-
super(actionService), _define_property(this, "actionService", void 0), this.actionService = actionService;
|
|
101
|
+
constructor(actionService, iamConfigService){
|
|
102
|
+
super(actionService), _define_property(this, "actionService", void 0), _define_property(this, "iamConfigService", void 0), this.actionService = actionService, this.iamConfigService = iamConfigService;
|
|
102
103
|
}
|
|
103
104
|
}
|
|
104
105
|
_ts_decorate([
|
|
@@ -107,15 +108,20 @@ _ts_decorate([
|
|
|
107
108
|
ApiBearerAuth(),
|
|
108
109
|
ApiOperation({
|
|
109
110
|
summary: 'Get actions for permission assignment',
|
|
110
|
-
description:
|
|
111
|
+
description: "Returns actions available for permission assignment, filtered by company whitelist when company feature is enabled. Optional companyId scopes the whitelist to a specific company (must match the caller's own company); defaults to the caller's session company when omitted."
|
|
111
112
|
}),
|
|
112
113
|
ApiResponse({
|
|
113
114
|
status: 200,
|
|
114
115
|
type: SingleResponseDto
|
|
115
116
|
}),
|
|
116
|
-
|
|
117
|
+
ApiBody({
|
|
118
|
+
type: ActionTreeForPermissionDto
|
|
119
|
+
}),
|
|
120
|
+
_ts_param(0, Body()),
|
|
121
|
+
_ts_param(1, CurrentUser()),
|
|
117
122
|
_ts_metadata("design:type", Function),
|
|
118
123
|
_ts_metadata("design:paramtypes", [
|
|
124
|
+
typeof ActionTreeForPermissionDto === "undefined" ? Object : ActionTreeForPermissionDto,
|
|
119
125
|
typeof ILoggedUserInfo === "undefined" ? Object : ILoggedUserInfo
|
|
120
126
|
]),
|
|
121
127
|
_ts_metadata("design:returntype", Promise)
|
|
@@ -149,8 +155,10 @@ ActionController = _ts_decorate([
|
|
|
149
155
|
ApiTags('IAM - Actions'),
|
|
150
156
|
Controller('iam/actions'),
|
|
151
157
|
_ts_param(0, Inject(ActionService)),
|
|
158
|
+
_ts_param(1, Inject(IAMConfigService)),
|
|
152
159
|
_ts_metadata("design:type", Function),
|
|
153
160
|
_ts_metadata("design:paramtypes", [
|
|
154
|
-
typeof ActionService === "undefined" ? Object : ActionService
|
|
161
|
+
typeof ActionService === "undefined" ? Object : ActionService,
|
|
162
|
+
typeof IAMConfigService === "undefined" ? Object : IAMConfigService
|
|
155
163
|
])
|
|
156
164
|
], ActionController);
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { buildMockUser } from '@test-utils/mocks/logged-user.mock';
|
|
2
|
+
import { ActionController } from './action.controller';
|
|
3
|
+
describe('ActionController', ()=>{
|
|
4
|
+
let controller;
|
|
5
|
+
let mockActionService;
|
|
6
|
+
let mockIamConfigService;
|
|
7
|
+
beforeEach(()=>{
|
|
8
|
+
mockActionService = {
|
|
9
|
+
insert: jest.fn(),
|
|
10
|
+
findById: jest.fn(),
|
|
11
|
+
getAll: jest.fn(),
|
|
12
|
+
update: jest.fn(),
|
|
13
|
+
delete: jest.fn(),
|
|
14
|
+
getActionsForPermission: jest.fn(),
|
|
15
|
+
getActionTree: jest.fn()
|
|
16
|
+
};
|
|
17
|
+
mockIamConfigService = {};
|
|
18
|
+
controller = new ActionController(mockActionService, mockIamConfigService);
|
|
19
|
+
});
|
|
20
|
+
it('wires the injected service onto the generated controller base', ()=>{
|
|
21
|
+
expect(controller.actionService).toBe(mockActionService);
|
|
22
|
+
expect(controller.service).toBe(mockActionService);
|
|
23
|
+
});
|
|
24
|
+
describe('getActionsForPermission', ()=>{
|
|
25
|
+
it('returns actions scoped to the caller session company when no companyId is given', async ()=>{
|
|
26
|
+
const user = buildMockUser();
|
|
27
|
+
mockActionService.getActionsForPermission.mockResolvedValue([
|
|
28
|
+
{
|
|
29
|
+
id: 'action-1',
|
|
30
|
+
code: 'user.create'
|
|
31
|
+
}
|
|
32
|
+
]);
|
|
33
|
+
const result = await controller.getActionsForPermission({}, user);
|
|
34
|
+
expect(mockActionService.getActionsForPermission).toHaveBeenCalledWith(user, undefined);
|
|
35
|
+
expect(result.success).toBe(true);
|
|
36
|
+
expect(result.data).toEqual([
|
|
37
|
+
{
|
|
38
|
+
id: 'action-1',
|
|
39
|
+
code: 'user.create'
|
|
40
|
+
}
|
|
41
|
+
]);
|
|
42
|
+
});
|
|
43
|
+
it('forwards an explicit companyId override', async ()=>{
|
|
44
|
+
const user = buildMockUser();
|
|
45
|
+
mockActionService.getActionsForPermission.mockResolvedValue([]);
|
|
46
|
+
await controller.getActionsForPermission({
|
|
47
|
+
companyId: 'company-2'
|
|
48
|
+
}, user);
|
|
49
|
+
expect(mockActionService.getActionsForPermission).toHaveBeenCalledWith(user, 'company-2');
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
describe('getActionTree', ()=>{
|
|
53
|
+
it('returns the hierarchical action tree with query filters applied', async ()=>{
|
|
54
|
+
const user = buildMockUser();
|
|
55
|
+
mockActionService.getActionTree.mockResolvedValue([
|
|
56
|
+
{
|
|
57
|
+
id: 'action-1',
|
|
58
|
+
children: []
|
|
59
|
+
}
|
|
60
|
+
]);
|
|
61
|
+
const result = await controller.getActionTree({
|
|
62
|
+
search: 'user',
|
|
63
|
+
isActive: true,
|
|
64
|
+
withDeleted: false
|
|
65
|
+
}, user);
|
|
66
|
+
expect(mockActionService.getActionTree).toHaveBeenCalledWith(user, 'user', true, false);
|
|
67
|
+
expect(result.data).toEqual([
|
|
68
|
+
{
|
|
69
|
+
id: 'action-1',
|
|
70
|
+
children: []
|
|
71
|
+
}
|
|
72
|
+
]);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { CompanyActionPermissionController } from './company-action-permission.controller';
|
|
2
|
+
describe('CompanyActionPermissionController', ()=>{
|
|
3
|
+
let controller;
|
|
4
|
+
let mockPermissionService;
|
|
5
|
+
beforeEach(()=>{
|
|
6
|
+
mockPermissionService = {
|
|
7
|
+
assignCompanyActions: jest.fn(),
|
|
8
|
+
getCompanyActions: jest.fn()
|
|
9
|
+
};
|
|
10
|
+
controller = new CompanyActionPermissionController(mockPermissionService);
|
|
11
|
+
});
|
|
12
|
+
describe('assignCompanyActions', ()=>{
|
|
13
|
+
it('whitelists actions for a company', async ()=>{
|
|
14
|
+
mockPermissionService.assignCompanyActions.mockResolvedValue({
|
|
15
|
+
added: 3,
|
|
16
|
+
removed: 0,
|
|
17
|
+
total: 3
|
|
18
|
+
});
|
|
19
|
+
const result = await controller.assignCompanyActions({
|
|
20
|
+
companyId: 'company-1',
|
|
21
|
+
actionIds: [
|
|
22
|
+
'action-1',
|
|
23
|
+
'action-2',
|
|
24
|
+
'action-3'
|
|
25
|
+
]
|
|
26
|
+
});
|
|
27
|
+
expect(mockPermissionService.assignCompanyActions).toHaveBeenCalledWith({
|
|
28
|
+
companyId: 'company-1',
|
|
29
|
+
actionIds: [
|
|
30
|
+
'action-1',
|
|
31
|
+
'action-2',
|
|
32
|
+
'action-3'
|
|
33
|
+
]
|
|
34
|
+
});
|
|
35
|
+
expect(result.success).toBe(true);
|
|
36
|
+
expect(result.data).toEqual({
|
|
37
|
+
added: 3,
|
|
38
|
+
removed: 0,
|
|
39
|
+
total: 3
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
describe('getCompanyActions', ()=>{
|
|
44
|
+
it('returns the whitelisted actions for a company', async ()=>{
|
|
45
|
+
mockPermissionService.getCompanyActions.mockResolvedValue([
|
|
46
|
+
{
|
|
47
|
+
actionId: 'action-1'
|
|
48
|
+
}
|
|
49
|
+
]);
|
|
50
|
+
const result = await controller.getCompanyActions({
|
|
51
|
+
companyId: 'company-1'
|
|
52
|
+
});
|
|
53
|
+
expect(mockPermissionService.getCompanyActions).toHaveBeenCalledWith('company-1');
|
|
54
|
+
expect(result.data).toEqual([
|
|
55
|
+
{
|
|
56
|
+
actionId: 'action-1'
|
|
57
|
+
}
|
|
58
|
+
]);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
});
|