From 11d2f1786f06024dfbbbfd9ac00e48791cbc0521 Mon Sep 17 00:00:00 2001 From: Derek Burgman Date: Mon, 30 May 2022 03:11:07 -0500 Subject: [PATCH] feat: added grantedRoleMapReader --- packages/model/src/lib/index.ts | 1 + packages/model/src/lib/service/index.ts | 1 + .../model/src/lib/service/permission/index.ts | 1 + .../src/lib/service/permission/role.spec.ts | 61 ++++++++++ .../model/src/lib/service/permission/role.ts | 115 ++++++++++++++++++ packages/util/src/lib/set/set.ts | 2 +- 6 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 packages/model/src/lib/service/index.ts create mode 100644 packages/model/src/lib/service/permission/index.ts create mode 100644 packages/model/src/lib/service/permission/role.spec.ts create mode 100644 packages/model/src/lib/service/permission/role.ts diff --git a/packages/model/src/lib/index.ts b/packages/model/src/lib/index.ts index d5e7ab193..532cf82bd 100644 --- a/packages/model/src/lib/index.ts +++ b/packages/model/src/lib/index.ts @@ -1 +1,2 @@ +export * from './service'; export * from './transform'; diff --git a/packages/model/src/lib/service/index.ts b/packages/model/src/lib/service/index.ts new file mode 100644 index 000000000..f6cc7e4d4 --- /dev/null +++ b/packages/model/src/lib/service/index.ts @@ -0,0 +1 @@ +export * from './permission'; diff --git a/packages/model/src/lib/service/permission/index.ts b/packages/model/src/lib/service/permission/index.ts new file mode 100644 index 000000000..efbebd067 --- /dev/null +++ b/packages/model/src/lib/service/permission/index.ts @@ -0,0 +1 @@ +export * from './role'; diff --git a/packages/model/src/lib/service/permission/role.spec.ts b/packages/model/src/lib/service/permission/role.spec.ts new file mode 100644 index 000000000..588903f46 --- /dev/null +++ b/packages/model/src/lib/service/permission/role.spec.ts @@ -0,0 +1,61 @@ +import { GrantedRoleMap, grantedRoleMapReader, KnownGrantedRole } from './role'; + +const FIRST_ROLE = 'first'; +const SECOND_ROLE = 'second'; + +type TestRoles = KnownGrantedRole | typeof FIRST_ROLE | typeof SECOND_ROLE; + +describe('grantedRoleMapReader()', () => { + const rolesMap: GrantedRoleMap = { + read: true, + first: true + }; + + it('should create a reader for the input.', () => { + const result = grantedRoleMapReader(rolesMap); + expect(result).toBeDefined(); + }); + + describe('reader', () => { + const reader = grantedRoleMapReader(rolesMap); + + describe('hasRole', () => { + it('should return true if the role is granted.', () => { + expect(reader.hasRole('first')).toBe(true); + }); + + it('should return false if the role not is granted.', () => { + expect(reader.hasRole('second')).toBe(false); + }); + }); + + describe('containsRoles', () => { + describe('any', () => { + it('should return true if the role is granted.', () => { + expect(reader.containsRoles('any', 'first')).toBe(true); + }); + + it('should return true if the roles are granted.', () => { + expect(reader.containsRoles('any', ['read', 'first'])).toBe(true); + }); + + it('should return true if any role is granted.', () => { + expect(reader.containsRoles('any', ['read', 'second'])).toBe(true); + }); + + it('should return false if none of the input roles are granted.', () => { + expect(reader.containsRoles('any', 'second')).toBe(false); + }); + }); + describe('all', () => { + it('should return true if all roles are granted.', () => { + expect(reader.containsRoles('all', ['read', 'first'])).toBe(true); + }); + + it('should return false if only some of the input roles are granted.', () => { + expect(reader.containsRoles('all', ['read', 'second'])).toBe(false); + }); + }); + }); + }); +}); diff --git a/packages/model/src/lib/service/permission/role.ts b/packages/model/src/lib/service/permission/role.ts new file mode 100644 index 000000000..3aef2257d --- /dev/null +++ b/packages/model/src/lib/service/permission/role.ts @@ -0,0 +1,115 @@ +import { ArrayOrValue, asArray, Maybe, SetIncludesMode } from '@dereekb/util'; + +/** + * A granted role for a model. + */ +export type GrantedRole = string; + +export const GRANTED_READ_ROLE_KEY = 'read'; + +/** + * Communicates that the current context has read access to a model. + */ +export type GrantedReadRole = typeof GRANTED_READ_ROLE_KEY; + +export type KnownGrantedRole = GrantedReadRole; + +export const GRANTED_FULL_ACCESS_ROLE_KEY = '__FULL__'; + +/** + * Communicates that the current context has full access to a model. + */ +export type GrantedFullAccessGrantedRole = typeof GRANTED_FULL_ACCESS_ROLE_KEY; + +export const NO_ACCESS_ROLES_MAP_KEY = '__EMPTY__'; +export type NoAccessGrantedRole = typeof NO_ACCESS_ROLES_MAP_KEY; + +export type NoAccessRolesMap = { + [NO_ACCESS_ROLES_MAP_KEY]: true; +}; + +export type FullAccessRolesMap = { + [GRANTED_FULL_ACCESS_ROLE_KEY]: true; +}; + +export type GrantedRoleMap = NoAccessRolesMap | FullAccessRolesMap | GrantedRoleKeysMap; + +export type GrantedRoleKeysMap = { + [key in T]?: Maybe; +}; + +export interface GrantedRoleMapReader { + /** + * Returns true if no access has been given. + */ + hasNoAccess(): boolean; + + /** + * Returns true if the role is granted. + */ + hasRole(role: T): boolean; + + /** + * Returns true if the roles are granted. + */ + hasRoles(setIncludes: SetIncludesMode, roles: ArrayOrValue): boolean; + + /** + * Returns true if the map explicitly contains the role. + */ + containsRoles(setIncludes: SetIncludesMode, roles: ArrayOrValue): boolean; +} + +export function grantedRoleMapReader(map: GrantedRoleMap): GrantedRoleMapReader { + return new GrantedRoleMapReaderInstance(map); +} + +export class GrantedRoleMapReaderInstance implements GrantedRoleMapReader { + constructor(private readonly _map: GrantedRoleMap) {} + + hasNoAccess(): boolean { + return (this._map as NoAccessRolesMap)[NO_ACCESS_ROLES_MAP_KEY]; + } + + hasRole(role: T): boolean { + return this.hasRoles('any', role); + } + + hasRoles(setIncludes: SetIncludesMode, inputRoles: ArrayOrValue): boolean { + if ((this._map as FullAccessRolesMap)[GRANTED_FULL_ACCESS_ROLE_KEY]) { + return true; + } else { + return this.containsRoles(setIncludes, inputRoles); + } + } + + containsRoles(setIncludes: SetIncludesMode, inputRoles: ArrayOrValue): boolean { + const roles = asArray(inputRoles); + + if (setIncludes === 'any') { + return this.containsAnyRole(roles); + } else { + return this.containsEachRole(roles); + } + } + + containsAnyRole(roles: GrantedRole[]): boolean { + for (const role of roles) { + if ((this._map as GrantedRoleKeysMap)[role]) { + return true; + } + } + + return false; + } + + containsEachRole(roles: GrantedRole[]): boolean { + for (let role of roles) { + if (!(this._map as GrantedRoleKeysMap)[role]) { + return false; + } + } + + return true; + } +} diff --git a/packages/util/src/lib/set/set.ts b/packages/util/src/lib/set/set.ts index 946d2d49e..37fbcf89f 100644 --- a/packages/util/src/lib/set/set.ts +++ b/packages/util/src/lib/set/set.ts @@ -73,7 +73,7 @@ export function filterValuesFromSet(values: T[], set: Set, exclude = false * - all_reverse: All values must be included in the set (values is a subset of set) * - any: Any value from values is in the set */ -export type SetIncludesMode = 'all' | 'all_reverse' | 'any'; +export type SetIncludesMode = 'all' | 'any'; /** * Contextual function that checks whether or not the input values are included.