@flusys/nestjs-iam 6.1.0 → 6.1.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/controllers/company-action-permission.controller.js +1 -1
- package/cjs/controllers/company-action-permission.controller.spec.js +1 -1
- package/cjs/controllers/user-action-permission.controller.js +1 -1
- package/cjs/controllers/user-action-permission.controller.spec.js +1 -1
- package/cjs/dtos/permission.dto.js +7 -0
- package/cjs/entities/permission-base.entity.js +1 -10
- package/cjs/entities/permission-with-company.entity.js +0 -3
- package/cjs/entities/user-iam-permission.entity.js +0 -3
- package/cjs/modules/iam.module.js +2 -2
- package/cjs/services/action.service.js +4 -4
- package/cjs/services/action.service.spec.js +11 -11
- package/cjs/services/permission-cache.service.js +363 -52
- package/cjs/services/permission-cache.service.spec.js +462 -15
- package/cjs/services/permission.service.js +92 -396
- package/cjs/services/permission.service.spec.js +35 -310
- package/cjs/services/role.service.js +1 -1
- package/cjs/services/role.service.spec.js +4 -7
- package/dtos/permission.dto.d.ts +1 -0
- package/entities/permission-base.entity.d.ts +0 -1
- package/fesm/controllers/company-action-permission.controller.js +1 -1
- package/fesm/controllers/company-action-permission.controller.spec.js +1 -1
- package/fesm/controllers/user-action-permission.controller.js +1 -1
- package/fesm/controllers/user-action-permission.controller.spec.js +1 -1
- package/fesm/dtos/permission.dto.js +7 -0
- package/fesm/entities/permission-base.entity.js +1 -10
- package/fesm/entities/permission-with-company.entity.js +0 -3
- package/fesm/entities/user-iam-permission.entity.js +0 -3
- package/fesm/modules/iam.module.js +4 -4
- package/fesm/services/action.service.js +4 -4
- package/fesm/services/action.service.spec.js +11 -11
- package/fesm/services/permission-cache.service.js +365 -51
- package/fesm/services/permission-cache.service.spec.js +462 -15
- package/fesm/services/permission.service.js +92 -396
- package/fesm/services/permission.service.spec.js +35 -310
- package/fesm/services/role.service.js +1 -1
- package/fesm/services/role.service.spec.js +4 -7
- package/package.json +3 -3
- package/services/permission-cache.service.d.ts +20 -3
- package/services/permission.service.d.ts +2 -16
|
@@ -21,7 +21,6 @@ function buildPermissionRow(overrides = {}) {
|
|
|
21
21
|
sourceId: 'user-1',
|
|
22
22
|
targetType: IamEntityType.ACTION,
|
|
23
23
|
targetId: 'action-1',
|
|
24
|
-
userId: 'user-1',
|
|
25
24
|
companyId: null,
|
|
26
25
|
branchId: null,
|
|
27
26
|
validFrom: null,
|
|
@@ -95,8 +94,9 @@ describe('PermissionService', ()=>{
|
|
|
95
94
|
mockPermissionCacheService = {
|
|
96
95
|
invalidateUser: jest.fn(),
|
|
97
96
|
invalidateUsers: jest.fn(),
|
|
98
|
-
|
|
99
|
-
|
|
97
|
+
invalidateRoleMembersCache: jest.fn(),
|
|
98
|
+
invalidateCompanyMembersCache: jest.fn(),
|
|
99
|
+
getMyPermissionsResponse: jest.fn()
|
|
100
100
|
};
|
|
101
101
|
const module = await Test.createTestingModule({
|
|
102
102
|
providers: [
|
|
@@ -168,9 +168,7 @@ describe('PermissionService', ()=>{
|
|
|
168
168
|
removed: 1,
|
|
169
169
|
total: 3
|
|
170
170
|
});
|
|
171
|
-
expect(mockPermissionCacheService.invalidateUser).toHaveBeenCalledWith('user-1', null, [
|
|
172
|
-
null
|
|
173
|
-
]);
|
|
171
|
+
expect(mockPermissionCacheService.invalidateUser).toHaveBeenCalledWith('user-1', null, []);
|
|
174
172
|
});
|
|
175
173
|
it('scopes new permissions and cache invalidation to the company/branch when the company feature is enabled', async ()=>{
|
|
176
174
|
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
@@ -241,7 +239,7 @@ describe('PermissionService', ()=>{
|
|
|
241
239
|
describe('getUserActions', ()=>{
|
|
242
240
|
it('returns an empty array without querying actions when the user has no permissions', async ()=>{
|
|
243
241
|
mockPermissionRepo.find.mockResolvedValue([]);
|
|
244
|
-
const result = await service.getUserActions('user-1', undefined);
|
|
242
|
+
const result = await service.getUserActions('user-1', undefined, undefined);
|
|
245
243
|
expect(result).toEqual([]);
|
|
246
244
|
expect(mockActionRepo.find).not.toHaveBeenCalled();
|
|
247
245
|
});
|
|
@@ -260,7 +258,7 @@ describe('PermissionService', ()=>{
|
|
|
260
258
|
id: 'action-1'
|
|
261
259
|
})
|
|
262
260
|
]);
|
|
263
|
-
const result = await service.getUserActions('user-1', undefined);
|
|
261
|
+
const result = await service.getUserActions('user-1', undefined, undefined);
|
|
264
262
|
expect(result).toHaveLength(1);
|
|
265
263
|
expect(result[0]).toEqual(expect.objectContaining({
|
|
266
264
|
actionId: 'action-1',
|
|
@@ -270,7 +268,7 @@ describe('PermissionService', ()=>{
|
|
|
270
268
|
it('filters by companyId/branchId when the company feature is enabled', async ()=>{
|
|
271
269
|
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
272
270
|
mockPermissionRepo.find.mockResolvedValue([]);
|
|
273
|
-
await service.getUserActions('user-1', '
|
|
271
|
+
await service.getUserActions('user-1', 'company-1', 'branch-1');
|
|
274
272
|
expect(mockPermissionRepo.find).toHaveBeenCalledWith({
|
|
275
273
|
where: expect.objectContaining({
|
|
276
274
|
companyId: 'company-1',
|
|
@@ -332,31 +330,18 @@ describe('PermissionService', ()=>{
|
|
|
332
330
|
});
|
|
333
331
|
});
|
|
334
332
|
it('removes role actions and reports counts and invalidates role member caches', async ()=>{
|
|
335
|
-
mockPermissionRepo.find.
|
|
336
|
-
.mockResolvedValueOnce([
|
|
337
|
-
buildPermissionRow({
|
|
338
|
-
sourceId: 'user-9'
|
|
339
|
-
})
|
|
340
|
-
]); // invalidateRoleMembersCache lookup (sourceId = the user who holds the role)
|
|
333
|
+
mockPermissionRepo.find.mockResolvedValue([]); // existing check for add
|
|
341
334
|
mockPermissionRepo.save.mockResolvedValue([
|
|
342
335
|
{}
|
|
343
336
|
]);
|
|
344
337
|
mockPermissionRepo.delete.mockResolvedValue(buildDeleteResult(1));
|
|
345
|
-
mockRoleRepo.findOne.mockResolvedValue({
|
|
346
|
-
id: 'role-1',
|
|
347
|
-
companyId: null
|
|
348
|
-
});
|
|
349
338
|
const result = await service.assignRoleActions(dto);
|
|
350
339
|
expect(result).toEqual({
|
|
351
340
|
added: 1,
|
|
352
341
|
removed: 1,
|
|
353
342
|
total: 2
|
|
354
343
|
});
|
|
355
|
-
expect(mockPermissionCacheService.
|
|
356
|
-
'user-9'
|
|
357
|
-
], null, [
|
|
358
|
-
null
|
|
359
|
-
]);
|
|
344
|
+
expect(mockPermissionCacheService.invalidateRoleMembersCache).toHaveBeenCalledWith('role-1');
|
|
360
345
|
});
|
|
361
346
|
});
|
|
362
347
|
describe('getRoleActions', ()=>{
|
|
@@ -395,7 +380,9 @@ describe('PermissionService', ()=>{
|
|
|
395
380
|
return mockPermissionRepo;
|
|
396
381
|
})
|
|
397
382
|
};
|
|
398
|
-
|
|
383
|
+
mockDataSourceProvider.getDataSource.mockResolvedValue({
|
|
384
|
+
transaction: jest.fn().mockImplementation((cb)=>cb(fakeManager))
|
|
385
|
+
});
|
|
399
386
|
return fakeManager;
|
|
400
387
|
}
|
|
401
388
|
it('adds new company-whitelisted actions inside a transaction, skipping duplicates', async ()=>{
|
|
@@ -429,7 +416,7 @@ describe('PermissionService', ()=>{
|
|
|
429
416
|
})
|
|
430
417
|
]);
|
|
431
418
|
expect(result.added).toBe(1);
|
|
432
|
-
expect(mockPermissionCacheService.
|
|
419
|
+
expect(mockPermissionCacheService.invalidateCompanyMembersCache).toHaveBeenCalledWith('company-1');
|
|
433
420
|
});
|
|
434
421
|
it('cascades removal to role-action and user-action permissions for the company', async ()=>{
|
|
435
422
|
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
@@ -497,7 +484,7 @@ describe('PermissionService', ()=>{
|
|
|
497
484
|
targetId: 'action-2'
|
|
498
485
|
}
|
|
499
486
|
]);
|
|
500
|
-
const result = await service.
|
|
487
|
+
const result = await service.getCompanyActions('company-1', true);
|
|
501
488
|
expect(result).toEqual([
|
|
502
489
|
'action-1',
|
|
503
490
|
'action-2'
|
|
@@ -584,287 +571,26 @@ describe('PermissionService', ()=>{
|
|
|
584
571
|
});
|
|
585
572
|
// ==================== My Permissions / permission-mode branching ====================
|
|
586
573
|
describe('getMyPermissions', ()=>{
|
|
587
|
-
it('
|
|
588
|
-
|
|
574
|
+
it('delegates to the cache service and returns its response', async ()=>{
|
|
575
|
+
const response = {
|
|
589
576
|
frontendActions: [
|
|
590
577
|
{
|
|
591
578
|
id: 'action-1',
|
|
592
579
|
code: 'user.view',
|
|
593
580
|
name: 'View',
|
|
594
|
-
description: null
|
|
595
|
-
parentId: null
|
|
596
|
-
}
|
|
597
|
-
],
|
|
598
|
-
backendCodes: [
|
|
599
|
-
'user.read'
|
|
600
|
-
]
|
|
601
|
-
});
|
|
602
|
-
const result = await service.getMyPermissions('user-1', null, 'company-1');
|
|
603
|
-
expect(result.frontendActions).toHaveLength(1);
|
|
604
|
-
expect(result.cachedEndpoints).toBe(1);
|
|
605
|
-
expect(mockPermissionRepo.find).not.toHaveBeenCalled();
|
|
606
|
-
});
|
|
607
|
-
it('fetches from the database and caches the result on a cache miss', async ()=>{
|
|
608
|
-
mockPermissionCacheService.getMyPermissions.mockResolvedValue(null);
|
|
609
|
-
mockPermissionRepo.find.mockResolvedValue([]);
|
|
610
|
-
const result = await service.getMyPermissions('user-1', null, null);
|
|
611
|
-
expect(result).toEqual({
|
|
612
|
-
frontendActions: [],
|
|
613
|
-
cachedEndpoints: 0
|
|
614
|
-
});
|
|
615
|
-
expect(mockPermissionCacheService.setMyPermissions).toHaveBeenCalled();
|
|
616
|
-
});
|
|
617
|
-
it('filters frontend actions by parentCodes when provided', async ()=>{
|
|
618
|
-
mockPermissionCacheService.getMyPermissions.mockResolvedValue({
|
|
619
|
-
frontendActions: [
|
|
620
|
-
{
|
|
621
|
-
id: 'child-1',
|
|
622
|
-
code: 'user.view',
|
|
623
|
-
name: 'View',
|
|
624
|
-
description: null,
|
|
625
|
-
parentId: 'parent-1'
|
|
626
|
-
},
|
|
627
|
-
{
|
|
628
|
-
id: 'child-2',
|
|
629
|
-
code: 'other.view',
|
|
630
|
-
name: 'View 2',
|
|
631
|
-
description: null,
|
|
632
|
-
parentId: 'other-parent'
|
|
581
|
+
description: null
|
|
633
582
|
}
|
|
634
583
|
],
|
|
635
|
-
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
code: 'user'
|
|
641
|
-
}
|
|
642
|
-
]);
|
|
643
|
-
const result = await service.getMyPermissions('user-1', null, null, [
|
|
644
|
-
'user'
|
|
645
|
-
]);
|
|
646
|
-
expect(result.frontendActions).toHaveLength(1);
|
|
647
|
-
expect(result.frontendActions[0].id).toBe('child-1');
|
|
648
|
-
});
|
|
649
|
-
it('returns an empty frontendActions list when parentCodes resolve to no known parents', async ()=>{
|
|
650
|
-
mockPermissionCacheService.getMyPermissions.mockResolvedValue({
|
|
651
|
-
frontendActions: [
|
|
652
|
-
{
|
|
653
|
-
id: 'child-1',
|
|
654
|
-
code: 'user.view',
|
|
655
|
-
name: 'View',
|
|
656
|
-
description: null,
|
|
657
|
-
parentId: 'parent-1'
|
|
658
|
-
}
|
|
659
|
-
],
|
|
660
|
-
backendCodes: []
|
|
661
|
-
});
|
|
662
|
-
mockActionRepo.find.mockResolvedValue([]);
|
|
663
|
-
const result = await service.getMyPermissions('user-1', null, null, [
|
|
664
|
-
'unknown-parent-code'
|
|
665
|
-
]);
|
|
666
|
-
expect(result.frontendActions).toEqual([]);
|
|
667
|
-
});
|
|
668
|
-
});
|
|
669
|
-
describe('collectAllActionIds (permission-mode branching)', ()=>{
|
|
670
|
-
it('RBAC mode: collects only role-derived action ids, ignoring direct user actions', async ()=>{
|
|
671
|
-
mockIamConfigService.getPermissionMode.mockReturnValue(IAMPermissionMode.RBAC);
|
|
672
|
-
mockPermissionRepo.find.mockImplementation(async ({ where })=>{
|
|
673
|
-
if (where.permissionType === IamPermissionType.USER_ROLE) {
|
|
674
|
-
return [
|
|
675
|
-
buildPermissionRow({
|
|
676
|
-
permissionType: IamPermissionType.USER_ROLE,
|
|
677
|
-
targetId: 'role-1'
|
|
678
|
-
})
|
|
679
|
-
];
|
|
680
|
-
}
|
|
681
|
-
if (where.permissionType === IamPermissionType.ROLE_ACTION) {
|
|
682
|
-
return [
|
|
683
|
-
buildPermissionRow({
|
|
684
|
-
permissionType: IamPermissionType.ROLE_ACTION,
|
|
685
|
-
targetId: 'action-role-1'
|
|
686
|
-
})
|
|
687
|
-
];
|
|
688
|
-
}
|
|
689
|
-
if (where.permissionType === IamPermissionType.USER_ACTION) {
|
|
690
|
-
throw new Error('DIRECT lookup should not run in RBAC mode');
|
|
691
|
-
}
|
|
692
|
-
return [];
|
|
693
|
-
});
|
|
694
|
-
const result = await service.collectAllActionIds('user-1', null, null);
|
|
695
|
-
expect(Array.from(result)).toEqual([
|
|
696
|
-
'action-role-1'
|
|
697
|
-
]);
|
|
698
|
-
});
|
|
699
|
-
it('DIRECT mode: collects only directly-assigned user action ids, ignoring roles', async ()=>{
|
|
700
|
-
mockIamConfigService.getPermissionMode.mockReturnValue(IAMPermissionMode.DIRECT);
|
|
701
|
-
mockPermissionRepo.find.mockImplementation(async ({ where })=>{
|
|
702
|
-
if (where.permissionType === IamPermissionType.USER_ROLE) {
|
|
703
|
-
throw new Error('RBAC lookup should not run in DIRECT mode');
|
|
704
|
-
}
|
|
705
|
-
if (where.permissionType === IamPermissionType.USER_ACTION) {
|
|
706
|
-
return [
|
|
707
|
-
buildPermissionRow({
|
|
708
|
-
permissionType: IamPermissionType.USER_ACTION,
|
|
709
|
-
targetId: 'action-direct-1'
|
|
710
|
-
})
|
|
711
|
-
];
|
|
712
|
-
}
|
|
713
|
-
return [];
|
|
714
|
-
});
|
|
715
|
-
const result = await service.collectAllActionIds('user-1', null, null);
|
|
716
|
-
expect(Array.from(result)).toEqual([
|
|
717
|
-
'action-direct-1'
|
|
718
|
-
]);
|
|
719
|
-
});
|
|
720
|
-
it('FULL mode: merges role-derived and direct action ids, de-duplicating overlaps', async ()=>{
|
|
721
|
-
mockIamConfigService.getPermissionMode.mockReturnValue(IAMPermissionMode.FULL);
|
|
722
|
-
mockPermissionRepo.find.mockImplementation(async ({ where })=>{
|
|
723
|
-
if (where.permissionType === IamPermissionType.USER_ROLE) {
|
|
724
|
-
return [
|
|
725
|
-
buildPermissionRow({
|
|
726
|
-
permissionType: IamPermissionType.USER_ROLE,
|
|
727
|
-
targetId: 'role-1'
|
|
728
|
-
})
|
|
729
|
-
];
|
|
730
|
-
}
|
|
731
|
-
if (where.permissionType === IamPermissionType.ROLE_ACTION) {
|
|
732
|
-
return [
|
|
733
|
-
buildPermissionRow({
|
|
734
|
-
permissionType: IamPermissionType.ROLE_ACTION,
|
|
735
|
-
targetId: 'action-shared'
|
|
736
|
-
})
|
|
737
|
-
];
|
|
738
|
-
}
|
|
739
|
-
if (where.permissionType === IamPermissionType.USER_ACTION) {
|
|
740
|
-
return [
|
|
741
|
-
buildPermissionRow({
|
|
742
|
-
permissionType: IamPermissionType.USER_ACTION,
|
|
743
|
-
targetId: 'action-shared'
|
|
744
|
-
}),
|
|
745
|
-
buildPermissionRow({
|
|
746
|
-
permissionType: IamPermissionType.USER_ACTION,
|
|
747
|
-
targetId: 'action-direct-only'
|
|
748
|
-
})
|
|
749
|
-
];
|
|
750
|
-
}
|
|
751
|
-
return [];
|
|
752
|
-
});
|
|
753
|
-
const result = await service.collectAllActionIds('user-1', null, null);
|
|
754
|
-
expect(Array.from(result).sort()).toEqual([
|
|
755
|
-
'action-direct-only',
|
|
756
|
-
'action-shared'
|
|
757
|
-
]);
|
|
758
|
-
});
|
|
759
|
-
it('excludes expired/not-yet-valid permission rows via isValid()', async ()=>{
|
|
760
|
-
mockIamConfigService.getPermissionMode.mockReturnValue(IAMPermissionMode.DIRECT);
|
|
761
|
-
const expired = buildPermissionRow({
|
|
762
|
-
permissionType: IamPermissionType.USER_ACTION,
|
|
763
|
-
targetId: 'action-expired',
|
|
764
|
-
validUntil: new Date('2000-01-01')
|
|
765
|
-
});
|
|
766
|
-
const active = buildPermissionRow({
|
|
767
|
-
permissionType: IamPermissionType.USER_ACTION,
|
|
768
|
-
targetId: 'action-active'
|
|
769
|
-
});
|
|
770
|
-
mockPermissionRepo.find.mockResolvedValue([
|
|
771
|
-
expired,
|
|
772
|
-
active
|
|
773
|
-
]);
|
|
774
|
-
const result = await service.collectAllActionIds('user-1', null, null);
|
|
775
|
-
expect(Array.from(result)).toEqual([
|
|
776
|
-
'action-active'
|
|
777
|
-
]);
|
|
778
|
-
});
|
|
779
|
-
});
|
|
780
|
-
describe('applyCompanyWhitelist', ()=>{
|
|
781
|
-
it('is a no-op when the company feature is disabled', async ()=>{
|
|
782
|
-
const actionIds = new Set([
|
|
783
|
-
'action-1',
|
|
784
|
-
'action-2'
|
|
785
|
-
]);
|
|
786
|
-
await service.applyCompanyWhitelist(actionIds, 'company-1');
|
|
787
|
-
expect(actionIds).toEqual(new Set([
|
|
788
|
-
'action-1',
|
|
789
|
-
'action-2'
|
|
790
|
-
]));
|
|
791
|
-
expect(mockPermissionRepo.find).not.toHaveBeenCalled();
|
|
792
|
-
});
|
|
793
|
-
it('is a no-op when companyId is missing even if the company feature is enabled', async ()=>{
|
|
794
|
-
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
795
|
-
const actionIds = new Set([
|
|
796
|
-
'action-1'
|
|
797
|
-
]);
|
|
798
|
-
await service.applyCompanyWhitelist(actionIds, null);
|
|
799
|
-
expect(actionIds).toEqual(new Set([
|
|
800
|
-
'action-1'
|
|
801
|
-
]));
|
|
802
|
-
});
|
|
803
|
-
it('does not filter anything when the company has an empty whitelist', async ()=>{
|
|
804
|
-
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
805
|
-
mockPermissionRepo.find.mockResolvedValue([]);
|
|
806
|
-
const actionIds = new Set([
|
|
807
|
-
'action-1',
|
|
808
|
-
'action-2'
|
|
809
|
-
]);
|
|
810
|
-
await service.applyCompanyWhitelist(actionIds, 'company-1');
|
|
811
|
-
expect(actionIds).toEqual(new Set([
|
|
812
|
-
'action-1',
|
|
813
|
-
'action-2'
|
|
814
|
-
]));
|
|
815
|
-
});
|
|
816
|
-
it('removes action ids that are not present in the company whitelist', async ()=>{
|
|
817
|
-
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
818
|
-
mockPermissionRepo.find.mockResolvedValue([
|
|
819
|
-
{
|
|
820
|
-
targetId: 'action-1'
|
|
821
|
-
}
|
|
822
|
-
]);
|
|
823
|
-
const actionIds = new Set([
|
|
824
|
-
'action-1',
|
|
825
|
-
'action-2'
|
|
826
|
-
]);
|
|
827
|
-
await service.applyCompanyWhitelist(actionIds, 'company-1');
|
|
828
|
-
expect(actionIds).toEqual(new Set([
|
|
829
|
-
'action-1'
|
|
830
|
-
]));
|
|
831
|
-
});
|
|
832
|
-
});
|
|
833
|
-
// ==================== Cache invalidation helpers ====================
|
|
834
|
-
describe('invalidateCompanyMembersCache', ()=>{
|
|
835
|
-
it('returns 0 without querying when the company feature is disabled', async ()=>{
|
|
836
|
-
const result = await service.invalidateCompanyMembersCache('company-1');
|
|
837
|
-
expect(result).toBe(0);
|
|
838
|
-
expect(mockPermissionRepo.createQueryBuilder).not.toHaveBeenCalled();
|
|
839
|
-
});
|
|
840
|
-
it('returns 0 when the company has no members with permissions', async ()=>{
|
|
841
|
-
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
842
|
-
mockRawMany(mockPermissionRepo, []);
|
|
843
|
-
const result = await service.invalidateCompanyMembersCache('company-1');
|
|
844
|
-
expect(result).toBe(0);
|
|
845
|
-
});
|
|
846
|
-
it('invalidates every distinct member user id for the company', async ()=>{
|
|
847
|
-
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
848
|
-
mockRawMany(mockPermissionRepo, [
|
|
849
|
-
{
|
|
850
|
-
userId: 'user-1',
|
|
851
|
-
branchId: 'branch-1'
|
|
852
|
-
},
|
|
853
|
-
{
|
|
854
|
-
userId: 'user-2',
|
|
855
|
-
branchId: null
|
|
856
|
-
}
|
|
584
|
+
cachedEndpoints: 1
|
|
585
|
+
};
|
|
586
|
+
mockPermissionCacheService.getMyPermissionsResponse.mockResolvedValue(response);
|
|
587
|
+
const result = await service.getMyPermissions('user-1', null, 'company-1', [
|
|
588
|
+
'parent-code'
|
|
857
589
|
]);
|
|
858
|
-
mockPermissionCacheService.
|
|
859
|
-
|
|
860
|
-
expect(mockPermissionCacheService.invalidateUsers).toHaveBeenCalledWith([
|
|
861
|
-
'user-1',
|
|
862
|
-
'user-2'
|
|
863
|
-
], 'company-1', [
|
|
864
|
-
'branch-1',
|
|
865
|
-
null
|
|
590
|
+
expect(mockPermissionCacheService.getMyPermissionsResponse).toHaveBeenCalledWith('user-1', null, 'company-1', [
|
|
591
|
+
'parent-code'
|
|
866
592
|
]);
|
|
867
|
-
expect(result).toBe(
|
|
593
|
+
expect(result).toBe(response);
|
|
868
594
|
});
|
|
869
595
|
});
|
|
870
596
|
describe('revokeCompanyPermissions', ()=>{
|
|
@@ -882,10 +608,7 @@ describe('PermissionService', ()=>{
|
|
|
882
608
|
]);
|
|
883
609
|
mockPermissionRepo.find.mockResolvedValue([
|
|
884
610
|
{
|
|
885
|
-
|
|
886
|
-
},
|
|
887
|
-
{
|
|
888
|
-
userId: null
|
|
611
|
+
sourceId: 'user-1'
|
|
889
612
|
}
|
|
890
613
|
]);
|
|
891
614
|
const fakeTxRepo = {
|
|
@@ -894,12 +617,14 @@ describe('PermissionService', ()=>{
|
|
|
894
617
|
const fakeTxRoleRepo = {
|
|
895
618
|
delete: jest.fn().mockResolvedValue(buildDeleteResult(1))
|
|
896
619
|
};
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
620
|
+
mockDataSourceProvider.getDataSource.mockResolvedValue({
|
|
621
|
+
transaction: jest.fn().mockImplementation((cb)=>cb({
|
|
622
|
+
getRepository: jest.fn((target)=>{
|
|
623
|
+
if (target === mockRoleRepo.target) return fakeTxRoleRepo;
|
|
624
|
+
return fakeTxRepo;
|
|
625
|
+
})
|
|
626
|
+
}))
|
|
627
|
+
});
|
|
903
628
|
await service.revokeCompanyPermissions('company-1');
|
|
904
629
|
expect(fakeTxRepo.delete).toHaveBeenCalledWith(expect.objectContaining({
|
|
905
630
|
permissionType: IamPermissionType.COMPANY_ACTION
|
|
@@ -921,7 +646,7 @@ describe('PermissionService', ()=>{
|
|
|
921
646
|
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
922
647
|
mockPermissionRepo.find.mockResolvedValue([
|
|
923
648
|
{
|
|
924
|
-
|
|
649
|
+
sourceId: 'user-1'
|
|
925
650
|
}
|
|
926
651
|
]);
|
|
927
652
|
mockPermissionRepo.delete.mockResolvedValue(buildDeleteResult(1));
|
|
@@ -60,7 +60,7 @@ export class RoleService extends ApiService {
|
|
|
60
60
|
}
|
|
61
61
|
});
|
|
62
62
|
const affectedUserIds = [
|
|
63
|
-
...new Set(userRoleRows.map((row)=>row.
|
|
63
|
+
...new Set(userRoleRows.map((row)=>row.sourceId))
|
|
64
64
|
];
|
|
65
65
|
await permissionRepo.delete({
|
|
66
66
|
permissionType: IamPermissionType.ROLE_ACTION,
|
|
@@ -243,13 +243,10 @@ describe('RoleService', ()=>{
|
|
|
243
243
|
const permissionRepo = createMockRepository();
|
|
244
244
|
permissionRepo.find.mockResolvedValue([
|
|
245
245
|
{
|
|
246
|
-
|
|
246
|
+
sourceId: 'user-1'
|
|
247
247
|
},
|
|
248
248
|
{
|
|
249
|
-
|
|
250
|
-
},
|
|
251
|
-
{
|
|
252
|
-
userId: null
|
|
249
|
+
sourceId: 'user-2'
|
|
253
250
|
}
|
|
254
251
|
]);
|
|
255
252
|
const queryRunner = buildQueryRunner(permissionRepo);
|
|
@@ -286,10 +283,10 @@ describe('RoleService', ()=>{
|
|
|
286
283
|
const permissionRepo = createMockRepository();
|
|
287
284
|
permissionRepo.find.mockResolvedValue([
|
|
288
285
|
{
|
|
289
|
-
|
|
286
|
+
sourceId: 'user-1'
|
|
290
287
|
},
|
|
291
288
|
{
|
|
292
|
-
|
|
289
|
+
sourceId: 'user-1'
|
|
293
290
|
}
|
|
294
291
|
]);
|
|
295
292
|
const queryRunner = buildQueryRunner(permissionRepo);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flusys/nestjs-iam",
|
|
3
|
-
"version": "6.1.
|
|
3
|
+
"version": "6.1.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.1.
|
|
94
|
-
"@flusys/nestjs-shared": "6.1.
|
|
93
|
+
"@flusys/nestjs-core": "6.1.2",
|
|
94
|
+
"@flusys/nestjs-shared": "6.1.2"
|
|
95
95
|
}
|
|
96
96
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { HybridCache, PermissionCacheKeyOptions, PermissionGuardConfig } from '@flusys/nestjs-shared';
|
|
2
|
+
import { MyPermissionsResponseDto } from '../dtos/permission.dto';
|
|
3
|
+
import { IAMConfigService } from './iam-config.service';
|
|
4
|
+
import { IAMDataSourceService } from './iam-datasource.service';
|
|
2
5
|
export { PermissionCacheKeyOptions } from '@flusys/nestjs-shared';
|
|
3
6
|
export interface CachedMyPermissions {
|
|
4
7
|
frontendActions: Array<{
|
|
@@ -10,14 +13,18 @@ export interface CachedMyPermissions {
|
|
|
10
13
|
}>;
|
|
11
14
|
backendCodes: string[];
|
|
12
15
|
}
|
|
13
|
-
export declare const MY_PERMISSIONS_CACHE_PREFIX = "my-permissions";
|
|
14
16
|
export declare class PermissionCacheService {
|
|
15
17
|
private readonly cacheManager;
|
|
18
|
+
private readonly iamConfigService;
|
|
19
|
+
private readonly dataSourceProvider;
|
|
16
20
|
private readonly guardConfig?;
|
|
17
21
|
private readonly TRACKED_KEYS_PREFIX;
|
|
18
22
|
private readonly TRACKED_SCOPES_PREFIX;
|
|
19
23
|
private readonly TTL;
|
|
20
|
-
constructor(cacheManager: HybridCache, guardConfig?: PermissionGuardConfig | undefined);
|
|
24
|
+
constructor(cacheManager: HybridCache, iamConfigService: IAMConfigService, dataSourceProvider: IAMDataSourceService, guardConfig?: PermissionGuardConfig | undefined);
|
|
25
|
+
private getPermissionRepository;
|
|
26
|
+
private getActionRepository;
|
|
27
|
+
private getRoleRepository;
|
|
21
28
|
private parseDurationMs;
|
|
22
29
|
private buildCacheKey;
|
|
23
30
|
private scopeToken;
|
|
@@ -26,7 +33,17 @@ export declare class PermissionCacheService {
|
|
|
26
33
|
setMyPermissions(options: PermissionCacheKeyOptions, data: CachedMyPermissions): Promise<void>;
|
|
27
34
|
getMyPermissions(options: PermissionCacheKeyOptions): Promise<CachedMyPermissions | null>;
|
|
28
35
|
private trackKey;
|
|
36
|
+
getMyPermissionsResponse(userId: string, branchId: string | null, companyId?: string | null, parentCodes?: string[]): Promise<MyPermissionsResponseDto>;
|
|
37
|
+
private buildResponseFromCache;
|
|
38
|
+
private getParentIdsByCodesWithCache;
|
|
39
|
+
private fetchAndCachePermissions;
|
|
40
|
+
private collectAllActionIds;
|
|
41
|
+
private buildAndCachePermissionData;
|
|
42
|
+
private getUserRoleIds;
|
|
43
|
+
private getRoleActionIds;
|
|
44
|
+
private getUserActionIds;
|
|
29
45
|
invalidateUser(userId: string, companyId?: string | null, branchIds?: (string | null)[]): Promise<void>;
|
|
30
|
-
private invalidateScope;
|
|
31
46
|
invalidateUsers(userIds: string[], companyId?: string | null, branchIds?: (string | null)[]): Promise<number>;
|
|
47
|
+
invalidateRoleMembersCache(roleId: string): Promise<number>;
|
|
48
|
+
invalidateCompanyMembersCache(companyId: string): Promise<number>;
|
|
32
49
|
}
|
|
@@ -12,30 +12,16 @@ export declare class PermissionService {
|
|
|
12
12
|
private getRoleRepository;
|
|
13
13
|
private getActionsMap;
|
|
14
14
|
assignUserActions(dto: AssignUserActionsDto): Promise<PermissionOperationResultDto>;
|
|
15
|
-
getUserActions(userId: string,
|
|
15
|
+
getUserActions(userId: string, companyId: string | undefined, branchId: string | undefined): Promise<UserActionResponseDto[]>;
|
|
16
16
|
assignRoleActions(dto: AssignRoleActionsDto): Promise<PermissionOperationResultDto>;
|
|
17
17
|
getRoleActions(roleId: string): Promise<RoleActionResponseDto[]>;
|
|
18
18
|
assignCompanyActions(dto: AssignCompanyActionsDto): Promise<PermissionOperationResultDto>;
|
|
19
|
-
|
|
20
|
-
private removeCompanyActionsWithCascade;
|
|
21
|
-
getCompanyActions(companyId: string): Promise<CompanyActionResponseDto[]>;
|
|
22
|
-
getCompanyActionIds(companyId: string): Promise<string[]>;
|
|
19
|
+
getCompanyActions(companyId: string, isOnlyId?: boolean): Promise<CompanyActionResponseDto[] | string[]>;
|
|
23
20
|
assignUserRoles(dto: AssignUserRolesDto): Promise<PermissionOperationResultDto>;
|
|
24
21
|
getUserRoles(userId: string, branchId?: string | undefined, companyId?: string | undefined): Promise<UserRoleResponseDto[]>;
|
|
25
22
|
getMyPermissions(userId: string, branchId: string | null, companyId?: string | null, parentCodes?: string[]): Promise<MyPermissionsResponseDto>;
|
|
26
|
-
private buildResponseFromCache;
|
|
27
|
-
private getParentIdsByCodesWithCache;
|
|
28
|
-
private fetchAndCachePermissions;
|
|
29
|
-
private collectAllActionIds;
|
|
30
|
-
private applyCompanyWhitelist;
|
|
31
|
-
private buildAndCachePermissionData;
|
|
32
23
|
private splitItemsByAction;
|
|
33
24
|
private buildOperationResult;
|
|
34
|
-
private getUserRoleIds;
|
|
35
|
-
private getRoleActionIds;
|
|
36
|
-
private getUserActionIds;
|
|
37
|
-
private invalidateRoleMembersCache;
|
|
38
|
-
invalidateCompanyMembersCache(companyId: string): Promise<number>;
|
|
39
25
|
revokeCompanyPermissions(companyId: string): Promise<void>;
|
|
40
26
|
revokeBranchPermissions(branchId: string, companyId: string): Promise<void>;
|
|
41
27
|
revokeUserCompanyAccess(userId: string, companyId: string): Promise<void>;
|