@flusys/nestjs-iam 6.2.0 → 6.2.2
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/dtos/permission.dto.js +0 -7
- package/cjs/services/permission-cache.service.js +2 -2
- package/cjs/services/permission-cache.service.spec.js +32 -13
- package/cjs/services/permission.service.spec.js +0 -1
- package/dtos/permission.dto.d.ts +0 -1
- package/fesm/dtos/permission.dto.js +0 -7
- package/fesm/services/permission-cache.service.js +2 -2
- package/fesm/services/permission-cache.service.spec.js +32 -13
- package/fesm/services/permission.service.spec.js +0 -1
- package/package.json +3 -3
|
@@ -544,18 +544,11 @@ _ts_decorate([
|
|
|
544
544
|
], UserRoleResponseDto.prototype, "createdAt", void 0);
|
|
545
545
|
let FrontendActionDto = class FrontendActionDto {
|
|
546
546
|
constructor(){
|
|
547
|
-
_define_property(this, "id", void 0);
|
|
548
547
|
_define_property(this, "code", void 0);
|
|
549
548
|
_define_property(this, "name", void 0);
|
|
550
549
|
_define_property(this, "description", void 0);
|
|
551
550
|
}
|
|
552
551
|
};
|
|
553
|
-
_ts_decorate([
|
|
554
|
-
(0, _swagger.ApiProperty)({
|
|
555
|
-
description: 'Action ID'
|
|
556
|
-
}),
|
|
557
|
-
_ts_metadata("design:type", String)
|
|
558
|
-
], FrontendActionDto.prototype, "id", void 0);
|
|
559
552
|
_ts_decorate([
|
|
560
553
|
(0, _swagger.ApiProperty)({
|
|
561
554
|
description: 'Action code'
|
|
@@ -166,7 +166,6 @@ let PermissionCacheService = class PermissionCacheService {
|
|
|
166
166
|
}
|
|
167
167
|
return {
|
|
168
168
|
frontendActions: frontendActions.map((a)=>({
|
|
169
|
-
id: a.id,
|
|
170
169
|
code: a.code,
|
|
171
170
|
name: a.name,
|
|
172
171
|
description: a.description
|
|
@@ -392,7 +391,8 @@ let PermissionCacheService = class PermissionCacheService {
|
|
|
392
391
|
} catch {
|
|
393
392
|
trackedKeys = [];
|
|
394
393
|
}
|
|
395
|
-
|
|
394
|
+
const hasCompanyWideScope = branchIds?.some((branchId)=>!branchId);
|
|
395
|
+
if (!branchIds?.length || hasCompanyWideScope) {
|
|
396
396
|
trackedKeys.forEach((key)=>keysToDelete.add(key));
|
|
397
397
|
keysToDelete.add(trackedKeysKey);
|
|
398
398
|
await untrackScope(trackedKeysKey);
|
|
@@ -211,8 +211,6 @@ describe('PermissionCacheService', ()=>{
|
|
|
211
211
|
expect(mockCacheManager.del).not.toHaveBeenCalledWith(expect.stringContaining('permission-tracked-scopes'));
|
|
212
212
|
});
|
|
213
213
|
it('invalidates only the requested branch keys, keeping remaining branches tracked', async ()=>{
|
|
214
|
-
// company-scoped key format only differentiates by branchId when the guard config
|
|
215
|
-
// enables the company feature — otherwise every branch maps to the same cache key.
|
|
216
214
|
const guardConfig = {
|
|
217
215
|
enableCompanyFeature: true
|
|
218
216
|
};
|
|
@@ -229,18 +227,15 @@ describe('PermissionCacheService', ()=>{
|
|
|
229
227
|
];
|
|
230
228
|
return undefined;
|
|
231
229
|
});
|
|
232
|
-
// companyId given directly invokes invalidateScope (bypasses the tracked-scopes fan-out)
|
|
233
230
|
await service.invalidateUser('user-1', 'company-1', [
|
|
234
231
|
'branch-a'
|
|
235
232
|
]);
|
|
236
|
-
// the specific branch key gets deleted
|
|
237
233
|
const deletedKeys = mockCacheManager.del.mock.calls.map(([key])=>key);
|
|
238
234
|
expect(deletedKeys).toContain((0, _nestjsshared.buildPermissionCacheKey)({
|
|
239
235
|
userId: 'user-1',
|
|
240
236
|
companyId: 'company-1',
|
|
241
237
|
branchId: 'branch-a'
|
|
242
238
|
}, guardConfig));
|
|
243
|
-
// remaining tracked keys get re-saved rather than the tracked-keys index being wiped
|
|
244
239
|
expect(mockCacheManager.set).toHaveBeenCalledWith(expect.stringContaining('permission-tracked-keys'), expect.arrayContaining([
|
|
245
240
|
keptKey,
|
|
246
241
|
'some-other-tracked-key'
|
|
@@ -269,6 +264,37 @@ describe('PermissionCacheService', ()=>{
|
|
|
269
264
|
expect(deletedKeys).toContain(onlyKey);
|
|
270
265
|
expect(deletedKeys.some((k)=>String(k).includes('permission-tracked-keys'))).toBe(true);
|
|
271
266
|
});
|
|
267
|
+
it('wipes every tracked branch key when a null branchId (company-wide grant) is in the scope list', async ()=>{
|
|
268
|
+
const guardConfig = {
|
|
269
|
+
enableCompanyFeature: true
|
|
270
|
+
};
|
|
271
|
+
const service = buildService(guardConfig);
|
|
272
|
+
const branchAKey = (0, _nestjsshared.buildPermissionCacheKey)({
|
|
273
|
+
userId: 'user-1',
|
|
274
|
+
companyId: 'company-1',
|
|
275
|
+
branchId: 'branch-a'
|
|
276
|
+
}, guardConfig);
|
|
277
|
+
const branchBKey = (0, _nestjsshared.buildPermissionCacheKey)({
|
|
278
|
+
userId: 'user-1',
|
|
279
|
+
companyId: 'company-1',
|
|
280
|
+
branchId: 'branch-b'
|
|
281
|
+
}, guardConfig);
|
|
282
|
+
const trackedKeysKey = 'permission-tracked-keys:user:user-1:company:company-1';
|
|
283
|
+
mockCacheManager.get.mockImplementation(async (key)=>{
|
|
284
|
+
if (key.includes('permission-tracked-keys')) return [
|
|
285
|
+
branchAKey,
|
|
286
|
+
branchBKey
|
|
287
|
+
];
|
|
288
|
+
return undefined;
|
|
289
|
+
});
|
|
290
|
+
await service.invalidateUser('user-1', 'company-1', [
|
|
291
|
+
null
|
|
292
|
+
]);
|
|
293
|
+
const deletedKeys = mockCacheManager.del.mock.calls.map(([key])=>key);
|
|
294
|
+
expect(deletedKeys).toContain(branchAKey);
|
|
295
|
+
expect(deletedKeys).toContain(branchBKey);
|
|
296
|
+
expect(deletedKeys.some((k)=>String(k).includes(trackedKeysKey))).toBe(true);
|
|
297
|
+
});
|
|
272
298
|
it('without a companyId, invalidates every tracked scope for the user and clears the scopes index', async ()=>{
|
|
273
299
|
const service = buildService();
|
|
274
300
|
// scopes list mixes a nested tracked-keys index (company scope) with a raw cache key (untracked scope)
|
|
@@ -400,7 +426,7 @@ describe('PermissionCacheService', ()=>{
|
|
|
400
426
|
'user'
|
|
401
427
|
]);
|
|
402
428
|
expect(result.frontendActions).toHaveLength(1);
|
|
403
|
-
expect(result.frontendActions[0].
|
|
429
|
+
expect(result.frontendActions[0].code).toBe('user.view');
|
|
404
430
|
});
|
|
405
431
|
it('returns an empty frontendActions list when parentCodes resolve to no known parents', async ()=>{
|
|
406
432
|
const service = buildService();
|
|
@@ -563,7 +589,6 @@ describe('PermissionCacheService', ()=>{
|
|
|
563
589
|
expect(result.cachedEndpoints).toBe(1);
|
|
564
590
|
expect(result.frontendActions).toEqual([
|
|
565
591
|
{
|
|
566
|
-
id: 'action-1',
|
|
567
592
|
code: 'user.view',
|
|
568
593
|
name: 'View Users',
|
|
569
594
|
description: null
|
|
@@ -606,9 +631,6 @@ describe('PermissionCacheService', ()=>{
|
|
|
606
631
|
expect(result).toBe(2);
|
|
607
632
|
});
|
|
608
633
|
it('wipes every tracked cache entry per member instead of only branches with an existing permission row', async ()=>{
|
|
609
|
-
// Company-wide action changes are merged into every branch a member queries, including
|
|
610
|
-
// branches where the member holds no explicit permission row — so invalidation must not
|
|
611
|
-
// be narrowed to whatever branch_ids happen to already exist in the permission table.
|
|
612
634
|
const service = buildService();
|
|
613
635
|
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
614
636
|
mockRawMany(mockPermissionRepo, [
|
|
@@ -665,9 +687,6 @@ describe('PermissionCacheService', ()=>{
|
|
|
665
687
|
expect(result).toBe(1);
|
|
666
688
|
});
|
|
667
689
|
it('wipes every tracked cache entry for role members instead of only branches with an existing permission row', async ()=>{
|
|
668
|
-
// Role actions are merged into every branch a member queries, including branches
|
|
669
|
-
// where the member holds no explicit permission row — so invalidation must not be
|
|
670
|
-
// narrowed to whatever branch_ids happen to already exist in the permission table.
|
|
671
690
|
const service = buildService();
|
|
672
691
|
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
673
692
|
mockPermissionRepo.find.mockResolvedValue([
|
package/dtos/permission.dto.d.ts
CHANGED
|
@@ -478,18 +478,11 @@ _ts_decorate([
|
|
|
478
478
|
], UserRoleResponseDto.prototype, "createdAt", void 0);
|
|
479
479
|
export class FrontendActionDto {
|
|
480
480
|
constructor(){
|
|
481
|
-
_define_property(this, "id", void 0);
|
|
482
481
|
_define_property(this, "code", void 0);
|
|
483
482
|
_define_property(this, "name", void 0);
|
|
484
483
|
_define_property(this, "description", void 0);
|
|
485
484
|
}
|
|
486
485
|
}
|
|
487
|
-
_ts_decorate([
|
|
488
|
-
ApiProperty({
|
|
489
|
-
description: 'Action ID'
|
|
490
|
-
}),
|
|
491
|
-
_ts_metadata("design:type", String)
|
|
492
|
-
], FrontendActionDto.prototype, "id", void 0);
|
|
493
486
|
_ts_decorate([
|
|
494
487
|
ApiProperty({
|
|
495
488
|
description: 'Action code'
|
|
@@ -149,7 +149,6 @@ export class PermissionCacheService {
|
|
|
149
149
|
}
|
|
150
150
|
return {
|
|
151
151
|
frontendActions: frontendActions.map((a)=>({
|
|
152
|
-
id: a.id,
|
|
153
152
|
code: a.code,
|
|
154
153
|
name: a.name,
|
|
155
154
|
description: a.description
|
|
@@ -375,7 +374,8 @@ export class PermissionCacheService {
|
|
|
375
374
|
} catch {
|
|
376
375
|
trackedKeys = [];
|
|
377
376
|
}
|
|
378
|
-
|
|
377
|
+
const hasCompanyWideScope = branchIds?.some((branchId)=>!branchId);
|
|
378
|
+
if (!branchIds?.length || hasCompanyWideScope) {
|
|
379
379
|
trackedKeys.forEach((key)=>keysToDelete.add(key));
|
|
380
380
|
keysToDelete.add(trackedKeysKey);
|
|
381
381
|
await untrackScope(trackedKeysKey);
|
|
@@ -207,8 +207,6 @@ describe('PermissionCacheService', ()=>{
|
|
|
207
207
|
expect(mockCacheManager.del).not.toHaveBeenCalledWith(expect.stringContaining('permission-tracked-scopes'));
|
|
208
208
|
});
|
|
209
209
|
it('invalidates only the requested branch keys, keeping remaining branches tracked', async ()=>{
|
|
210
|
-
// company-scoped key format only differentiates by branchId when the guard config
|
|
211
|
-
// enables the company feature — otherwise every branch maps to the same cache key.
|
|
212
210
|
const guardConfig = {
|
|
213
211
|
enableCompanyFeature: true
|
|
214
212
|
};
|
|
@@ -225,18 +223,15 @@ describe('PermissionCacheService', ()=>{
|
|
|
225
223
|
];
|
|
226
224
|
return undefined;
|
|
227
225
|
});
|
|
228
|
-
// companyId given directly invokes invalidateScope (bypasses the tracked-scopes fan-out)
|
|
229
226
|
await service.invalidateUser('user-1', 'company-1', [
|
|
230
227
|
'branch-a'
|
|
231
228
|
]);
|
|
232
|
-
// the specific branch key gets deleted
|
|
233
229
|
const deletedKeys = mockCacheManager.del.mock.calls.map(([key])=>key);
|
|
234
230
|
expect(deletedKeys).toContain(buildPermissionCacheKey({
|
|
235
231
|
userId: 'user-1',
|
|
236
232
|
companyId: 'company-1',
|
|
237
233
|
branchId: 'branch-a'
|
|
238
234
|
}, guardConfig));
|
|
239
|
-
// remaining tracked keys get re-saved rather than the tracked-keys index being wiped
|
|
240
235
|
expect(mockCacheManager.set).toHaveBeenCalledWith(expect.stringContaining('permission-tracked-keys'), expect.arrayContaining([
|
|
241
236
|
keptKey,
|
|
242
237
|
'some-other-tracked-key'
|
|
@@ -265,6 +260,37 @@ describe('PermissionCacheService', ()=>{
|
|
|
265
260
|
expect(deletedKeys).toContain(onlyKey);
|
|
266
261
|
expect(deletedKeys.some((k)=>String(k).includes('permission-tracked-keys'))).toBe(true);
|
|
267
262
|
});
|
|
263
|
+
it('wipes every tracked branch key when a null branchId (company-wide grant) is in the scope list', async ()=>{
|
|
264
|
+
const guardConfig = {
|
|
265
|
+
enableCompanyFeature: true
|
|
266
|
+
};
|
|
267
|
+
const service = buildService(guardConfig);
|
|
268
|
+
const branchAKey = buildPermissionCacheKey({
|
|
269
|
+
userId: 'user-1',
|
|
270
|
+
companyId: 'company-1',
|
|
271
|
+
branchId: 'branch-a'
|
|
272
|
+
}, guardConfig);
|
|
273
|
+
const branchBKey = buildPermissionCacheKey({
|
|
274
|
+
userId: 'user-1',
|
|
275
|
+
companyId: 'company-1',
|
|
276
|
+
branchId: 'branch-b'
|
|
277
|
+
}, guardConfig);
|
|
278
|
+
const trackedKeysKey = 'permission-tracked-keys:user:user-1:company:company-1';
|
|
279
|
+
mockCacheManager.get.mockImplementation(async (key)=>{
|
|
280
|
+
if (key.includes('permission-tracked-keys')) return [
|
|
281
|
+
branchAKey,
|
|
282
|
+
branchBKey
|
|
283
|
+
];
|
|
284
|
+
return undefined;
|
|
285
|
+
});
|
|
286
|
+
await service.invalidateUser('user-1', 'company-1', [
|
|
287
|
+
null
|
|
288
|
+
]);
|
|
289
|
+
const deletedKeys = mockCacheManager.del.mock.calls.map(([key])=>key);
|
|
290
|
+
expect(deletedKeys).toContain(branchAKey);
|
|
291
|
+
expect(deletedKeys).toContain(branchBKey);
|
|
292
|
+
expect(deletedKeys.some((k)=>String(k).includes(trackedKeysKey))).toBe(true);
|
|
293
|
+
});
|
|
268
294
|
it('without a companyId, invalidates every tracked scope for the user and clears the scopes index', async ()=>{
|
|
269
295
|
const service = buildService();
|
|
270
296
|
// scopes list mixes a nested tracked-keys index (company scope) with a raw cache key (untracked scope)
|
|
@@ -396,7 +422,7 @@ describe('PermissionCacheService', ()=>{
|
|
|
396
422
|
'user'
|
|
397
423
|
]);
|
|
398
424
|
expect(result.frontendActions).toHaveLength(1);
|
|
399
|
-
expect(result.frontendActions[0].
|
|
425
|
+
expect(result.frontendActions[0].code).toBe('user.view');
|
|
400
426
|
});
|
|
401
427
|
it('returns an empty frontendActions list when parentCodes resolve to no known parents', async ()=>{
|
|
402
428
|
const service = buildService();
|
|
@@ -559,7 +585,6 @@ describe('PermissionCacheService', ()=>{
|
|
|
559
585
|
expect(result.cachedEndpoints).toBe(1);
|
|
560
586
|
expect(result.frontendActions).toEqual([
|
|
561
587
|
{
|
|
562
|
-
id: 'action-1',
|
|
563
588
|
code: 'user.view',
|
|
564
589
|
name: 'View Users',
|
|
565
590
|
description: null
|
|
@@ -602,9 +627,6 @@ describe('PermissionCacheService', ()=>{
|
|
|
602
627
|
expect(result).toBe(2);
|
|
603
628
|
});
|
|
604
629
|
it('wipes every tracked cache entry per member instead of only branches with an existing permission row', async ()=>{
|
|
605
|
-
// Company-wide action changes are merged into every branch a member queries, including
|
|
606
|
-
// branches where the member holds no explicit permission row — so invalidation must not
|
|
607
|
-
// be narrowed to whatever branch_ids happen to already exist in the permission table.
|
|
608
630
|
const service = buildService();
|
|
609
631
|
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
610
632
|
mockRawMany(mockPermissionRepo, [
|
|
@@ -661,9 +683,6 @@ describe('PermissionCacheService', ()=>{
|
|
|
661
683
|
expect(result).toBe(1);
|
|
662
684
|
});
|
|
663
685
|
it('wipes every tracked cache entry for role members instead of only branches with an existing permission row', async ()=>{
|
|
664
|
-
// Role actions are merged into every branch a member queries, including branches
|
|
665
|
-
// where the member holds no explicit permission row — so invalidation must not be
|
|
666
|
-
// narrowed to whatever branch_ids happen to already exist in the permission table.
|
|
667
686
|
const service = buildService();
|
|
668
687
|
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
669
688
|
mockPermissionRepo.find.mockResolvedValue([
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flusys/nestjs-iam",
|
|
3
|
-
"version": "6.2.
|
|
3
|
+
"version": "6.2.2",
|
|
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.2.
|
|
94
|
-
"@flusys/nestjs-shared": "6.2.
|
|
93
|
+
"@flusys/nestjs-core": "6.2.2",
|
|
94
|
+
"@flusys/nestjs-shared": "6.2.2"
|
|
95
95
|
}
|
|
96
96
|
}
|