@flusys/nestjs-iam 6.1.0 → 6.1.1
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
|
@@ -25,7 +25,6 @@ function buildPermissionRow(overrides = {}) {
|
|
|
25
25
|
sourceId: 'user-1',
|
|
26
26
|
targetType: _iamentitytypeenum.IamEntityType.ACTION,
|
|
27
27
|
targetId: 'action-1',
|
|
28
|
-
userId: 'user-1',
|
|
29
28
|
companyId: null,
|
|
30
29
|
branchId: null,
|
|
31
30
|
validFrom: null,
|
|
@@ -99,8 +98,9 @@ describe('PermissionService', ()=>{
|
|
|
99
98
|
mockPermissionCacheService = {
|
|
100
99
|
invalidateUser: jest.fn(),
|
|
101
100
|
invalidateUsers: jest.fn(),
|
|
102
|
-
|
|
103
|
-
|
|
101
|
+
invalidateRoleMembersCache: jest.fn(),
|
|
102
|
+
invalidateCompanyMembersCache: jest.fn(),
|
|
103
|
+
getMyPermissionsResponse: jest.fn()
|
|
104
104
|
};
|
|
105
105
|
const module = await _testing.Test.createTestingModule({
|
|
106
106
|
providers: [
|
|
@@ -172,9 +172,7 @@ describe('PermissionService', ()=>{
|
|
|
172
172
|
removed: 1,
|
|
173
173
|
total: 3
|
|
174
174
|
});
|
|
175
|
-
expect(mockPermissionCacheService.invalidateUser).toHaveBeenCalledWith('user-1', null, [
|
|
176
|
-
null
|
|
177
|
-
]);
|
|
175
|
+
expect(mockPermissionCacheService.invalidateUser).toHaveBeenCalledWith('user-1', null, []);
|
|
178
176
|
});
|
|
179
177
|
it('scopes new permissions and cache invalidation to the company/branch when the company feature is enabled', async ()=>{
|
|
180
178
|
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
@@ -245,7 +243,7 @@ describe('PermissionService', ()=>{
|
|
|
245
243
|
describe('getUserActions', ()=>{
|
|
246
244
|
it('returns an empty array without querying actions when the user has no permissions', async ()=>{
|
|
247
245
|
mockPermissionRepo.find.mockResolvedValue([]);
|
|
248
|
-
const result = await service.getUserActions('user-1', undefined);
|
|
246
|
+
const result = await service.getUserActions('user-1', undefined, undefined);
|
|
249
247
|
expect(result).toEqual([]);
|
|
250
248
|
expect(mockActionRepo.find).not.toHaveBeenCalled();
|
|
251
249
|
});
|
|
@@ -264,7 +262,7 @@ describe('PermissionService', ()=>{
|
|
|
264
262
|
id: 'action-1'
|
|
265
263
|
})
|
|
266
264
|
]);
|
|
267
|
-
const result = await service.getUserActions('user-1', undefined);
|
|
265
|
+
const result = await service.getUserActions('user-1', undefined, undefined);
|
|
268
266
|
expect(result).toHaveLength(1);
|
|
269
267
|
expect(result[0]).toEqual(expect.objectContaining({
|
|
270
268
|
actionId: 'action-1',
|
|
@@ -274,7 +272,7 @@ describe('PermissionService', ()=>{
|
|
|
274
272
|
it('filters by companyId/branchId when the company feature is enabled', async ()=>{
|
|
275
273
|
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
276
274
|
mockPermissionRepo.find.mockResolvedValue([]);
|
|
277
|
-
await service.getUserActions('user-1', '
|
|
275
|
+
await service.getUserActions('user-1', 'company-1', 'branch-1');
|
|
278
276
|
expect(mockPermissionRepo.find).toHaveBeenCalledWith({
|
|
279
277
|
where: expect.objectContaining({
|
|
280
278
|
companyId: 'company-1',
|
|
@@ -336,31 +334,18 @@ describe('PermissionService', ()=>{
|
|
|
336
334
|
});
|
|
337
335
|
});
|
|
338
336
|
it('removes role actions and reports counts and invalidates role member caches', async ()=>{
|
|
339
|
-
mockPermissionRepo.find.
|
|
340
|
-
.mockResolvedValueOnce([
|
|
341
|
-
buildPermissionRow({
|
|
342
|
-
sourceId: 'user-9'
|
|
343
|
-
})
|
|
344
|
-
]); // invalidateRoleMembersCache lookup (sourceId = the user who holds the role)
|
|
337
|
+
mockPermissionRepo.find.mockResolvedValue([]); // existing check for add
|
|
345
338
|
mockPermissionRepo.save.mockResolvedValue([
|
|
346
339
|
{}
|
|
347
340
|
]);
|
|
348
341
|
mockPermissionRepo.delete.mockResolvedValue(buildDeleteResult(1));
|
|
349
|
-
mockRoleRepo.findOne.mockResolvedValue({
|
|
350
|
-
id: 'role-1',
|
|
351
|
-
companyId: null
|
|
352
|
-
});
|
|
353
342
|
const result = await service.assignRoleActions(dto);
|
|
354
343
|
expect(result).toEqual({
|
|
355
344
|
added: 1,
|
|
356
345
|
removed: 1,
|
|
357
346
|
total: 2
|
|
358
347
|
});
|
|
359
|
-
expect(mockPermissionCacheService.
|
|
360
|
-
'user-9'
|
|
361
|
-
], null, [
|
|
362
|
-
null
|
|
363
|
-
]);
|
|
348
|
+
expect(mockPermissionCacheService.invalidateRoleMembersCache).toHaveBeenCalledWith('role-1');
|
|
364
349
|
});
|
|
365
350
|
});
|
|
366
351
|
describe('getRoleActions', ()=>{
|
|
@@ -399,7 +384,9 @@ describe('PermissionService', ()=>{
|
|
|
399
384
|
return mockPermissionRepo;
|
|
400
385
|
})
|
|
401
386
|
};
|
|
402
|
-
|
|
387
|
+
mockDataSourceProvider.getDataSource.mockResolvedValue({
|
|
388
|
+
transaction: jest.fn().mockImplementation((cb)=>cb(fakeManager))
|
|
389
|
+
});
|
|
403
390
|
return fakeManager;
|
|
404
391
|
}
|
|
405
392
|
it('adds new company-whitelisted actions inside a transaction, skipping duplicates', async ()=>{
|
|
@@ -433,7 +420,7 @@ describe('PermissionService', ()=>{
|
|
|
433
420
|
})
|
|
434
421
|
]);
|
|
435
422
|
expect(result.added).toBe(1);
|
|
436
|
-
expect(mockPermissionCacheService.
|
|
423
|
+
expect(mockPermissionCacheService.invalidateCompanyMembersCache).toHaveBeenCalledWith('company-1');
|
|
437
424
|
});
|
|
438
425
|
it('cascades removal to role-action and user-action permissions for the company', async ()=>{
|
|
439
426
|
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
@@ -501,7 +488,7 @@ describe('PermissionService', ()=>{
|
|
|
501
488
|
targetId: 'action-2'
|
|
502
489
|
}
|
|
503
490
|
]);
|
|
504
|
-
const result = await service.
|
|
491
|
+
const result = await service.getCompanyActions('company-1', true);
|
|
505
492
|
expect(result).toEqual([
|
|
506
493
|
'action-1',
|
|
507
494
|
'action-2'
|
|
@@ -588,287 +575,26 @@ describe('PermissionService', ()=>{
|
|
|
588
575
|
});
|
|
589
576
|
// ==================== My Permissions / permission-mode branching ====================
|
|
590
577
|
describe('getMyPermissions', ()=>{
|
|
591
|
-
it('
|
|
592
|
-
|
|
578
|
+
it('delegates to the cache service and returns its response', async ()=>{
|
|
579
|
+
const response = {
|
|
593
580
|
frontendActions: [
|
|
594
581
|
{
|
|
595
582
|
id: 'action-1',
|
|
596
583
|
code: 'user.view',
|
|
597
584
|
name: 'View',
|
|
598
|
-
description: null
|
|
599
|
-
parentId: null
|
|
600
|
-
}
|
|
601
|
-
],
|
|
602
|
-
backendCodes: [
|
|
603
|
-
'user.read'
|
|
604
|
-
]
|
|
605
|
-
});
|
|
606
|
-
const result = await service.getMyPermissions('user-1', null, 'company-1');
|
|
607
|
-
expect(result.frontendActions).toHaveLength(1);
|
|
608
|
-
expect(result.cachedEndpoints).toBe(1);
|
|
609
|
-
expect(mockPermissionRepo.find).not.toHaveBeenCalled();
|
|
610
|
-
});
|
|
611
|
-
it('fetches from the database and caches the result on a cache miss', async ()=>{
|
|
612
|
-
mockPermissionCacheService.getMyPermissions.mockResolvedValue(null);
|
|
613
|
-
mockPermissionRepo.find.mockResolvedValue([]);
|
|
614
|
-
const result = await service.getMyPermissions('user-1', null, null);
|
|
615
|
-
expect(result).toEqual({
|
|
616
|
-
frontendActions: [],
|
|
617
|
-
cachedEndpoints: 0
|
|
618
|
-
});
|
|
619
|
-
expect(mockPermissionCacheService.setMyPermissions).toHaveBeenCalled();
|
|
620
|
-
});
|
|
621
|
-
it('filters frontend actions by parentCodes when provided', async ()=>{
|
|
622
|
-
mockPermissionCacheService.getMyPermissions.mockResolvedValue({
|
|
623
|
-
frontendActions: [
|
|
624
|
-
{
|
|
625
|
-
id: 'child-1',
|
|
626
|
-
code: 'user.view',
|
|
627
|
-
name: 'View',
|
|
628
|
-
description: null,
|
|
629
|
-
parentId: 'parent-1'
|
|
630
|
-
},
|
|
631
|
-
{
|
|
632
|
-
id: 'child-2',
|
|
633
|
-
code: 'other.view',
|
|
634
|
-
name: 'View 2',
|
|
635
|
-
description: null,
|
|
636
|
-
parentId: 'other-parent'
|
|
585
|
+
description: null
|
|
637
586
|
}
|
|
638
587
|
],
|
|
639
|
-
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
code: 'user'
|
|
645
|
-
}
|
|
646
|
-
]);
|
|
647
|
-
const result = await service.getMyPermissions('user-1', null, null, [
|
|
648
|
-
'user'
|
|
649
|
-
]);
|
|
650
|
-
expect(result.frontendActions).toHaveLength(1);
|
|
651
|
-
expect(result.frontendActions[0].id).toBe('child-1');
|
|
652
|
-
});
|
|
653
|
-
it('returns an empty frontendActions list when parentCodes resolve to no known parents', async ()=>{
|
|
654
|
-
mockPermissionCacheService.getMyPermissions.mockResolvedValue({
|
|
655
|
-
frontendActions: [
|
|
656
|
-
{
|
|
657
|
-
id: 'child-1',
|
|
658
|
-
code: 'user.view',
|
|
659
|
-
name: 'View',
|
|
660
|
-
description: null,
|
|
661
|
-
parentId: 'parent-1'
|
|
662
|
-
}
|
|
663
|
-
],
|
|
664
|
-
backendCodes: []
|
|
665
|
-
});
|
|
666
|
-
mockActionRepo.find.mockResolvedValue([]);
|
|
667
|
-
const result = await service.getMyPermissions('user-1', null, null, [
|
|
668
|
-
'unknown-parent-code'
|
|
669
|
-
]);
|
|
670
|
-
expect(result.frontendActions).toEqual([]);
|
|
671
|
-
});
|
|
672
|
-
});
|
|
673
|
-
describe('collectAllActionIds (permission-mode branching)', ()=>{
|
|
674
|
-
it('RBAC mode: collects only role-derived action ids, ignoring direct user actions', async ()=>{
|
|
675
|
-
mockIamConfigService.getPermissionMode.mockReturnValue(_permissiontypeenum.IAMPermissionMode.RBAC);
|
|
676
|
-
mockPermissionRepo.find.mockImplementation(async ({ where })=>{
|
|
677
|
-
if (where.permissionType === _iampermissiontypeenum.IamPermissionType.USER_ROLE) {
|
|
678
|
-
return [
|
|
679
|
-
buildPermissionRow({
|
|
680
|
-
permissionType: _iampermissiontypeenum.IamPermissionType.USER_ROLE,
|
|
681
|
-
targetId: 'role-1'
|
|
682
|
-
})
|
|
683
|
-
];
|
|
684
|
-
}
|
|
685
|
-
if (where.permissionType === _iampermissiontypeenum.IamPermissionType.ROLE_ACTION) {
|
|
686
|
-
return [
|
|
687
|
-
buildPermissionRow({
|
|
688
|
-
permissionType: _iampermissiontypeenum.IamPermissionType.ROLE_ACTION,
|
|
689
|
-
targetId: 'action-role-1'
|
|
690
|
-
})
|
|
691
|
-
];
|
|
692
|
-
}
|
|
693
|
-
if (where.permissionType === _iampermissiontypeenum.IamPermissionType.USER_ACTION) {
|
|
694
|
-
throw new Error('DIRECT lookup should not run in RBAC mode');
|
|
695
|
-
}
|
|
696
|
-
return [];
|
|
697
|
-
});
|
|
698
|
-
const result = await service.collectAllActionIds('user-1', null, null);
|
|
699
|
-
expect(Array.from(result)).toEqual([
|
|
700
|
-
'action-role-1'
|
|
701
|
-
]);
|
|
702
|
-
});
|
|
703
|
-
it('DIRECT mode: collects only directly-assigned user action ids, ignoring roles', async ()=>{
|
|
704
|
-
mockIamConfigService.getPermissionMode.mockReturnValue(_permissiontypeenum.IAMPermissionMode.DIRECT);
|
|
705
|
-
mockPermissionRepo.find.mockImplementation(async ({ where })=>{
|
|
706
|
-
if (where.permissionType === _iampermissiontypeenum.IamPermissionType.USER_ROLE) {
|
|
707
|
-
throw new Error('RBAC lookup should not run in DIRECT mode');
|
|
708
|
-
}
|
|
709
|
-
if (where.permissionType === _iampermissiontypeenum.IamPermissionType.USER_ACTION) {
|
|
710
|
-
return [
|
|
711
|
-
buildPermissionRow({
|
|
712
|
-
permissionType: _iampermissiontypeenum.IamPermissionType.USER_ACTION,
|
|
713
|
-
targetId: 'action-direct-1'
|
|
714
|
-
})
|
|
715
|
-
];
|
|
716
|
-
}
|
|
717
|
-
return [];
|
|
718
|
-
});
|
|
719
|
-
const result = await service.collectAllActionIds('user-1', null, null);
|
|
720
|
-
expect(Array.from(result)).toEqual([
|
|
721
|
-
'action-direct-1'
|
|
722
|
-
]);
|
|
723
|
-
});
|
|
724
|
-
it('FULL mode: merges role-derived and direct action ids, de-duplicating overlaps', async ()=>{
|
|
725
|
-
mockIamConfigService.getPermissionMode.mockReturnValue(_permissiontypeenum.IAMPermissionMode.FULL);
|
|
726
|
-
mockPermissionRepo.find.mockImplementation(async ({ where })=>{
|
|
727
|
-
if (where.permissionType === _iampermissiontypeenum.IamPermissionType.USER_ROLE) {
|
|
728
|
-
return [
|
|
729
|
-
buildPermissionRow({
|
|
730
|
-
permissionType: _iampermissiontypeenum.IamPermissionType.USER_ROLE,
|
|
731
|
-
targetId: 'role-1'
|
|
732
|
-
})
|
|
733
|
-
];
|
|
734
|
-
}
|
|
735
|
-
if (where.permissionType === _iampermissiontypeenum.IamPermissionType.ROLE_ACTION) {
|
|
736
|
-
return [
|
|
737
|
-
buildPermissionRow({
|
|
738
|
-
permissionType: _iampermissiontypeenum.IamPermissionType.ROLE_ACTION,
|
|
739
|
-
targetId: 'action-shared'
|
|
740
|
-
})
|
|
741
|
-
];
|
|
742
|
-
}
|
|
743
|
-
if (where.permissionType === _iampermissiontypeenum.IamPermissionType.USER_ACTION) {
|
|
744
|
-
return [
|
|
745
|
-
buildPermissionRow({
|
|
746
|
-
permissionType: _iampermissiontypeenum.IamPermissionType.USER_ACTION,
|
|
747
|
-
targetId: 'action-shared'
|
|
748
|
-
}),
|
|
749
|
-
buildPermissionRow({
|
|
750
|
-
permissionType: _iampermissiontypeenum.IamPermissionType.USER_ACTION,
|
|
751
|
-
targetId: 'action-direct-only'
|
|
752
|
-
})
|
|
753
|
-
];
|
|
754
|
-
}
|
|
755
|
-
return [];
|
|
756
|
-
});
|
|
757
|
-
const result = await service.collectAllActionIds('user-1', null, null);
|
|
758
|
-
expect(Array.from(result).sort()).toEqual([
|
|
759
|
-
'action-direct-only',
|
|
760
|
-
'action-shared'
|
|
761
|
-
]);
|
|
762
|
-
});
|
|
763
|
-
it('excludes expired/not-yet-valid permission rows via isValid()', async ()=>{
|
|
764
|
-
mockIamConfigService.getPermissionMode.mockReturnValue(_permissiontypeenum.IAMPermissionMode.DIRECT);
|
|
765
|
-
const expired = buildPermissionRow({
|
|
766
|
-
permissionType: _iampermissiontypeenum.IamPermissionType.USER_ACTION,
|
|
767
|
-
targetId: 'action-expired',
|
|
768
|
-
validUntil: new Date('2000-01-01')
|
|
769
|
-
});
|
|
770
|
-
const active = buildPermissionRow({
|
|
771
|
-
permissionType: _iampermissiontypeenum.IamPermissionType.USER_ACTION,
|
|
772
|
-
targetId: 'action-active'
|
|
773
|
-
});
|
|
774
|
-
mockPermissionRepo.find.mockResolvedValue([
|
|
775
|
-
expired,
|
|
776
|
-
active
|
|
777
|
-
]);
|
|
778
|
-
const result = await service.collectAllActionIds('user-1', null, null);
|
|
779
|
-
expect(Array.from(result)).toEqual([
|
|
780
|
-
'action-active'
|
|
781
|
-
]);
|
|
782
|
-
});
|
|
783
|
-
});
|
|
784
|
-
describe('applyCompanyWhitelist', ()=>{
|
|
785
|
-
it('is a no-op when the company feature is disabled', async ()=>{
|
|
786
|
-
const actionIds = new Set([
|
|
787
|
-
'action-1',
|
|
788
|
-
'action-2'
|
|
789
|
-
]);
|
|
790
|
-
await service.applyCompanyWhitelist(actionIds, 'company-1');
|
|
791
|
-
expect(actionIds).toEqual(new Set([
|
|
792
|
-
'action-1',
|
|
793
|
-
'action-2'
|
|
794
|
-
]));
|
|
795
|
-
expect(mockPermissionRepo.find).not.toHaveBeenCalled();
|
|
796
|
-
});
|
|
797
|
-
it('is a no-op when companyId is missing even if the company feature is enabled', async ()=>{
|
|
798
|
-
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
799
|
-
const actionIds = new Set([
|
|
800
|
-
'action-1'
|
|
801
|
-
]);
|
|
802
|
-
await service.applyCompanyWhitelist(actionIds, null);
|
|
803
|
-
expect(actionIds).toEqual(new Set([
|
|
804
|
-
'action-1'
|
|
805
|
-
]));
|
|
806
|
-
});
|
|
807
|
-
it('does not filter anything when the company has an empty whitelist', async ()=>{
|
|
808
|
-
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
809
|
-
mockPermissionRepo.find.mockResolvedValue([]);
|
|
810
|
-
const actionIds = new Set([
|
|
811
|
-
'action-1',
|
|
812
|
-
'action-2'
|
|
813
|
-
]);
|
|
814
|
-
await service.applyCompanyWhitelist(actionIds, 'company-1');
|
|
815
|
-
expect(actionIds).toEqual(new Set([
|
|
816
|
-
'action-1',
|
|
817
|
-
'action-2'
|
|
818
|
-
]));
|
|
819
|
-
});
|
|
820
|
-
it('removes action ids that are not present in the company whitelist', async ()=>{
|
|
821
|
-
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
822
|
-
mockPermissionRepo.find.mockResolvedValue([
|
|
823
|
-
{
|
|
824
|
-
targetId: 'action-1'
|
|
825
|
-
}
|
|
826
|
-
]);
|
|
827
|
-
const actionIds = new Set([
|
|
828
|
-
'action-1',
|
|
829
|
-
'action-2'
|
|
830
|
-
]);
|
|
831
|
-
await service.applyCompanyWhitelist(actionIds, 'company-1');
|
|
832
|
-
expect(actionIds).toEqual(new Set([
|
|
833
|
-
'action-1'
|
|
834
|
-
]));
|
|
835
|
-
});
|
|
836
|
-
});
|
|
837
|
-
// ==================== Cache invalidation helpers ====================
|
|
838
|
-
describe('invalidateCompanyMembersCache', ()=>{
|
|
839
|
-
it('returns 0 without querying when the company feature is disabled', async ()=>{
|
|
840
|
-
const result = await service.invalidateCompanyMembersCache('company-1');
|
|
841
|
-
expect(result).toBe(0);
|
|
842
|
-
expect(mockPermissionRepo.createQueryBuilder).not.toHaveBeenCalled();
|
|
843
|
-
});
|
|
844
|
-
it('returns 0 when the company has no members with permissions', async ()=>{
|
|
845
|
-
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
846
|
-
mockRawMany(mockPermissionRepo, []);
|
|
847
|
-
const result = await service.invalidateCompanyMembersCache('company-1');
|
|
848
|
-
expect(result).toBe(0);
|
|
849
|
-
});
|
|
850
|
-
it('invalidates every distinct member user id for the company', async ()=>{
|
|
851
|
-
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
852
|
-
mockRawMany(mockPermissionRepo, [
|
|
853
|
-
{
|
|
854
|
-
userId: 'user-1',
|
|
855
|
-
branchId: 'branch-1'
|
|
856
|
-
},
|
|
857
|
-
{
|
|
858
|
-
userId: 'user-2',
|
|
859
|
-
branchId: null
|
|
860
|
-
}
|
|
588
|
+
cachedEndpoints: 1
|
|
589
|
+
};
|
|
590
|
+
mockPermissionCacheService.getMyPermissionsResponse.mockResolvedValue(response);
|
|
591
|
+
const result = await service.getMyPermissions('user-1', null, 'company-1', [
|
|
592
|
+
'parent-code'
|
|
861
593
|
]);
|
|
862
|
-
mockPermissionCacheService.
|
|
863
|
-
|
|
864
|
-
expect(mockPermissionCacheService.invalidateUsers).toHaveBeenCalledWith([
|
|
865
|
-
'user-1',
|
|
866
|
-
'user-2'
|
|
867
|
-
], 'company-1', [
|
|
868
|
-
'branch-1',
|
|
869
|
-
null
|
|
594
|
+
expect(mockPermissionCacheService.getMyPermissionsResponse).toHaveBeenCalledWith('user-1', null, 'company-1', [
|
|
595
|
+
'parent-code'
|
|
870
596
|
]);
|
|
871
|
-
expect(result).toBe(
|
|
597
|
+
expect(result).toBe(response);
|
|
872
598
|
});
|
|
873
599
|
});
|
|
874
600
|
describe('revokeCompanyPermissions', ()=>{
|
|
@@ -886,10 +612,7 @@ describe('PermissionService', ()=>{
|
|
|
886
612
|
]);
|
|
887
613
|
mockPermissionRepo.find.mockResolvedValue([
|
|
888
614
|
{
|
|
889
|
-
|
|
890
|
-
},
|
|
891
|
-
{
|
|
892
|
-
userId: null
|
|
615
|
+
sourceId: 'user-1'
|
|
893
616
|
}
|
|
894
617
|
]);
|
|
895
618
|
const fakeTxRepo = {
|
|
@@ -898,12 +621,14 @@ describe('PermissionService', ()=>{
|
|
|
898
621
|
const fakeTxRoleRepo = {
|
|
899
622
|
delete: jest.fn().mockResolvedValue(buildDeleteResult(1))
|
|
900
623
|
};
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
624
|
+
mockDataSourceProvider.getDataSource.mockResolvedValue({
|
|
625
|
+
transaction: jest.fn().mockImplementation((cb)=>cb({
|
|
626
|
+
getRepository: jest.fn((target)=>{
|
|
627
|
+
if (target === mockRoleRepo.target) return fakeTxRoleRepo;
|
|
628
|
+
return fakeTxRepo;
|
|
629
|
+
})
|
|
630
|
+
}))
|
|
631
|
+
});
|
|
907
632
|
await service.revokeCompanyPermissions('company-1');
|
|
908
633
|
expect(fakeTxRepo.delete).toHaveBeenCalledWith(expect.objectContaining({
|
|
909
634
|
permissionType: _iampermissiontypeenum.IamPermissionType.COMPANY_ACTION
|
|
@@ -925,7 +650,7 @@ describe('PermissionService', ()=>{
|
|
|
925
650
|
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
926
651
|
mockPermissionRepo.find.mockResolvedValue([
|
|
927
652
|
{
|
|
928
|
-
|
|
653
|
+
sourceId: 'user-1'
|
|
929
654
|
}
|
|
930
655
|
]);
|
|
931
656
|
mockPermissionRepo.delete.mockResolvedValue(buildDeleteResult(1));
|
|
@@ -70,7 +70,7 @@ let RoleService = class RoleService extends _classes.ApiService {
|
|
|
70
70
|
}
|
|
71
71
|
});
|
|
72
72
|
const affectedUserIds = [
|
|
73
|
-
...new Set(userRoleRows.map((row)=>row.
|
|
73
|
+
...new Set(userRoleRows.map((row)=>row.sourceId))
|
|
74
74
|
];
|
|
75
75
|
await permissionRepo.delete({
|
|
76
76
|
permissionType: _iampermissiontypeenum.IamPermissionType.ROLE_ACTION,
|
|
@@ -247,13 +247,10 @@ describe('RoleService', ()=>{
|
|
|
247
247
|
const permissionRepo = (0, _repositorymock.createMockRepository)();
|
|
248
248
|
permissionRepo.find.mockResolvedValue([
|
|
249
249
|
{
|
|
250
|
-
|
|
250
|
+
sourceId: 'user-1'
|
|
251
251
|
},
|
|
252
252
|
{
|
|
253
|
-
|
|
254
|
-
},
|
|
255
|
-
{
|
|
256
|
-
userId: null
|
|
253
|
+
sourceId: 'user-2'
|
|
257
254
|
}
|
|
258
255
|
]);
|
|
259
256
|
const queryRunner = buildQueryRunner(permissionRepo);
|
|
@@ -290,10 +287,10 @@ describe('RoleService', ()=>{
|
|
|
290
287
|
const permissionRepo = (0, _repositorymock.createMockRepository)();
|
|
291
288
|
permissionRepo.find.mockResolvedValue([
|
|
292
289
|
{
|
|
293
|
-
|
|
290
|
+
sourceId: 'user-1'
|
|
294
291
|
},
|
|
295
292
|
{
|
|
296
|
-
|
|
293
|
+
sourceId: 'user-1'
|
|
297
294
|
}
|
|
298
295
|
]);
|
|
299
296
|
const queryRunner = buildQueryRunner(permissionRepo);
|
package/dtos/permission.dto.d.ts
CHANGED
|
@@ -47,7 +47,7 @@ export class CompanyActionPermissionController {
|
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
49
|
async getCompanyActions(dto) {
|
|
50
|
-
const actions = await this.permissionService.getCompanyActions(dto.companyId);
|
|
50
|
+
const actions = await this.permissionService.getCompanyActions(dto.companyId, false);
|
|
51
51
|
return {
|
|
52
52
|
success: true,
|
|
53
53
|
message: 'Company actions retrieved successfully',
|
|
@@ -50,7 +50,7 @@ describe('CompanyActionPermissionController', ()=>{
|
|
|
50
50
|
const result = await controller.getCompanyActions({
|
|
51
51
|
companyId: 'company-1'
|
|
52
52
|
});
|
|
53
|
-
expect(mockPermissionService.getCompanyActions).toHaveBeenCalledWith('company-1');
|
|
53
|
+
expect(mockPermissionService.getCompanyActions).toHaveBeenCalledWith('company-1', false);
|
|
54
54
|
expect(result.data).toEqual([
|
|
55
55
|
{
|
|
56
56
|
actionId: 'action-1'
|
|
@@ -48,7 +48,7 @@ export class UserActionPermissionController {
|
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
50
|
async getUserActions(dto, user) {
|
|
51
|
-
const actions = await this.permissionService.getUserActions(dto.userId, dto.
|
|
51
|
+
const actions = await this.permissionService.getUserActions(dto.userId, dto.companyId, dto.branchId);
|
|
52
52
|
return {
|
|
53
53
|
success: true,
|
|
54
54
|
message: 'User actions retrieved successfully',
|
|
@@ -53,7 +53,7 @@ describe('UserActionPermissionController', ()=>{
|
|
|
53
53
|
branchId: 'branch-1',
|
|
54
54
|
companyId: 'company-1'
|
|
55
55
|
}, user);
|
|
56
|
-
expect(mockPermissionService.getUserActions).toHaveBeenCalledWith('user-2', '
|
|
56
|
+
expect(mockPermissionService.getUserActions).toHaveBeenCalledWith('user-2', 'company-1', 'branch-1');
|
|
57
57
|
expect(result.data).toEqual([
|
|
58
58
|
{
|
|
59
59
|
actionId: 'action-1'
|
|
@@ -285,6 +285,7 @@ export class UserActionResponseDto {
|
|
|
285
285
|
_define_property(this, "actionId", void 0);
|
|
286
286
|
_define_property(this, "actionCode", void 0);
|
|
287
287
|
_define_property(this, "actionName", void 0);
|
|
288
|
+
_define_property(this, "companyId", void 0);
|
|
288
289
|
_define_property(this, "branchId", void 0);
|
|
289
290
|
_define_property(this, "createdAt", void 0);
|
|
290
291
|
}
|
|
@@ -319,6 +320,12 @@ _ts_decorate([
|
|
|
319
320
|
}),
|
|
320
321
|
_ts_metadata("design:type", String)
|
|
321
322
|
], UserActionResponseDto.prototype, "actionName", void 0);
|
|
323
|
+
_ts_decorate([
|
|
324
|
+
ApiPropertyOptional({
|
|
325
|
+
description: 'Company ID (null = global)'
|
|
326
|
+
}),
|
|
327
|
+
_ts_metadata("design:type", Object)
|
|
328
|
+
], UserActionResponseDto.prototype, "companyId", void 0);
|
|
322
329
|
_ts_decorate([
|
|
323
330
|
ApiPropertyOptional({
|
|
324
331
|
description: 'Branch ID (null = company-wide)'
|
|
@@ -32,7 +32,7 @@ import { Column } from 'typeorm';
|
|
|
32
32
|
return true;
|
|
33
33
|
}
|
|
34
34
|
constructor(...args){
|
|
35
|
-
super(...args), _define_property(this, "permissionType", void 0), _define_property(this, "sourceType", void 0), _define_property(this, "sourceId", void 0), _define_property(this, "targetType", void 0), _define_property(this, "targetId", void 0), _define_property(this, "
|
|
35
|
+
super(...args), _define_property(this, "permissionType", void 0), _define_property(this, "sourceType", void 0), _define_property(this, "sourceId", void 0), _define_property(this, "targetType", void 0), _define_property(this, "targetId", void 0), _define_property(this, "validFrom", void 0), _define_property(this, "validUntil", void 0), _define_property(this, "reason", void 0);
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
_ts_decorate([
|
|
@@ -75,15 +75,6 @@ _ts_decorate([
|
|
|
75
75
|
}),
|
|
76
76
|
_ts_metadata("design:type", String)
|
|
77
77
|
], PermissionBase.prototype, "targetId", void 0);
|
|
78
|
-
_ts_decorate([
|
|
79
|
-
Column({
|
|
80
|
-
type: 'varchar',
|
|
81
|
-
length: 150,
|
|
82
|
-
nullable: true,
|
|
83
|
-
name: 'user_id'
|
|
84
|
-
}),
|
|
85
|
-
_ts_metadata("design:type", Object)
|
|
86
|
-
], PermissionBase.prototype, "userId", void 0);
|
|
87
78
|
_ts_decorate([
|
|
88
79
|
Column({
|
|
89
80
|
type: 'timestamp',
|
|
@@ -4,7 +4,7 @@ function _ts_decorate(decorators, target, key, desc) {
|
|
|
4
4
|
else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
5
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
6
|
}
|
|
7
|
-
import { PERMISSION_GUARD_CONFIG } from '@flusys/nestjs-shared';
|
|
7
|
+
import { PERMISSION_GUARD_CONFIG, PERMISSIONS_CACHE_PREFIX } from '@flusys/nestjs-shared';
|
|
8
8
|
import { CacheModule, UtilsModule } from '@flusys/nestjs-shared/modules';
|
|
9
9
|
import { Module } from '@nestjs/common';
|
|
10
10
|
import { IAM_MODULE_OPTIONS } from '../config/iam.constants';
|
|
@@ -14,7 +14,7 @@ import { PermissionModeHelper } from '../helpers';
|
|
|
14
14
|
import { ActionService, PermissionService, RoleService } from '../services';
|
|
15
15
|
import { IAMConfigService } from '../services/iam-config.service';
|
|
16
16
|
import { IAMDataSourceService } from '../services/iam-datasource.service';
|
|
17
|
-
import {
|
|
17
|
+
import { PermissionCacheService } from '../services/permission-cache.service';
|
|
18
18
|
export class IAMModule {
|
|
19
19
|
static getControllers(permissionMode, enableCompanyFeature) {
|
|
20
20
|
const baseControllers = [
|
|
@@ -59,8 +59,8 @@ export class IAMModule {
|
|
|
59
59
|
provide: PERMISSION_GUARD_CONFIG,
|
|
60
60
|
useValue: {
|
|
61
61
|
enableCompanyFeature,
|
|
62
|
-
userPermissionKeyFormat: `${
|
|
63
|
-
companyPermissionKeyFormat: `${
|
|
62
|
+
userPermissionKeyFormat: `${PERMISSIONS_CACHE_PREFIX}:user:{userId}`,
|
|
63
|
+
companyPermissionKeyFormat: `${PERMISSIONS_CACHE_PREFIX}:company:{companyId}:branch:{branchId}:user:{userId}`
|
|
64
64
|
}
|
|
65
65
|
};
|
|
66
66
|
}
|