UNPKG

@superawesome/permissions

Version:

Fine grained permissions / access control with ownerships & attribute picking, done right.

1,382 lines (1,225 loc) 49.3 kB
// 3rd party import * as _ from 'lodash'; import * as _f from 'lodash/fp'; import { stdout } from 'test-console'; // own import { getLogger, setLogger } from '../logger'; import { Permissions } from '../Permissions'; import { ALL_DOCUMENTS, ALL_DOCUMENTS_IDS, listOwned_DocsOfMeAndMyCompanyUsers, listOwned_DocsOfMeAndMyManagedUsers, USERS, } from './data.fixtures'; import { Permit } from '../Permit.class'; import { permissionDefinitionDefaults, permissionDefinitions, permissionDefinitions_limitOwned, } from './permissionDefinitions.fixtures'; import { Tid } from '../types'; export enum ECrudActions { create = 'create', read = 'read', update = 'update', delete = 'delete', } let permit: Permit; const nonOwnResourceId = 99999; /** * @todo: restructure tests around features, instead of the naive user-can-do-that syntax :-) */ const permissions = new Permissions({ permissionDefinitions, permissionDefinitionDefaults, }).build(); const permissions_limitOwned = new Permissions({ permissionDefinitions: permissionDefinitions_limitOwned, permissionDefinitionDefaults, limitOwnReduce: ({ user, limitOwneds }) => _.overSome(limitOwneds.map((limitOwned) => limitOwned({ user }))), }).build(); describe('SuperAwesome Permissions', () => { describe('Basic usage & error handling', () => { describe('PermissionsDefinitions validations at addDefinitions phase', () => { it('throws error when PD lacks "resource"', () => { const errRE = /InvalidPermissionDefinitionError: missing "resource"/; expect( () => new Permissions({ permissionDefinitions: { roles: ['ADMIN'], grant: ['foo'] } }) ).toThrow(errRE); expect(() => { const p = new Permissions(); p.addDefinitions({ roles: ['ADMIN'], grant: ['foo'] }); }).toThrow(errRE); }); it('throws error when PD lacks "role"', () => { const errRE = /InvalidPermissionDefinitionError: missing "roles"/; expect( () => new Permissions({ permissionDefinitions: { resource: 'document', grant: ['foo'] } }) ).toThrow(errRE); expect(() => { const p = new Permissions(); p.addDefinitions({ resource: 'document', grant: ['foo'] }); }).toThrow(errRE); }); it('throws error when PD lacks "grant"', () => { const errRE = /InvalidPermissionDefinitionError: missing or empty "grant"/; expect( () => new Permissions({ permissionDefinitions: { resource: 'document', roles: ['someRole'] }, }) ).toThrow(errRE); }); describe('redefining actions attributes', () => { it('throws error when redefined attributes are DIFFERENT for same role & resource', () => expect( () => new Permissions({ permissionDefinitions: [ { resource: 'document', roles: ['someRole'], grant: { 'list:any': ['foo', 'bar'], }, }, { resource: 'document', roles: ['someRole'], grant: { 'list:any': ['bar', 'foo'], }, }, ], }) ).toThrow('InvalidPermissionDefinitionError: addDefinitions() redefining action error')); it('just warns when redefined attributes are SAME for same role & resource', () => { const oldLogger = getLogger(); setLogger({ warn(msg, data) { process.stdout.write(msg, data); }, debug: _.noop, } as any); const output = stdout.inspectSync( () => new Permissions({ permissionDefinitions: [ { resource: 'document', roles: ['matchingRole', 'someOtherRole'], grant: { 'list:any': ['foo', 'bar'], }, }, { resource: 'document', roles: ['matchingRole', 'anotherRole'], grant: { 'list:any': ['foo', 'bar'], }, }, ], }) ); expect(output[0]).toMatch( 'addDefinitions() redefining action in a PD with same attributes is obsolete:' ); setLogger(oldLogger); }); it('all is good for different roles', () => { const output = stdout.inspectSync( () => new Permissions({ permissionDefinitions: [ { resource: 'document', roles: ['matchingRole', 'someOtherRole'], grant: { 'list:any': ['foo', 'bar'], }, }, { resource: 'document', roles: ['nonMatchingRole', 'anotherRole'], grant: { 'list:any': ['foo', 'bar'], }, }, ], }) ); expect(output[0]).toBeUndefined(); }); }); describe('Validating ownership hooks', () => { describe('In the current added PD', () => { const permissionDefinition = { roles: ['ADMIN'], resource: 'document', grant: ['foo:own'], }; it.each([ [{ ...permissionDefinition }], [{ ...permissionDefinition, isOwner: _.noop as any } as any], [{ ...permissionDefinition, listOwner: _.noop as any } as any], [{ ...permissionDefinition, limitOwner: _.noop as any } as any], ])( `throws error @addDefinitions if PD has 'own' action but NO 'isOwner' or 'listOwned' for this role can be found`, (pd) => { expect(() => new Permissions({ permissionDefinitions: pd })).toThrow( /SA-Permissions: in addDefinitions\(\) PermissionDefinition has 'own' action but no/ ); } ); }); it(`doesnt throw error @addDefinitions if PD has no 'own' actions`, () => { expect(() => { new Permissions({ permissionDefinitions: [ { roles: ['EMPLOYEE', 'ADMIN', 'SOME_ROLE'], resource: 'document', grant: ['foo:any'], }, ], }).build(); }).not.toThrow(/./); }); it(`throws error @addDefinitions if PD has 'own' action and has both 'listOwned' & 'limitOwned' in the same PD`, () => { expect(() => { new Permissions({ permissionDefinitions: [ { roles: ['EMPLOYEE', 'ADMIN', 'SOME_ROLE'], resource: 'document', grant: ['foo:own'], isOwner: _.noop as any, limitOwned: _.noop as any, listOwned: _.noop as any, } as any, { roles: ['ADMIN', 'EMPLOYEE'], // both matched with above resource: 'document', grant: ['bar:own'], }, ], }).build(); }).toThrow( /SA-Permissions: in addDefinitions\(\) found BOTH "listOwned" & "limitOwned" callbacks in the added PermissionDefinition/ ); }); it(`throws error @addDefinitions if PD has 'own' action and can find PDs resulting having both 'listOwned' & 'limitOwned' for this resource`, () => { expect(() => { new Permissions({ permissionDefinitions: [ { roles: ['EMPLOYEE', 'ADMIN', 'SOME_ROLE'], resource: 'document', grant: ['foo:own'], isOwner: _.noop as any, limitOwned: _.noop as any, }, { roles: ['ADMIN', 'EMPLOYEE'], // both matched with above resource: 'document', grant: ['bar:own'], isOwner: _.noop as any, listOwned: _.noop as any, }, ], }).build(); }).toThrow( /SA-Permissions: in addDefinitions\(\) found BOTH "listOwned" & "limitOwned" callbacks in some PermissionDefinition/ ); }); }); }); describe(`grantPermit() phase / after build()`, () => { it('throws error @addDefinitions if addDefinitions is called after build()', () => { const permissions2 = new Permissions({ permissionDefinitions: { roles: ['ADMIN'], resource: 'document', grant: ['foo'], }, }); permissions2.build(); expect(() => permissions2.addDefinitions({ resource: 'document', roles: ['SOME_ROLE'], grant: ['foo'], }) ).toThrow(/InvalidInvocation: calling addDefinitions\(\) after having build\(\)/); }); it('doesnt throw error on grantPermit with empty user.roles', async () => { const permissions2 = new Permissions({ permissionDefinitions: { roles: ['ADMIN'], resource: 'document', grant: ['foo'], }, }); permissions2.build(); const permit2 = await permissions2.grantPermit({ resource: 'document', user: { roles: [], id: 1 }, action: 'foo', }); expect(permit2).toBeInstanceOf(Permit); expect(permit2.granted).toBe(false); expect(permit2.anyGranted).toBe(false); expect(permit2.anyAttributes).toStrictEqual([]); expect(permit2.ownGranted).toBe(false); expect(permit2.ownAttributes).toStrictEqual([]); expect(await permit2.attributes()).toStrictEqual([]); }); it('throws error on grantPermit with invalid resource', async () => { const permissions2 = new Permissions({ permissionDefinitions: { roles: ['ADMIN'], resource: 'document', grant: ['foo'], }, }); permissions2.build(); await expect( permissions2.grantPermit({ resource: 'invalidResource', user: { roles: ['ADMIN'], id: 1 }, action: 'foo', }) ).rejects.toThrow(/Invalid resource: "invalidResource"/); }); describe('Invalid actions', () => { it('throws error on grantPermit with missing action (from AccessControl::permission()', async () => { const permissions2 = new Permissions({ permissionDefinitions: { roles: ['ADMIN'], resource: 'document', grant: ['foo'], }, }); permissions2.build(); await expect( permissions2.grantPermit({ resource: 'document', user: { roles: ['ADMIN'], id: 1 }, action: 'invalidAction', }) ).rejects.toThrow(/Invalid action: invalidAction/); }); it('throws error on grantPermit with invalid action with colon (from SA-Permissions)', async () => { const permissions2 = new Permissions({ permissionDefinitions: { roles: ['ADMIN'], resource: 'document', grant: ['foo:any'], }, }); permissions2.build(); await expect( permissions2.grantPermit({ resource: 'document', user: { roles: ['ADMIN'], id: 1 }, action: 'foo:any', }) ).rejects.toThrow(/Invalid action structure: "foo:any"/); }); }); describe('Ownership hooks', () => { it('are called once per isOwn()/listOwn() call (they are added as _.uniq to Permit)', async () => { const isOwner = jest.fn().mockResolvedValue(false); const isOwner2 = jest.fn().mockResolvedValue(false); const listOwned = jest.fn().mockResolvedValue([1, 2, 3]); const listOwned2 = jest.fn().mockResolvedValue([3, 4, 5]); const permissions2 = new Permissions({ permissionDefinitions: [ { roles: ['ROLE1'], resource: 'document', grant: ['someOwnAction:own'], isOwner, listOwned, }, { roles: ['ROLE2'], resource: 'document', grant: ['someOwnAction:own'], isOwner, listOwned, }, { roles: ['ROLE3'], resource: 'document', grant: ['someOwnAction:own'], isOwner: isOwner2, listOwned: listOwned2, }, ], }).build(); const permit2 = await permissions2.grantPermit({ action: 'someOwnAction', resource: 'document', user: { id: 1, roles: ['ROLE1', 'ROLE2', 'ROLE3'] }, }); const isOwnDocumentId = await permit2.isOwn(999); expect(isOwner.mock.calls.length).toBe(1); expect(isOwner2.mock.calls.length).toBe(1); expect((permit2 as any)._isOwners.length).toBe(2); expect(isOwnDocumentId).toBe(false); const ownResourceIds = await permit2.listOwn(); expect(listOwned.mock.calls.length).toBe(1); expect(listOwned2.mock.calls.length).toBe(1); expect((permit2 as any)._listOwneds.length).toBe(2); expect(ownResourceIds).toEqual([1, 2, 3, 4, 5]); }); }); }); }); describe('example permissionDefinitions based specs', () => { describe(`Possession 'any' permission checks`, () => { _.each([undefined, 100, 200, USERS.superAdmin5.id * 100, nonOwnResourceId], (resourceId) => it(`grants SUPER_ADMIN to read ANY document: ${resourceId}`, async () => { expect( ( await permissions.grantPermit({ user: USERS.superAdmin5, resource: 'document', action: 'read', resourceId, }) ).granted ).toEqual(true); }) ); }); describe(`Possession 'own' permission checks:`, () => { describe('listOwn() & limitOwn() for owned resources', () => { describe(`simple role - an employee`, () => { const authQuery = { user: USERS.employee1, resource: 'document', action: 'browse', }; it('listOwn()', async () => { const permit2 = await permissions.grantPermit(authQuery); expect(await permit2.listOwn()).toEqual([1, 10, 100]); }); it('limitOwn()', async () => { const permit2 = await permissions_limitOwned.grantPermit(authQuery); expect( ALL_DOCUMENTS.filter(permit2.limitOwn()).map((doc) => doc.id) ).toIncludeSameMembers([1, 10, 100]); }); }); describe(`inherit from multiple roles - a Manager & CompanyAdmin`, () => { const user = USERS.managerAndCompanyAdmin7; const authQuery = { user, resource: 'document', action: 'read', }; let allowedDocIds: number[]; beforeAll(async () => { allowedDocIds = _.uniq([ ...(await listOwned_DocsOfMeAndMyManagedUsers(user)), ...(await listOwned_DocsOfMeAndMyCompanyUsers(user)), ]); }); it('listOwn()', async () => { const permit2 = await permissions.grantPermit(authQuery); expect(await permit2.listOwn()).toIncludeSameMembers(allowedDocIds); }); it('limitOwn()', async () => { const permit2 = await permissions_limitOwned.grantPermit(authQuery); expect( ALL_DOCUMENTS.filter(permit2.limitOwn()).map((doc) => doc.id) ).toIncludeSameMembers(allowedDocIds); }); }); }); describe(`Denies or grants based on possession & ownership of known/unknown resourceId:`, () => { // eslint-disable-next-line guard-for-in for (const crudAction in ECrudActions) { it(`denies EMPLOYEE to CRUD ${crudAction} ANY document (of unknown resourceId), but allows OWN resourceId later on`, async () => { const user = USERS.employee1; const permit2 = await permissions.grantPermit({ user, resource: 'document', action: crudAction, }); expect(permit2.granted).toBe(true); expect(permit2.ownGranted).toBe(true); // any is NOT granted expect(permit2.anyGranted).toBe(false); // but own resourceId is granted expect(await permit2.isOwn(USERS.employee1.id * 100)).toBe(true); }); it(`denies EMPLOYEE to CRUD ${crudAction} NOT OWNED document`, async () => { const user = USERS.employee1; const permit2 = await permissions.grantPermit({ user, resource: 'document', action: crudAction, resourceId: nonOwnResourceId, }); expect(permit2.granted).toBe(false); expect(permit2.anyGranted).toBe(false); expect(permit2.ownGranted).toBe(false); }); it(`grants EMPLOYEE to CRUD ${crudAction} OWNED document`, async () => { const user = USERS.employee1; const ownResourceId = USERS.employee1.id * 100; const permit2 = await permissions.grantPermit({ user, resource: 'document', action: crudAction, resourceId: ownResourceId, }); expect(permit2.granted).toBe(true); expect(permit2.ownGranted).toBe(true); expect(permit2.anyGranted).toBe(false); }); it(`grants SUPER_ADMIN to CRUD ${crudAction} NOT OWNED document`, async () => { const user = USERS.superAdmin5; const permit2 = await permissions.grantPermit({ user, resource: 'document', action: crudAction, resourceId: nonOwnResourceId, }); expect(permit2.granted).toBe(true); expect(permit2.anyGranted).toBe(true); // if anyGranted, we always have ownGranted expect(permit2.ownGranted).toBe(true); // we still know it's not an own resourceId expect(await permit2.isOwn(nonOwnResourceId)).toBe(false); // but recognise our own resourceId expect(await permit2.isOwn(user.id * 100)).toBe(true); }); } it(`if isOnwer or listOwned is defined in PD, but the grant was for 'any', we should still be able to use isOwn & listOwn.`, async () => { const permit2: Permit = await permissions.grantPermit({ user: USERS.employee1, resource: 'document', action: 'browse', }); expect(permit2.granted).toEqual(true); expect(permit2.anyGranted).toEqual(true); expect(permit2.ownGranted).toEqual(true); expect(await permit2.isOwn(100)).toEqual(true); expect(await permit2.isOwn(200)).toEqual(false); expect(await permit2.listOwn()).toEqual([1, 10, 100]); }); }); describe(`Can override default possession:`, () => { it(`grants EMPLOYEE to 'list' ANY document`, async () => { expect( ( await permissions.grantPermit({ user: USERS.employee1, resource: 'document', action: 'list', }) ).granted ).toEqual(true); }); }); _.each([1, 10, 100, 2, 20, 200, 4, 40, 400], (resourceId) => it('grants EMPLOYEE_MANAGER to read OWNED documents of managed users + created by self', async () => { expect( ( await permissions.grantPermit({ user: USERS.employeeManager2, resource: 'document', action: 'read', resourceId, }) ).granted ).toEqual(true); }) ); _.each([3, 5, 6], (resourceId) => it('denies EMPLOYEE_MANAGER to read:own NON-OWNED documents (of non-managed users)', async () => { expect( ( await permissions.grantPermit({ user: USERS.employeeManager2, resource: 'document', action: 'read', resourceId, }) ).granted ).toEqual(false); }) ); }); describe(`Multiple Roles:`, () => { it(`respects widening granting Role permissions (admin's grant should win)`, async () => { expect( ( await permissions.grantPermit({ user: USERS.employeeAndSuperAdmin8, resource: 'document', action: 'delete', }) ).granted ).toEqual(true); }); }); describe('retrieves correct PD data (roles, actions, resources) without polluting other instances', () => { let permissions2: Permissions; // the other instance, should not pollute ;-) beforeEach(() => { permissions2 = new Permissions({ permissionDefinitions: [ { roles: ['FOO_ROLE'], resource: 'fooResource', grant: { fooAction: ['*'] }, }, ], }).build(); }); it(`returns all known getResources()`, () => { expect(permissions.getResources()).toEqual(['comment', 'document', 'securityHole']); expect(permissions2.getResources()).toEqual(['fooResource']); }); it(`returns all known getRoles()`, () => { expect(permissions.getRoles()).toEqual( [ 'EMPLOYEE', 'EMPLOYEE_MANAGER', 'QA_MANAGER', 'COMPANY_ADMIN', 'SUPER_ADMIN', 'GOD', ].sort() ); expect(permissions2.getRoles()).toEqual(['FOO_ROLE']); }); it(`returns all known getResources()`, () => { expect(permissions.getActions()).toEqual( [ 'publish', 'browse', 'create', 'read', 'update', 'delete', 'like', 'list', 'preview', 'share', ].sort() ); expect(permissions2.getActions()).toEqual( ['create', 'read', 'update', 'delete', 'fooAction'].sort() ); }); }); describe(`Supports wildcards '*':`, () => { describe(`wildcard '*' for Action & Resource, for God like roles`, () => { for (const action of permissions.getActions()) { for (const resource of permissions.getResources()) { for (const resourceId of [ undefined, 100, 200, USERS.superAdmin5.id * 100, nonOwnResourceId, ]) { it(`grants GOD to ANY *Action (${action}) to ANY *Resource (${resource}): ${resourceId}`, async () => { expect( ( await permissions.grantPermit({ user: USERS.god6, resource, action, resourceId, }) ).granted ).toEqual(true); }); } } } }); describe(`wildcard '*' for Roles, for "permit everyone" like Resources / Routes`, () => { for (const role of permissions.getRoles()) { for (const resourceId of [ undefined, 100, 200, USERS.superAdmin5.id * 100, nonOwnResourceId, ]) { it(`grants ${role} to preview to ANY securityHole:`, async () => { expect( ( await permissions.grantPermit({ user: { id: 999, roles: [role] }, resource: 'securityHole', action: 'preview', resourceId, }) ).granted ).toEqual(true); }); } } }); }); describe(`Error handling`, () => { it(`filters out unknown roles but it does "logger.warn" about them once per instance`, async () => { const user = { ...USERS.employee1, roles: [...USERS.employee1.roles, 'UNKNOWN_ROLE'] }; // const inspect = stdout.inspect(); const oldLogger = getLogger(); const mockLogger = Object.create(oldLogger.constructor.prototype); mockLogger.warn = jest.fn(); setLogger(mockLogger); for (let i = 1; i < 3; i++) { const { granted } = await permissions.grantPermit({ user, resource: 'document', action: 'list', }); expect(granted).toEqual(true); } expect(mockLogger.warn.mock.calls.length).toBe(1); expect(mockLogger.warn.mock.calls[0][0]).toBe( `SA-Permissions(): at grantPermit(), role not found: UNKNOWN_ROLE (will not warn again about this role)` ); setLogger(oldLogger); }); }); describe('Multiple roles', () => { _.each([100, 600], (resourceId) => it(`denies QA_MANAGER to read non owned documents ${resourceId} (of non-managed users)`, async () => { expect( ( await permissions.grantPermit({ user: USERS.qaManager3, resource: 'document', action: 'read', resourceId, }) ).granted ).toEqual(false); }) ); describe('Inheriting multiple isOwner / listOwned from multiple roles', () => { const user = USERS.managerAndCompanyAdmin7; let allowedDocIds: number[]; beforeAll(async () => { allowedDocIds = _.uniq([ ...(await listOwned_DocsOfMeAndMyManagedUsers(user)), ...(await listOwned_DocsOfMeAndMyCompanyUsers(user)), ]); }); it(`grants managerAndCompanyAdmin7 to read:own all OWNED documents (created by user + of managed users + of company users)`, async () => { for (const resourceId of allowedDocIds) { const permit2 = await permissions.grantPermit({ user, resource: 'document', action: 'read', resourceId, }); expect(permit2.granted).toBeTrue(); } }); it(`returns a Permit::listOwn() produces a list of owned docs (created by user + of managed users + of company users) even if grant is denied because of resourceId`, async () => { for (const resourceId of ALL_DOCUMENTS_IDS) { const permit2 = await permissions.grantPermit({ user, resource: 'document', action: 'read', resourceId, }); expect(await permit2.listOwn()).toIncludeSameMembers(allowedDocIds); } }); it(`denies managerAndCompanyAdmin7 to read NOT OWNED document (NOT created by user + of managed users + of company users)`, async () => { for (const resourceId of _.difference(ALL_DOCUMENTS_IDS, allowedDocIds)) { const permit2 = await permissions.grantPermit({ user, resource: 'document', action: 'read', resourceId, }); expect(permit2.granted).toBeFalse(); } }); }); }); describe(`Permit::attributes(), pick() & filterPick():`, () => { const createItemWithId = (resourceItemKeys, id: Tid, addId = true) => _.reduce( resourceItemKeys, (item, key) => { item[key] = key + id; return item; }, addId ? { id: Number(id) } : {} ); const pickAndCheckEqualSet = async ( permit2: Permit, resourceItem: object, resourceId: number, expectedKeys: string[] ) => { const pickedItem = resourceId ? await permit2.pick(resourceItem, resourceId) : await permit2.pick(resourceItem); expect(_.keys(pickedItem)).toIncludeSameMembers(expectedKeys); expect(_.values(_.pickBy(pickedItem, (v, key) => key !== 'id'))).toIncludeSameMembers( expectedKeys.filter((key) => key !== 'id') ); }; const filterPickAndCheckEqualSet = async ( permit2: Permit, resourceItems: any[], expectedItems: any[] ) => { const pickedItems = await permit2.filterPick(resourceItems); expect(pickedItems).toIncludeSameMembers(expectedItems); }; const resourceItemKeys = [ 'title', 'content', 'views', 'likes', 'publishDate', 'createDate', 'revision', 'deletedAt', 'confidential', 'withLimits', ]; const aResourceItem = createItemWithId(resourceItemKeys, '', false); describe(`granted to a user having a single role:`, () => { const user = USERS.employee1; const ownResourceId = user.id * 100; describe(`on action granted with both 'own' & 'any' possession:`, () => { const action = 'list'; const anyAttributes = ['title', 'createDate']; const ownAttributes = ['*']; const ownPickedAttributes = [ // from EMPLOYEE 'title', 'content', 'views', 'likes', 'publishDate', 'createDate', 'revision', 'deletedAt', 'withLimits', 'confidential', ]; beforeAll(async () => { permit = await permissions.grantPermit({ user, action, resource: 'document', }); // for sanity expect(await permit.isOwn(ownResourceId)).toEqual(true); expect(await permit.isOwn(nonOwnResourceId)).toEqual(false); }); describe(`Permit::attributes():`, () => { it(`returns anyAttributes if no resourceId passed`, async () => { expect(await permit.attributes()).toEqual(anyAttributes); expect(permit.anyAttributes).toEqual(anyAttributes); }); it(`returns anyAttributes if NOT own resourceId passed`, async () => { expect(await permit.attributes(nonOwnResourceId)).toEqual(anyAttributes); }); it(`returns ownAttributes if own resourceId passed`, async () => { expect(await permit.attributes(ownResourceId)).toEqual(ownAttributes); expect(permit.ownAttributes).toEqual(ownAttributes); }); }); describe(`Permit::pick():`, () => { it(`returns anyAttributes if when no resourceId passed`, async () => { await pickAndCheckEqualSet(permit, aResourceItem, null, anyAttributes); }); it(`returns ownAttributes if when no resourceId passed`, async () => { await pickAndCheckEqualSet(permit, aResourceItem, ownResourceId, ownPickedAttributes); }); }); describe(`Permit::filterPick():`, () => {}); }); describe(`on action granted only with 'own' possession:`, () => { const action = 'read'; const ownAttributes = ['*', '!price', '!confidential']; beforeAll(async () => { permit = await permissions.grantPermit({ user, action, resource: 'document', }); // sanity expect(await permit.isOwn(ownResourceId)).toEqual(true); expect(await permit.isOwn(nonOwnResourceId)).toEqual(false); expect(await permit.anyGranted).toEqual(false); expect(await permit.ownGranted).toEqual(true); }); describe(`Permit::attributes():`, () => { it(`returns [] if no resourceId passed`, async () => { expect(await permit.attributes()).toEqual([]); }); it(`returns [] if NOT own resourceId passed`, async () => { expect(await permit.isOwn(nonOwnResourceId)).toEqual(false); expect(await permit.attributes(nonOwnResourceId)).toEqual([]); expect(permit.anyAttributes).toEqual([]); }); it(`returns ownAttributes if own resourceId passed`, async () => { expect(await permit.isOwn(ownResourceId)).toEqual(true); expect(await permit.attributes(ownResourceId)).toEqual(ownAttributes); expect(permit.ownAttributes).toEqual(ownAttributes); }); }); describe(`Permit::pick():`, () => {}); describe(`Permit::filterPick():`, () => {}); }); describe(`on action granted only with 'any' possession:`, () => { let permit2: Permit; const action = 'browse'; const anyAttributes = ['title', 'content']; beforeAll(async () => { permit2 = await permissions.grantPermit({ user, resource: 'document', action, }); }); describe(`Permit::attributes():`, () => { it(`permit.ownAttributes is equal to anyAttributes`, () => { expect(permit2.ownAttributes).toEqual(anyAttributes); }); it(`returns anyAttributes if no resourceId passed`, async () => { expect(await permit2.attributes()).toEqual(anyAttributes); }); it(`returns anyAttributes if NOT own resourceId passed`, async () => { expect(await permit2.isOwn(nonOwnResourceId)).toEqual(false); expect(await permit2.attributes(nonOwnResourceId)).toEqual(anyAttributes); }); it(`returns anyAttributes even if own resourceId passed`, async () => { expect(await permit2.isOwn(ownResourceId)).toEqual(true); expect(await permit2.attributes(ownResourceId)).toEqual(anyAttributes); }); }); describe(`Permit::pick():`, () => {}); describe(`Permit::filterPick():`, () => {}); }); }); describe(`granted to a user having multiple roles:`, () => { const user = USERS.employeeAndSuperAdmin8; const ownResourceId = user.id * 100; describe(`on action granted with both 'own' & 'any' possession, by diff roles:`, () => { const action = 'delete'; const anyAttributes = ['deletedAt']; // from SUPER_ADMIN const ownAttributes = ['*']; // from EMPLOYEE const ownPickedAttributes = [ // from EMPLOYEE 'title', 'content', 'views', 'likes', 'publishDate', 'createDate', 'revision', 'deletedAt', 'withLimits', 'confidential', ]; beforeAll(async () => { permit = await permissions.grantPermit({ user, action, resource: 'document', }); }); describe(`Permit::attributes():`, () => { it(`returns anyAttributes if no resourceId passed`, async () => { expect(await permit.attributes()).toEqual(anyAttributes); }); it(`returns anyAttributes if NOT own resourceId passed`, async () => { expect(await permit.isOwn(nonOwnResourceId)).toEqual(false); expect(await permit.attributes(nonOwnResourceId)).toEqual(anyAttributes); }); it(`returns ownAttributes if own resourceId passed`, async () => { expect(await permit.isOwn(ownResourceId)).toEqual(true); expect(await permit.attributes(ownResourceId)).toEqual(ownAttributes); }); }); describe(`Permit::pick():`, () => { describe(`picks anyAttributes`, () => { it(`when no resourceId passed`, async () => { await pickAndCheckEqualSet(permit, aResourceItem, null, anyAttributes); }); it(`when an non own resourceId passed as 2nd param`, async () => { await pickAndCheckEqualSet(permit, aResourceItem, nonOwnResourceId, anyAttributes); }); it(`when an non own resourceId passed as resourceItem.id`, async () => { await pickAndCheckEqualSet( permit, { ...aResourceItem, id: nonOwnResourceId }, null, anyAttributes ); }); it(`when an non own resourceId passed as 2nd param, even if resourceItem.id is own`, async () => { await pickAndCheckEqualSet( permit, { ...aResourceItem, id: ownResourceId }, nonOwnResourceId, anyAttributes ); }); }); describe(`picks ownAttributes`, () => { it(`when own resourceId passed as 2nd param`, async () => { await pickAndCheckEqualSet( permit, aResourceItem, ownResourceId, ownPickedAttributes ); }); it(`when own resourceId passed as resourceItem.id`, async () => { await pickAndCheckEqualSet(permit, { ...aResourceItem, id: ownResourceId }, null, [ ...ownPickedAttributes, 'id', ]); }); it(`when own resourceId passed as 2nd param, even if resourceItem.id isnt own`, async () => { await pickAndCheckEqualSet( permit, { ...aResourceItem, id: nonOwnResourceId }, ownResourceId, [...ownPickedAttributes, 'id'] ); }); }); }); describe(`Permit::filterPick():`, () => { it(`returns a mapped array of all items, picking the right attributes for each item, based on ownership passed as resourceItem.id`, async () => { await filterPickAndCheckEqualSet( permit, [ createItemWithId(resourceItemKeys, ownResourceId / 10), createItemWithId(resourceItemKeys, nonOwnResourceId), createItemWithId(resourceItemKeys, ownResourceId), createItemWithId(resourceItemKeys, nonOwnResourceId * 10), ], [ createItemWithId(ownPickedAttributes, ownResourceId / 10), createItemWithId(anyAttributes, nonOwnResourceId, false), createItemWithId(ownPickedAttributes, ownResourceId), createItemWithId(anyAttributes, nonOwnResourceId * 10, false), ] ); }); }); }); describe(`on action granted only with 'own' possession, only on one Role:`, () => { const action = 'publish'; const ownAttributes = ['title', 'content', 'createDate'].sort(); // from EMPLOYEE beforeAll(async () => { permit = await permissions.grantPermit({ user, action, resource: 'document', }); }); describe(`Permit::pick():`, () => { it(`returns [] if no resourceId passed`, async () => { expect(await permit.attributes()).toEqual([]); }); it(`returns [] if NOT own resourceId passed`, async () => { expect(await permit.isOwn(nonOwnResourceId)).toEqual(false); expect(await permit.attributes(nonOwnResourceId)).toEqual([]); }); it(`returns ownAttributes if own resourceId passed`, async () => { expect(await permit.isOwn(ownResourceId)).toEqual(true); expect(await permit.attributes(ownResourceId)).toEqual(ownAttributes); }); }); describe(`Permit::pick():`, () => {}); describe(`Permit::filterPick():`, () => {}); }); describe(`on action granted only with 'own' possession on one Role, but 'any' on another with diff attributes:`, () => { const action = 'list'; const mergedAnyAttributes = ['*', '!confidential']; const anyPickedAttributes = [ 'title', 'content', 'views', 'likes', 'publishDate', 'createDate', 'revision', 'deletedAt', 'withLimits', ]; const ownPickedAttributes = [...anyPickedAttributes, 'confidential']; beforeAll(async () => { permit = await permissions.grantPermit({ user, action, resource: 'document', }); }); describe(`Permit::attributes():`, () => { it(`returns mergedAnyAttributes of both roles for any if no resourceId passed`, async () => { expect(await permit.attributes()).toEqual(mergedAnyAttributes); }); it(`returns mergedAnyAttributes of both roles if NOT own resourceId passed`, async () => { expect(await permit.isOwn(nonOwnResourceId)).toEqual(false); expect(await permit.attributes(nonOwnResourceId)).toEqual(mergedAnyAttributes); }); it(`returns own attributes from EMPLOYEE role which override all others, if own resourceId passed`, async () => { expect(await permit.isOwn(ownResourceId)).toEqual(true); expect(await permit.attributes(ownResourceId)).toEqual(['*']); }); }); describe(`Permit::pick():`, () => {}); describe(`Permit::filterPick():`, () => { it(`filters all items & picks mergedAttributes`, async () => { await filterPickAndCheckEqualSet( permit, [ createItemWithId(resourceItemKeys, ownResourceId / 10), createItemWithId(resourceItemKeys, nonOwnResourceId), createItemWithId(resourceItemKeys, ownResourceId), createItemWithId(resourceItemKeys, nonOwnResourceId * 10), createItemWithId(resourceItemKeys, null, false), ], [ createItemWithId(ownPickedAttributes, ownResourceId / 10), createItemWithId(anyPickedAttributes, nonOwnResourceId), createItemWithId(ownPickedAttributes, ownResourceId), createItemWithId(anyPickedAttributes, nonOwnResourceId * 10), createItemWithId(anyPickedAttributes, null, false), ] ); }); }); }); describe(`on action granted only with 'any' possession, by both roles, with diff attributes:`, () => { const action = 'browse'; const anyAttributesEmployee = ['title', 'content']; const anyAttributesSuperAdmin = ['title', 'content', 'views', 'likes']; const mergedAttributes = _.uniq([ ...anyAttributesEmployee, ...anyAttributesSuperAdmin, ]).sort(); beforeAll(async () => { permit = await permissions.grantPermit({ user, action, resource: 'document', }); }); describe(`Permit::pick():`, () => { it(`returns mergedAttributes if no resourceId passed`, async () => { expect(await permit.attributes()).toEqual(mergedAttributes); }); it(`returns mergedAttributes if NOT own resourceId passed`, async () => { expect(await permit.isOwn(nonOwnResourceId)).toEqual(false); expect(await permit.attributes(nonOwnResourceId)).toEqual(mergedAttributes); }); it(`returns mergedAttributes even if own resourceId passed`, async () => { expect(await permit.isOwn(ownResourceId)).toEqual(true); expect(await permit.attributes(ownResourceId)).toEqual(mergedAttributes); }); }); describe(`Permit::pick():`, () => {}); describe(`Permit::filterPick():`, () => { it(`filters all items & picks mergedAttributes`, async () => { await filterPickAndCheckEqualSet( permit, [ createItemWithId(resourceItemKeys, ownResourceId / 10), createItemWithId(resourceItemKeys, nonOwnResourceId), createItemWithId(resourceItemKeys, ownResourceId), createItemWithId(resourceItemKeys, nonOwnResourceId * 10), createItemWithId(resourceItemKeys, null, false), ], [ createItemWithId(mergedAttributes, ownResourceId / 10, false), createItemWithId(mergedAttributes, nonOwnResourceId, false), createItemWithId(mergedAttributes, ownResourceId, false), createItemWithId(mergedAttributes, nonOwnResourceId * 10, false), createItemWithId(mergedAttributes, null, false), ] ); }); }); }); describe(`on action granted only with 'own' possession, by both roles, with diff attributes:`, () => { const action = 'share'; const ownAttributesEmployee = ['title', 'content', 'publishDate']; const ownAttributesSuperAdmin = [ 'title', 'content', 'publishDate', 'createDate', 'revision', ]; const mergedAttributes = _f.flow( _f.uniq, _f.sortBy(_f.identity) )([...ownAttributesEmployee, ...ownAttributesSuperAdmin]); beforeAll(async () => { permit = await permissions.grantPermit({ user, action, resource: 'document', }); }); describe(`Permit::attributes():`, () => { it(`returns [] if no resourceId passed`, async () => { expect(await permit.attributes()).toEqual([]); }); it(`returns [] if NOT own resourceId passed`, async () => { expect(await permit.isOwn(nonOwnResourceId)).toEqual(false); expect(await permit.attributes(nonOwnResourceId)).toEqual([]); }); it(`returns mergedAttributes even if own resourceId passed`, async () => { expect(await permit.isOwn(ownResourceId)).toEqual(true); expect(await permit.attributes(ownResourceId)).toEqual(mergedAttributes); }); }); describe(`Permit::pick():`, () => {}); describe(`Permit::filterPick():`, () => { it(`filters only own items & picks merged ownAttributes`, async () => { await filterPickAndCheckEqualSet( permit, [ createItemWithId(resourceItemKeys, ownResourceId / 10), createItemWithId(resourceItemKeys, nonOwnResourceId), createItemWithId(resourceItemKeys, ownResourceId), createItemWithId(resourceItemKeys, nonOwnResourceId * 10), createItemWithId(resourceItemKeys, null, false), ], [ createItemWithId(mergedAttributes, ownResourceId / 10, false), createItemWithId(mergedAttributes, ownResourceId, false), ] ); }); }); }); }); }); }); });