@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
|
@@ -25,15 +25,57 @@ function _ts_param(paramIndex, decorator) {
|
|
|
25
25
|
decorator(target, key, paramIndex);
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
|
-
import {
|
|
28
|
+
import { ApiService, HybridCache } from '@flusys/nestjs-shared/classes';
|
|
29
29
|
import { UtilsService } from '@flusys/nestjs-shared/modules';
|
|
30
30
|
import { applyCompanyFilter } from '@flusys/nestjs-shared/utils';
|
|
31
31
|
import { Inject, Injectable, Scope } from '@nestjs/common';
|
|
32
|
+
import { In } from 'typeorm';
|
|
33
|
+
import { UserIamPermissionWithCompany } from '../entities/permission-with-company.entity';
|
|
32
34
|
import { RoleWithCompany } from '../entities/role-with-company.entity';
|
|
33
35
|
import { Role } from '../entities/role.entity';
|
|
36
|
+
import { UserIamPermission } from '../entities/user-iam-permission.entity';
|
|
37
|
+
import { IamEntityType } from '../enums/iam-entity-type.enum';
|
|
38
|
+
import { IamPermissionType } from '../enums/iam-permission-type.enum';
|
|
34
39
|
import { IAMConfigService } from './iam-config.service';
|
|
35
40
|
import { IAMDataSourceService } from './iam-datasource.service';
|
|
41
|
+
import { PermissionCacheService } from './permission-cache.service';
|
|
36
42
|
export class RoleService extends ApiService {
|
|
43
|
+
async beforeDeleteOperation(dto, user, queryRunner) {
|
|
44
|
+
await super.beforeDeleteOperation(dto, user, queryRunner);
|
|
45
|
+
const roleIds = Array.isArray(dto.id) ? dto.id : [
|
|
46
|
+
dto.id
|
|
47
|
+
];
|
|
48
|
+
if (roleIds.length === 0) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const enableCompanyFeature = this.iamConfigService.isCompanyFeatureEnabled();
|
|
52
|
+
const permissionEntity = enableCompanyFeature ? UserIamPermissionWithCompany : UserIamPermission;
|
|
53
|
+
const permissionRepo = queryRunner.manager.getRepository(permissionEntity);
|
|
54
|
+
const userRoleRows = await permissionRepo.find({
|
|
55
|
+
where: {
|
|
56
|
+
permissionType: IamPermissionType.USER_ROLE,
|
|
57
|
+
sourceType: IamEntityType.USER,
|
|
58
|
+
targetType: IamEntityType.ROLE,
|
|
59
|
+
targetId: In(roleIds)
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
const affectedUserIds = [
|
|
63
|
+
...new Set(userRoleRows.map((row)=>row.userId).filter((id)=>!!id))
|
|
64
|
+
];
|
|
65
|
+
await permissionRepo.delete({
|
|
66
|
+
permissionType: IamPermissionType.ROLE_ACTION,
|
|
67
|
+
sourceType: IamEntityType.ROLE,
|
|
68
|
+
sourceId: In(roleIds)
|
|
69
|
+
});
|
|
70
|
+
await permissionRepo.delete({
|
|
71
|
+
permissionType: IamPermissionType.USER_ROLE,
|
|
72
|
+
targetType: IamEntityType.ROLE,
|
|
73
|
+
targetId: In(roleIds)
|
|
74
|
+
});
|
|
75
|
+
if (affectedUserIds.length > 0) {
|
|
76
|
+
await this.permissionCacheService.invalidateUsers(affectedUserIds);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
37
79
|
resolveEntity() {
|
|
38
80
|
return this.iamConfigService.isCompanyFeatureEnabled() ? RoleWithCompany : Role;
|
|
39
81
|
}
|
|
@@ -107,8 +149,8 @@ export class RoleService extends ApiService {
|
|
|
107
149
|
deletedById: entity.deletedById
|
|
108
150
|
};
|
|
109
151
|
}
|
|
110
|
-
constructor(cacheManager, utilsService, iamConfigService, dataSourceProvider){
|
|
111
|
-
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;
|
|
152
|
+
constructor(cacheManager, utilsService, iamConfigService, dataSourceProvider, permissionCacheService){
|
|
153
|
+
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;
|
|
112
154
|
}
|
|
113
155
|
}
|
|
114
156
|
RoleService = _ts_decorate([
|
|
@@ -119,11 +161,13 @@ RoleService = _ts_decorate([
|
|
|
119
161
|
_ts_param(1, Inject(UtilsService)),
|
|
120
162
|
_ts_param(2, Inject(IAMConfigService)),
|
|
121
163
|
_ts_param(3, Inject(IAMDataSourceService)),
|
|
164
|
+
_ts_param(4, Inject(PermissionCacheService)),
|
|
122
165
|
_ts_metadata("design:type", Function),
|
|
123
166
|
_ts_metadata("design:paramtypes", [
|
|
124
167
|
typeof HybridCache === "undefined" ? Object : HybridCache,
|
|
125
168
|
typeof UtilsService === "undefined" ? Object : UtilsService,
|
|
126
169
|
typeof IAMConfigService === "undefined" ? Object : IAMConfigService,
|
|
127
|
-
typeof IAMDataSourceService === "undefined" ? Object : IAMDataSourceService
|
|
170
|
+
typeof IAMDataSourceService === "undefined" ? Object : IAMDataSourceService,
|
|
171
|
+
typeof PermissionCacheService === "undefined" ? Object : PermissionCacheService
|
|
128
172
|
])
|
|
129
173
|
], RoleService);
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { Test } from '@nestjs/testing';
|
|
2
|
+
import { UtilsService } from '@flusys/nestjs-shared/modules';
|
|
3
|
+
import { createMockDataSourceProvider, createMockRepository } from '@test-utils/mocks/repository.mock';
|
|
4
|
+
import { buildMockUser } from '@test-utils/mocks/logged-user.mock';
|
|
5
|
+
import { Role } from '../entities/role.entity';
|
|
6
|
+
import { RoleWithCompany } from '../entities/role-with-company.entity';
|
|
7
|
+
import { IAMConfigService } from './iam-config.service';
|
|
8
|
+
import { IAMDataSourceService } from './iam-datasource.service';
|
|
9
|
+
import { PermissionCacheService } from './permission-cache.service';
|
|
10
|
+
import { RoleService } from './role.service';
|
|
11
|
+
function buildRole(overrides = {}) {
|
|
12
|
+
return {
|
|
13
|
+
id: 'role-uuid-1',
|
|
14
|
+
readOnly: false,
|
|
15
|
+
name: 'Manager',
|
|
16
|
+
description: null,
|
|
17
|
+
isActive: true,
|
|
18
|
+
serial: 1,
|
|
19
|
+
createdAt: new Date(),
|
|
20
|
+
updatedAt: new Date(),
|
|
21
|
+
deletedAt: null,
|
|
22
|
+
createdById: null,
|
|
23
|
+
updatedById: null,
|
|
24
|
+
deletedById: null,
|
|
25
|
+
...overrides
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
describe('RoleService', ()=>{
|
|
29
|
+
let service;
|
|
30
|
+
let mockRepo;
|
|
31
|
+
let mockIamConfigService;
|
|
32
|
+
let mockPermissionCacheService;
|
|
33
|
+
beforeEach(async ()=>{
|
|
34
|
+
mockRepo = createMockRepository();
|
|
35
|
+
const mockDataSourceProvider = createMockDataSourceProvider(mockRepo);
|
|
36
|
+
mockIamConfigService = {
|
|
37
|
+
isCompanyFeatureEnabled: jest.fn().mockReturnValue(false)
|
|
38
|
+
};
|
|
39
|
+
mockPermissionCacheService = {
|
|
40
|
+
invalidateUsers: jest.fn()
|
|
41
|
+
};
|
|
42
|
+
const module = await Test.createTestingModule({
|
|
43
|
+
providers: [
|
|
44
|
+
RoleService,
|
|
45
|
+
{
|
|
46
|
+
provide: 'CACHE_INSTANCE',
|
|
47
|
+
useValue: {}
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
provide: UtilsService,
|
|
51
|
+
useValue: {}
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
provide: IAMConfigService,
|
|
55
|
+
useValue: mockIamConfigService
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
provide: IAMDataSourceService,
|
|
59
|
+
useValue: mockDataSourceProvider
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
provide: PermissionCacheService,
|
|
63
|
+
useValue: mockPermissionCacheService
|
|
64
|
+
}
|
|
65
|
+
]
|
|
66
|
+
}).compile();
|
|
67
|
+
service = await module.resolve(RoleService);
|
|
68
|
+
});
|
|
69
|
+
describe('resolveEntity', ()=>{
|
|
70
|
+
it('resolves to Role when the company feature is disabled', ()=>{
|
|
71
|
+
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(false);
|
|
72
|
+
expect(service.resolveEntity()).toBe(Role);
|
|
73
|
+
});
|
|
74
|
+
it('resolves to RoleWithCompany when the company feature is enabled', ()=>{
|
|
75
|
+
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
76
|
+
expect(service.resolveEntity()).toBe(RoleWithCompany);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
describe('convertSingleDtoToEntity', ()=>{
|
|
80
|
+
it('creates a plain entity without companyId when the company feature is disabled', async ()=>{
|
|
81
|
+
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(false);
|
|
82
|
+
await service.ensureDataSourceRepository();
|
|
83
|
+
mockRepo.create.mockReturnValue({});
|
|
84
|
+
const entity = await service.convertSingleDtoToEntity({
|
|
85
|
+
name: 'Manager'
|
|
86
|
+
}, buildMockUser());
|
|
87
|
+
expect(entity.companyId).toBeUndefined();
|
|
88
|
+
});
|
|
89
|
+
it('stamps companyId from the DTO on create when the company feature is enabled', async ()=>{
|
|
90
|
+
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
91
|
+
await service.ensureDataSourceRepository();
|
|
92
|
+
mockRepo.create.mockReturnValue({});
|
|
93
|
+
const entity = await service.convertSingleDtoToEntity({
|
|
94
|
+
name: 'Manager',
|
|
95
|
+
companyId: 'company-explicit'
|
|
96
|
+
}, buildMockUser({
|
|
97
|
+
companyId: 'company-from-user'
|
|
98
|
+
}));
|
|
99
|
+
expect(entity.companyId).toBe('company-explicit');
|
|
100
|
+
});
|
|
101
|
+
it('falls back to the user companyId on create when the DTO omits it', async ()=>{
|
|
102
|
+
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
103
|
+
await service.ensureDataSourceRepository();
|
|
104
|
+
mockRepo.create.mockReturnValue({});
|
|
105
|
+
const entity = await service.convertSingleDtoToEntity({
|
|
106
|
+
name: 'Manager'
|
|
107
|
+
}, buildMockUser({
|
|
108
|
+
companyId: 'company-from-user'
|
|
109
|
+
}));
|
|
110
|
+
expect(entity.companyId).toBe('company-from-user');
|
|
111
|
+
});
|
|
112
|
+
it('does not override companyId on update when the DTO omits it', async ()=>{
|
|
113
|
+
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
114
|
+
await service.ensureDataSourceRepository();
|
|
115
|
+
const existing = buildRole({
|
|
116
|
+
id: 'role-uuid-1',
|
|
117
|
+
companyId: 'existing-company'
|
|
118
|
+
});
|
|
119
|
+
mockRepo.findOne.mockResolvedValue(existing);
|
|
120
|
+
const entity = await service.convertSingleDtoToEntity({
|
|
121
|
+
id: 'role-uuid-1',
|
|
122
|
+
name: 'Manager Updated'
|
|
123
|
+
}, buildMockUser({
|
|
124
|
+
companyId: 'company-from-user'
|
|
125
|
+
}));
|
|
126
|
+
expect(entity.companyId).toBe('existing-company');
|
|
127
|
+
});
|
|
128
|
+
it('overrides companyId on update when explicitly provided in the DTO', async ()=>{
|
|
129
|
+
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
130
|
+
await service.ensureDataSourceRepository();
|
|
131
|
+
const existing = buildRole({
|
|
132
|
+
id: 'role-uuid-1',
|
|
133
|
+
companyId: 'existing-company'
|
|
134
|
+
});
|
|
135
|
+
mockRepo.findOne.mockResolvedValue(existing);
|
|
136
|
+
const entity = await service.convertSingleDtoToEntity({
|
|
137
|
+
id: 'role-uuid-1',
|
|
138
|
+
name: 'Manager Updated',
|
|
139
|
+
companyId: 'new-company'
|
|
140
|
+
}, buildMockUser());
|
|
141
|
+
expect(entity.companyId).toBe('new-company');
|
|
142
|
+
});
|
|
143
|
+
it('throws NotFoundException when updating a role that does not exist', async ()=>{
|
|
144
|
+
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(false);
|
|
145
|
+
await service.ensureDataSourceRepository();
|
|
146
|
+
mockRepo.findOne.mockResolvedValue(null);
|
|
147
|
+
await expect(service.convertSingleDtoToEntity({
|
|
148
|
+
id: 'missing-role'
|
|
149
|
+
}, buildMockUser())).rejects.toThrow();
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
describe('getSelectQuery', ()=>{
|
|
153
|
+
it('selects the default field set without companyId when the company feature is disabled', async ()=>{
|
|
154
|
+
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(false);
|
|
155
|
+
const query = mockRepo.createQueryBuilder();
|
|
156
|
+
await service.getSelectQuery(query, buildMockUser());
|
|
157
|
+
expect(query.select).toHaveBeenCalledWith(expect.not.arrayContaining([
|
|
158
|
+
'role.companyId'
|
|
159
|
+
]));
|
|
160
|
+
});
|
|
161
|
+
it('includes companyId in the default field set when the company feature is enabled', async ()=>{
|
|
162
|
+
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
163
|
+
const query = mockRepo.createQueryBuilder();
|
|
164
|
+
await service.getSelectQuery(query, buildMockUser());
|
|
165
|
+
expect(query.select).toHaveBeenCalledWith(expect.arrayContaining([
|
|
166
|
+
'role.companyId'
|
|
167
|
+
]));
|
|
168
|
+
});
|
|
169
|
+
it('uses the explicitly provided select list as-is', async ()=>{
|
|
170
|
+
const query = mockRepo.createQueryBuilder();
|
|
171
|
+
await service.getSelectQuery(query, buildMockUser(), [
|
|
172
|
+
'id',
|
|
173
|
+
'name'
|
|
174
|
+
]);
|
|
175
|
+
expect(query.select).toHaveBeenCalledWith([
|
|
176
|
+
'role.id',
|
|
177
|
+
'role.name'
|
|
178
|
+
]);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
describe('getGlobalSearchQuery', ()=>{
|
|
182
|
+
it('filters by name or description', async ()=>{
|
|
183
|
+
const query = mockRepo.createQueryBuilder();
|
|
184
|
+
await service.getGlobalSearchQuery(query, 'manager');
|
|
185
|
+
expect(query.andWhere).toHaveBeenCalledWith('(role.name LIKE :search OR role.description LIKE :search)', {
|
|
186
|
+
search: '%manager%'
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
describe('getExtraManipulateQuery', ()=>{
|
|
191
|
+
it('applies a company filter clause when the company feature is enabled and the user has a company', async ()=>{
|
|
192
|
+
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
193
|
+
const query = mockRepo.createQueryBuilder();
|
|
194
|
+
await service.getExtraManipulateQuery(query, {}, buildMockUser({
|
|
195
|
+
companyId: 'company-1'
|
|
196
|
+
}));
|
|
197
|
+
expect(query.andWhere).toHaveBeenCalledWith('role.companyId = :companyId', {
|
|
198
|
+
companyId: 'company-1'
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
it('does not apply a company filter when the company feature is disabled', async ()=>{
|
|
202
|
+
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(false);
|
|
203
|
+
const query = mockRepo.createQueryBuilder();
|
|
204
|
+
await service.getExtraManipulateQuery(query, {}, buildMockUser({
|
|
205
|
+
companyId: 'company-1'
|
|
206
|
+
}));
|
|
207
|
+
expect(query.andWhere).not.toHaveBeenCalled();
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
describe('convertEntityToResponseDto', ()=>{
|
|
211
|
+
it('defaults companyId to null when the entity has no companyId field', ()=>{
|
|
212
|
+
const entity = buildRole();
|
|
213
|
+
const dto = service.convertEntityToResponseDto(entity, false);
|
|
214
|
+
expect(dto.companyId).toBeNull();
|
|
215
|
+
});
|
|
216
|
+
it('surfaces companyId when present on the entity', ()=>{
|
|
217
|
+
const entity = buildRole({
|
|
218
|
+
companyId: 'company-1'
|
|
219
|
+
});
|
|
220
|
+
const dto = service.convertEntityToResponseDto(entity, false);
|
|
221
|
+
expect(dto.companyId).toBe('company-1');
|
|
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 = createMockRepository();
|
|
234
|
+
const queryRunner = buildQueryRunner(permissionRepo);
|
|
235
|
+
await service.beforeDeleteOperation({
|
|
236
|
+
id: [],
|
|
237
|
+
type: 'delete'
|
|
238
|
+
}, buildMockUser(), queryRunner);
|
|
239
|
+
expect(permissionRepo.find).not.toHaveBeenCalled();
|
|
240
|
+
});
|
|
241
|
+
it('cascades ROLE_ACTION and USER_ROLE cleanup and invalidates affected users cache', async ()=>{
|
|
242
|
+
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(false);
|
|
243
|
+
const permissionRepo = createMockRepository();
|
|
244
|
+
permissionRepo.find.mockResolvedValue([
|
|
245
|
+
{
|
|
246
|
+
userId: 'user-1'
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
userId: 'user-2'
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
userId: null
|
|
253
|
+
}
|
|
254
|
+
]);
|
|
255
|
+
const queryRunner = buildQueryRunner(permissionRepo);
|
|
256
|
+
await service.beforeDeleteOperation({
|
|
257
|
+
id: 'role-uuid-1',
|
|
258
|
+
type: 'delete'
|
|
259
|
+
}, buildMockUser(), queryRunner);
|
|
260
|
+
expect(permissionRepo.delete).toHaveBeenCalledWith(expect.objectContaining({
|
|
261
|
+
permissionType: 'role_action',
|
|
262
|
+
sourceType: 'role'
|
|
263
|
+
}));
|
|
264
|
+
expect(permissionRepo.delete).toHaveBeenCalledWith(expect.objectContaining({
|
|
265
|
+
permissionType: 'user_role',
|
|
266
|
+
targetType: 'role'
|
|
267
|
+
}));
|
|
268
|
+
expect(mockPermissionCacheService.invalidateUsers).toHaveBeenCalledWith([
|
|
269
|
+
'user-1',
|
|
270
|
+
'user-2'
|
|
271
|
+
]);
|
|
272
|
+
});
|
|
273
|
+
it('does not invalidate any cache when no users are affected', async ()=>{
|
|
274
|
+
const permissionRepo = createMockRepository();
|
|
275
|
+
permissionRepo.find.mockResolvedValue([]);
|
|
276
|
+
const queryRunner = buildQueryRunner(permissionRepo);
|
|
277
|
+
await service.beforeDeleteOperation({
|
|
278
|
+
id: [
|
|
279
|
+
'role-uuid-1'
|
|
280
|
+
],
|
|
281
|
+
type: 'delete'
|
|
282
|
+
}, buildMockUser(), queryRunner);
|
|
283
|
+
expect(mockPermissionCacheService.invalidateUsers).not.toHaveBeenCalled();
|
|
284
|
+
});
|
|
285
|
+
it('supports array ids and dedupes affected user ids', async ()=>{
|
|
286
|
+
const permissionRepo = createMockRepository();
|
|
287
|
+
permissionRepo.find.mockResolvedValue([
|
|
288
|
+
{
|
|
289
|
+
userId: 'user-1'
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
userId: 'user-1'
|
|
293
|
+
}
|
|
294
|
+
]);
|
|
295
|
+
const queryRunner = buildQueryRunner(permissionRepo);
|
|
296
|
+
await service.beforeDeleteOperation({
|
|
297
|
+
id: [
|
|
298
|
+
'role-uuid-1',
|
|
299
|
+
'role-uuid-2'
|
|
300
|
+
],
|
|
301
|
+
type: 'delete'
|
|
302
|
+
}, buildMockUser(), queryRunner);
|
|
303
|
+
expect(mockPermissionCacheService.invalidateUsers).toHaveBeenCalledWith([
|
|
304
|
+
'user-1'
|
|
305
|
+
]);
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flusys/nestjs-iam",
|
|
3
|
-
"version": "6.0
|
|
3
|
+
"version": "6.1.0",
|
|
4
4
|
"description": "Identity and Access Management (IAM) module for NestJS applications",
|
|
5
5
|
"main": "cjs/index.js",
|
|
6
6
|
"module": "fesm/index.js",
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
"express": "^5.0.0"
|
|
91
91
|
},
|
|
92
92
|
"dependencies": {
|
|
93
|
-
"@flusys/nestjs-core": "6.0
|
|
94
|
-
"@flusys/nestjs-shared": "6.0
|
|
93
|
+
"@flusys/nestjs-core": "6.1.0",
|
|
94
|
+
"@flusys/nestjs-shared": "6.1.0"
|
|
95
95
|
}
|
|
96
96
|
}
|
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
import { ApiService, HybridCache } from '@flusys/nestjs-shared/classes';
|
|
2
|
+
import { DeleteDto } from '@flusys/nestjs-shared/dtos';
|
|
2
3
|
import { ILoggedUserInfo } from '@flusys/nestjs-shared/interfaces';
|
|
3
4
|
import { UtilsService } from '@flusys/nestjs-shared/modules';
|
|
4
|
-
import { SelectQueryBuilder } from 'typeorm';
|
|
5
|
+
import { QueryRunner, SelectQueryBuilder } from 'typeorm';
|
|
5
6
|
import { CreateActionDto, UpdateActionDto } from '../dtos/action.dto';
|
|
6
7
|
import { Action } from '../entities/action.entity';
|
|
7
8
|
import { IAction, IActionTree } from '../interfaces/action.interface';
|
|
8
9
|
import { IAMConfigService } from './iam-config.service';
|
|
9
10
|
import { IAMDataSourceService } from './iam-datasource.service';
|
|
11
|
+
import { PermissionCacheService } from './permission-cache.service';
|
|
10
12
|
import { PermissionService } from './permission.service';
|
|
11
13
|
export declare class ActionService extends ApiService<CreateActionDto, UpdateActionDto, IAction, Action> {
|
|
12
14
|
protected cacheManager: HybridCache;
|
|
13
15
|
protected utilsService: UtilsService;
|
|
14
16
|
private readonly iamConfigService;
|
|
15
17
|
private readonly permissionService;
|
|
16
|
-
|
|
18
|
+
private readonly permissionCacheService;
|
|
19
|
+
constructor(cacheManager: HybridCache, utilsService: UtilsService, iamConfigService: IAMConfigService, dataSourceProvider: IAMDataSourceService, permissionService: PermissionService, permissionCacheService: PermissionCacheService);
|
|
20
|
+
protected beforeDeleteOperation(dto: DeleteDto, user: ILoggedUserInfo | null, queryRunner: QueryRunner): Promise<void>;
|
|
17
21
|
getSelectQuery(query: SelectQueryBuilder<Action>, _user: ILoggedUserInfo | null, select?: string[]): Promise<{
|
|
18
22
|
query: SelectQueryBuilder<Action>;
|
|
19
23
|
isRaw: boolean;
|
|
@@ -25,7 +29,7 @@ export declare class ActionService extends ApiService<CreateActionDto, UpdateAct
|
|
|
25
29
|
protected convertEntityToResponseDto(entity: Action, _isRaw: boolean): IAction;
|
|
26
30
|
private readonly actionSelectFields;
|
|
27
31
|
private requireUser;
|
|
28
|
-
getActionsForPermission(user: ILoggedUserInfo): Promise<IAction[]>;
|
|
32
|
+
getActionsForPermission(user: ILoggedUserInfo, companyId?: string): Promise<IAction[]>;
|
|
29
33
|
getActionTree(user: ILoggedUserInfo, search?: string, isActive?: boolean, withDeleted?: boolean): Promise<IActionTree[]>;
|
|
30
34
|
private buildActionTree;
|
|
31
35
|
}
|
|
@@ -1,10 +1,5 @@
|
|
|
1
|
-
import { HybridCache } from '@flusys/nestjs-shared';
|
|
2
|
-
export
|
|
3
|
-
userId: string;
|
|
4
|
-
companyId?: string | null;
|
|
5
|
-
branchId?: string | null;
|
|
6
|
-
enableCompanyFeature: boolean;
|
|
7
|
-
}
|
|
1
|
+
import { HybridCache, PermissionCacheKeyOptions, PermissionGuardConfig } from '@flusys/nestjs-shared';
|
|
2
|
+
export { PermissionCacheKeyOptions } from '@flusys/nestjs-shared';
|
|
8
3
|
export interface CachedMyPermissions {
|
|
9
4
|
frontendActions: Array<{
|
|
10
5
|
id: string;
|
|
@@ -15,24 +10,23 @@ export interface CachedMyPermissions {
|
|
|
15
10
|
}>;
|
|
16
11
|
backendCodes: string[];
|
|
17
12
|
}
|
|
13
|
+
export declare const MY_PERMISSIONS_CACHE_PREFIX = "my-permissions";
|
|
18
14
|
export declare class PermissionCacheService {
|
|
19
15
|
private readonly cacheManager;
|
|
16
|
+
private readonly guardConfig?;
|
|
17
|
+
private readonly TRACKED_KEYS_PREFIX;
|
|
18
|
+
private readonly TRACKED_SCOPES_PREFIX;
|
|
20
19
|
private readonly TTL;
|
|
21
|
-
|
|
22
|
-
private
|
|
23
|
-
private readonly MY_PERMISSIONS_PREFIX;
|
|
24
|
-
private readonly ACTION_CODE_PREFIX;
|
|
25
|
-
constructor(cacheManager: HybridCache);
|
|
26
|
-
generateCacheKey(options: PermissionCacheKeyOptions): string;
|
|
27
|
-
generateMyPermissionsCacheKey(options: PermissionCacheKeyOptions): string;
|
|
20
|
+
constructor(cacheManager: HybridCache, guardConfig?: PermissionGuardConfig | undefined);
|
|
21
|
+
private parseDurationMs;
|
|
28
22
|
private buildCacheKey;
|
|
29
|
-
|
|
23
|
+
private scopeToken;
|
|
24
|
+
private buildTrackedKeysKey;
|
|
25
|
+
private buildTrackedScopesKey;
|
|
30
26
|
setMyPermissions(options: PermissionCacheKeyOptions, data: CachedMyPermissions): Promise<void>;
|
|
31
27
|
getMyPermissions(options: PermissionCacheKeyOptions): Promise<CachedMyPermissions | null>;
|
|
32
|
-
private
|
|
33
|
-
setActionCodeMap(codeToIdMap: Record<string, string>, tenantId?: string): Promise<void>;
|
|
34
|
-
getActionIdsByCodes(codes: string[], tenantId?: string): Promise<Record<string, string> | null>;
|
|
28
|
+
private trackKey;
|
|
35
29
|
invalidateUser(userId: string, companyId?: string | null, branchIds?: (string | null)[]): Promise<void>;
|
|
30
|
+
private invalidateScope;
|
|
36
31
|
invalidateUsers(userIds: string[], companyId?: string | null, branchIds?: (string | null)[]): Promise<number>;
|
|
37
|
-
invalidateRole(_roleId: string, userIds: string[], companyId?: string | null, branchIds?: (string | null)[]): Promise<number>;
|
|
38
32
|
}
|
|
@@ -10,6 +10,7 @@ export declare class PermissionService {
|
|
|
10
10
|
private getPermissionRepository;
|
|
11
11
|
private getActionRepository;
|
|
12
12
|
private getRoleRepository;
|
|
13
|
+
private getActionsMap;
|
|
13
14
|
assignUserActions(dto: AssignUserActionsDto): Promise<PermissionOperationResultDto>;
|
|
14
15
|
getUserActions(userId: string, branchId: string | undefined, companyId?: string | undefined): Promise<UserActionResponseDto[]>;
|
|
15
16
|
assignRoleActions(dto: AssignRoleActionsDto): Promise<PermissionOperationResultDto>;
|
|
@@ -23,7 +24,6 @@ export declare class PermissionService {
|
|
|
23
24
|
getUserRoles(userId: string, branchId?: string | undefined, companyId?: string | undefined): Promise<UserRoleResponseDto[]>;
|
|
24
25
|
getMyPermissions(userId: string, branchId: string | null, companyId?: string | null, parentCodes?: string[]): Promise<MyPermissionsResponseDto>;
|
|
25
26
|
private buildResponseFromCache;
|
|
26
|
-
private getCurrentTenantId;
|
|
27
27
|
private getParentIdsByCodesWithCache;
|
|
28
28
|
private fetchAndCachePermissions;
|
|
29
29
|
private collectAllActionIds;
|
|
@@ -34,7 +34,10 @@ export declare class PermissionService {
|
|
|
34
34
|
private getUserRoleIds;
|
|
35
35
|
private getRoleActionIds;
|
|
36
36
|
private getUserActionIds;
|
|
37
|
-
private invalidateUserPermissionCache;
|
|
38
37
|
private invalidateRoleMembersCache;
|
|
39
|
-
|
|
38
|
+
invalidateCompanyMembersCache(companyId: string): Promise<number>;
|
|
39
|
+
revokeCompanyPermissions(companyId: string): Promise<void>;
|
|
40
|
+
revokeBranchPermissions(branchId: string, companyId: string): Promise<void>;
|
|
41
|
+
revokeUserCompanyAccess(userId: string, companyId: string): Promise<void>;
|
|
42
|
+
revokeUserBranchAccess(userId: string, branchId: string, companyId: string): Promise<void>;
|
|
40
43
|
}
|
|
@@ -1,17 +1,21 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ApiService, HybridCache } from '@flusys/nestjs-shared/classes';
|
|
2
|
+
import { DeleteDto } from '@flusys/nestjs-shared/dtos';
|
|
2
3
|
import { ILoggedUserInfo } from '@flusys/nestjs-shared/interfaces';
|
|
3
4
|
import { UtilsService } from '@flusys/nestjs-shared/modules';
|
|
4
|
-
import { EntityTarget, SelectQueryBuilder } from 'typeorm';
|
|
5
|
+
import { EntityTarget, QueryRunner, SelectQueryBuilder } from 'typeorm';
|
|
5
6
|
import { CreateRoleDto, UpdateRoleDto } from '../dtos/role.dto';
|
|
6
7
|
import { RoleBase } from '../entities/role-base.entity';
|
|
7
8
|
import { IRole } from '../interfaces/role.interface';
|
|
8
9
|
import { IAMConfigService } from './iam-config.service';
|
|
9
10
|
import { IAMDataSourceService } from './iam-datasource.service';
|
|
11
|
+
import { PermissionCacheService } from './permission-cache.service';
|
|
10
12
|
export declare class RoleService extends ApiService<CreateRoleDto, UpdateRoleDto, IRole, RoleBase> {
|
|
11
13
|
protected cacheManager: HybridCache;
|
|
12
14
|
protected utilsService: UtilsService;
|
|
13
15
|
private readonly iamConfigService;
|
|
14
|
-
|
|
16
|
+
private readonly permissionCacheService;
|
|
17
|
+
constructor(cacheManager: HybridCache, utilsService: UtilsService, iamConfigService: IAMConfigService, dataSourceProvider: IAMDataSourceService, permissionCacheService: PermissionCacheService);
|
|
18
|
+
protected beforeDeleteOperation(dto: DeleteDto, user: ILoggedUserInfo | null, queryRunner: QueryRunner): Promise<void>;
|
|
15
19
|
protected resolveEntity(): EntityTarget<RoleBase>;
|
|
16
20
|
convertSingleDtoToEntity(dto: CreateRoleDto | UpdateRoleDto, user: ILoggedUserInfo | null): Promise<RoleBase>;
|
|
17
21
|
getSelectQuery(query: SelectQueryBuilder<RoleBase>, _user: ILoggedUserInfo | null, select?: string[]): Promise<{
|