import Notation from 'notation' ; import Matcher from 'matcher' ; import { ArrayUtil } from './array' ; import { ConditionUtil } from '../conditions' ; import { AccessControlError , IQueryInfo , IAccessInfo , ICondition } from '../core' ; import cloneDeep from 'lodash.clonedeep' ; export class CommonUtil { public static isStringOrArray ( value ) { return typeof value === 'string' || ArrayUtil . isFilledStringArray ( value ) ; } public static eachKey ( obj , callback ) { return Object . keys ( obj ) . forEach ( callback ) ; } public static someTrue ( elements ) { return elements . some ( ( elm ) => elm ) ; } public static allTrue ( elements ) { return elements . every ( ( elm ) => elm ) ; } public static allFalse ( elements ) { return elements . every ( ( elm ) => ! elm ) ; } public static anyMatch ( strings , patterns ) { const stringArray = ArrayUtil . toStringArray ( strings ) ; const patternArray = ArrayUtil . toStringArray ( patterns ) ; return Matcher ( stringArray , patternArray ) . length !== 0 ; } public static toExtendedJSON ( o ) { return JSON . stringify ( o , function ( key , value ) { if ( typeof value === 'function' ) { return '/Function(' + value . toString ( ) + ')/' ; } return value ; } ) ; } public static fromExtendedJSON ( json ) { return JSON . parse ( json , function ( key , value ) { if ( typeof value === 'string' && value . startsWith ( '/Function(' ) && value . endsWith ( ')/' ) ) { value = value . substring ( 10 , value . length - 2 ) ; return new Function ( 'return ' + value ) ( ) ; } return value ; } ) ; } public static containsPromises ( elements ) { return elements . some ( ( elm ) => { return elm && typeof ( elm . then ) === 'function' && Promise . resolve ( elm ) == elm ; } ) ; } public static clone ( o ) { return cloneDeep ( o ) ; } public static type ( o ) { return Object . prototype . toString . call ( o ) . match ( /\s(\w+)/i ) [ 1 ] . toLowerCase ( ) ; } public static hasDefined ( o , propName ) { return o . hasOwnProperty ( propName ) && o [ propName ] !== undefined ; } /**
     *  Gets roles and extended roles in a flat array.
     */ public static async getFlatRoles ( grants , roles , context ? , skipConditions ? ) { roles = ArrayUtil . toStringArray ( roles ) ; if ( ! roles ) throw new AccessControlError ( `Invalid role(s): ${ JSON . stringify ( roles ) }` ) ; let arr = roles . slice ( ) ; for ( let roleName of roles ) { let roleItem = grants [ roleName ] ; if ( ! roleItem ) throw new AccessControlError ( `Role not found: "${ roleName }"` ) ; if ( roleItem . $extend ) { let rolesMetCondition = [ ] ; if ( skipConditions ) { rolesMetCondition = Object . keys ( roleItem . $extend ) ; } else { for ( let extendedRoleName of Object . keys ( roleItem . $extend ) ) { if ( await ConditionUtil . evaluate ( roleItem . $extend [ extendedRoleName ] . condition , context ) ) { rolesMetCondition . push ( extendedRoleName ) ; } } } arr = ArrayUtil . uniqConcat ( arr , await this . getFlatRoles ( grants , rolesMetCondition , context , skipConditions ) ) ; } } return arr ; } public static getFlatRolesSync ( grants , roles , context ? , skipConditions ? ) { roles = ArrayUtil . toStringArray ( roles ) ; if ( ! roles ) throw new AccessControlError ( `Invalid role(s): ${ JSON . stringify ( roles ) }` ) ; let arr = roles . slice ( ) ; for ( let roleName of roles ) { let roleItem = grants [ roleName ] ; if ( ! roleItem ) throw new AccessControlError ( `Role not found: "${ roleName }"` ) ; if ( roleItem . $extend ) { let rolesMetCondition = [ ] ; if ( skipConditions ) { rolesMetCondition = Object . keys ( roleItem . $extend ) ; } else { for ( let extendedRoleName of Object . keys ( roleItem . $extend ) ) { const conditionResult = ConditionUtil . evaluate ( roleItem . $extend [ extendedRoleName ] . condition , context ) ; if ( typeof ( conditionResult ) !== 'boolean' ) { throw new AccessControlError ( `Expected the condition function should return boolean, but returning ${ conditionResult }` ) ; } else if ( conditionResult === true ) { rolesMetCondition . push ( extendedRoleName ) ; } } } arr = ArrayUtil . uniqConcat ( arr , this . getFlatRolesSync ( grants , rolesMetCondition , context , skipConditions ) ) ; } } return arr ; } public static normalizeGrantsObject ( grants ) { const grantsCopy = this . clone ( grants ) ; for ( let role in grantsCopy ) { if ( ! grantsCopy [ role ] . grants ) { continue ; } grantsCopy [ role ] . grants . forEach ( ( grant ) => { ConditionUtil . validateCondition ( grant . condition ) ; grant . attributes = grant . attributes || [ '*' ] ; } ) ; grantsCopy [ role ] . score = grantsCopy [ role ] . score || 1 ; } return grantsCopy ; } public static normalizeQueryInfo ( query ) { const newQuery = this . clone ( query ) ; newQuery . role = ArrayUtil . toStringArray ( newQuery . role ) ; if ( ! ArrayUtil . isFilledStringArray ( newQuery . role ) ) { throw new AccessControlError ( `Invalid role(s): ${ JSON . stringify ( newQuery . role ) }` ) ; } if ( newQuery . resource ) { if ( typeof newQuery . resource !== 'string' || newQuery . resource . trim ( ) === '' ) { throw new AccessControlError ( `Invalid resource: "${ newQuery . resource }"` ) ; } newQuery . resource = newQuery . resource . trim ( ) ; } if ( newQuery . action ) { if ( typeof newQuery . action !== 'string' || newQuery . action . trim ( ) === '' ) { throw new AccessControlError ( `Invalid action: ${ newQuery . action }` ) ; } } return newQuery ; } public static normalizeAccessInfo ( access ) { const newAccess = this . clone ( access ) ; newAccess . role = ArrayUtil . toStringArray ( newAccess . role ) ; if ( ! ArrayUtil . isFilledStringArray ( newAccess . role ) ) { throw new AccessControlError ( `Invalid role(s): ${ JSON . stringify ( newAccess . role ) }` ) ; } newAccess . resource = ArrayUtil . toStringArray ( newAccess . resource ) ; if ( ! ArrayUtil . isFilledStringArray ( newAccess . resource ) ) { throw new AccessControlError ( `Invalid resource(s): ${ JSON . stringify ( newAccess . resource ) }` ) ; } newAccess . action = ArrayUtil . toStringArray ( newAccess . action ) ; if ( ! ArrayUtil . isFilledStringArray ( newAccess . action ) ) { throw new AccessControlError ( `Invalid resource(s): ${ JSON . stringify ( newAccess . action ) }` ) ; } newAccess . attributes = ! newAccess . attributes ? [ '*' ] : ArrayUtil . toStringArray ( newAccess . attributes ) ; return newAccess ; } param { IAccessInfo } access returns { IAccessInfo } public static resetAttributes ( access ) { if ( ! access . attributes || ArrayUtil . isEmptyArray ( access . attributes ) ) { access . attributes = [ '*' ] ; } return access ; } param { IAccessInfo | IQueryInfo } info returns { Boolean } public static isInfoFulfilled ( info ) { return this . hasDefined ( info , 'role' ) && this . hasDefined ( info , 'action' ) && this . hasDefined ( info , 'resource' ) ; } param { Any } grants param { IAccessInfo } access throws public static commitToGrants ( grants , access ) { access = this . normalizeAccessInfo ( access ) ; ( access . role as Array < string > ) . forEach ( ( role ) => { grants [ role ] = grants [ role ] || { score : 1 } ; grants [ role ] . grants = grants [ role ] . grants || [ ] ; ConditionUtil . validateCondition ( access . condition ) ; grants [ role ] . grants . push ( { resource : access . resource , action : access . action , attributes : access . attributes , condition : access . condition } ) ; } ) ; } public static async getUnionGrantsOfRoles ( grants , query ) { if ( ! grants ) { throw new AccessControlError ( 'Grants are not set.' ) ; } query = this . normalizeQueryInfo ( query ) ; const roles = await this . getFlatRoles ( grants , query . role , query . context , query . skipConditions ) ; return roles . filter ( ( role ) => { return grants [ role ] && grants [ role ] . grants ; } ) . map ( ( role ) => { return grants [ role ] . grants ; } ) . reduce ( ( allGrants , roleGrants ) => { return allGrants . concat ( roleGrants ) ; } , [ ] ) ; } public static getUnionGrantsOfRolesSync ( grants , query ) { if ( ! grants ) { throw new AccessControlError ( 'Grants are not set.' ) ; } query = this . normalizeQueryInfo ( query ) ; const roles = this . getFlatRolesSync ( grants , query . role , query . context , query . skipConditions ) ; return roles . filter ( ( role ) => { return grants [ role ] && grants [ role ] . grants ; } ) . map ( ( role ) => { return grants [ role ] . grants ; } ) . reduce ( ( allGrants , roleGrants ) => { return allGrants . concat ( roleGrants ) ; } , [ ] ) ; } public static async getUnionResourcesOfRoles ( grants , query ) { query . skipConditions = query . skipConditions || ! query . context ; const matchingGrants = ( await this . getUnionGrantsOfRoles ( grants , query ) ) ; return ( await this . filterGrantsAllowing ( matchingGrants , query ) ) . map ( ( grant ) => { return ArrayUtil . toStringArray ( grant . resource ) ; } ) . reduce ( Notation . Glob . union , [ ] ) ; } public static getUnionResourcesOfRolesSync ( grants , query ) { query . skipConditions = query . skipConditions || ! query . context ; const matchingGrants = ( this . getUnionGrantsOfRolesSync ( grants , query ) ) ; return ( this . filterGrantsAllowingSync ( matchingGrants , query ) ) . map ( ( grant ) => { return ArrayUtil . toStringArray ( grant . resource ) ; } ) . reduce ( Notation . Glob . union , [ ] ) ; } public static async getUnionActionsOfRoles ( grants , query ) { query . skipConditions = query . skipConditions || ! query . context ; const matchingGrants = ( await this . getUnionGrantsOfRoles ( grants , query ) ) . filter ( ( grant ) => { return this . anyMatch ( query . resource , grant . resource ) ; } ) ; return ( await this . filterGrantsAllowing ( matchingGrants , query ) ) . map ( ( grant ) => { return ArrayUtil . toStringArray ( grant . action ) ; } ) . reduce ( Notation . Glob . union , [ ] ) ; } public static getUnionActionsOfRolesSync ( grants , query ) { query . skipConditions = query . skipConditions || ! query . context ; const matchingGrants = ( this . getUnionGrantsOfRolesSync ( grants , query ) ) . filter ( ( grant ) => { return this . anyMatch ( query . resource , grant . resource ) ; } ) ; return ( this . filterGrantsAllowingSync ( matchingGrants , query ) ) . map ( ( grant ) => { return ArrayUtil . toStringArray ( grant . action ) ; } ) . reduce ( Notation . Glob . union , [ ] ) ; } param { Any } grants param { IQueryInfo } query returns { Array < String > } public static async getUnionAttrsOfRoles ( grants , query ) { const matchingGrants = ( await this . getUnionGrantsOfRoles ( grants , query ) ) . filter ( ( grant ) => { return this . anyMatch ( query . resource , grant . resource ) && this . anyMatch ( query . action , grant . action ) ; } ) ; return ( await this . filterGrantsAllowing ( matchingGrants , query ) ) . map ( ( grant ) => { return ArrayUtil . toStringArray ( grant . attributes ) ; } ) . reduce ( Notation . Glob . union , [ ] ) ; } public static getUnionAttrsOfRolesSync ( grants , query ) { const matchingGrants = ( this . getUnionGrantsOfRolesSync ( grants , query ) ) . filter ( ( grant ) => { return this . anyMatch ( query . resource , grant . resource ) && this . anyMatch ( query . action , grant . action ) ; } ) ; return ( this . filterGrantsAllowingSync ( matchingGrants , query ) ) . map ( ( grant ) => { return ArrayUtil . toStringArray ( grant . attributes ) ; } ) . reduce ( Notation . Glob . union , [ ] ) ; } public static async filterGrantsAllowing ( grants , query ) { if ( query . skipConditions ) { return grants ; } else { const matchingGrants = [ ] ; for ( let grant of grants ) { if ( await ConditionUtil . evaluate ( grant . condition , query . context ) ) { matchingGrants . push ( grant ) ; } } return matchingGrants ; } } public static filterGrantsAllowingSync ( grants , query ) { if ( query . skipConditions ) { return grants ; } else { const matchingGrants = [ ] ; for ( let grant of grants ) { const conditionResult = query . skipConditions || ConditionUtil . evaluate ( grant . condition , query . context ) ; if ( typeof ( conditionResult ) !== 'boolean' ) { throw new AccessControlError ( `Expected the condition function should return boolean, but returning ${ conditionResult }` ) ; } if ( conditionResult ) { matchingGrants . push ( grant ) ; } } return matchingGrants ; } } public static async areGrantsAllowing ( grants , query ) { if ( ! grants ) { return false ; } let result = false ; for ( let grant of grants ) { result = result || ( this . anyMatch ( query . resource , grant . resource ) && this . anyMatch ( query . action , grant . action ) && ( query . skipConditions || await ConditionUtil . evaluate ( grant . condition , query . context ) ) ) ; } return result ; } public static areGrantsAllowingSync ( grants , query ) { if ( ! grants ) { return false ; } let result = false ; for ( let grant of grants ) { const conditionResult = query . skipConditions || ConditionUtil . evaluate ( grant . condition , query . context ) ; if ( typeof ( conditionResult ) !== 'boolean' ) { throw new AccessControlError ( `Expected the condition function should return boolean, but returning ${ conditionResult }` ) ; } result = result || ( this . anyMatch ( query . resource , grant . resource ) && this . anyMatch ( query . action , grant . action ) && ( query . skipConditions || conditionResult ) ) ; } return result ; } public static async areExtendingRolesAllowing ( roleExtensionObject , allowingRoles , query ) { if ( ! roleExtensionObject ) { return false ; } let result = false ; for ( let roleName in roleExtensionObject ) { result = result || ( allowingRoles [ roleName ] && ( query . skipConditions || await ConditionUtil . evaluate ( roleExtensionObject [ roleName ] . condition , query . context ) ) ) ; } return result ; } public static areExtendingRolesAllowingSync ( roleExtensionObject , allowingRoles , query ) { if ( ! roleExtensionObject ) { return false ; } let result = false ; for ( let roleName in roleExtensionObject ) { const conditionResult = query . skipConditions || ConditionUtil . evaluate ( roleExtensionObject [ roleName ] . condition , query . context ) ; if ( typeof ( conditionResult ) !== 'boolean' ) { throw new AccessControlError ( `Expected the condition function should return boolean, but returning ${ conditionResult }` ) ; } result = result || ( allowingRoles [ roleName ] && ( query . skipConditions || conditionResult ) ) ; } return result ; } public static async getAllowingRoles ( grants , query ) { if ( ! grants ) { throw new AccessControlError ( 'Grants are not set.' ) ; } const allowingRoles = { } ; let sortedRoles = Object . keys ( grants ) . sort ( ( role1 , role2 ) => { return grants [ role1 ] . score - grants [ role2 ] . score ; } ) ; for ( let role of sortedRoles ) { allowingRoles [ role ] = await this . areGrantsAllowing ( grants [ role ] . grants , query ) || await this . areExtendingRolesAllowing ( grants [ role ] . $extend , allowingRoles , query ) ; } return Object . keys ( allowingRoles ) . filter ( ( role ) => { return allowingRoles [ role ] ; } ) ; } public static getAllowingRolesSync ( grants , query ) { if ( ! grants ) { throw new AccessControlError ( 'Grants are not set.' ) ; } const allowingRoles = { } ; let sortedRoles = Object . keys ( grants ) . sort ( ( role1 , role2 ) => { return grants [ role1 ] . score - grants [ role2 ] . score ; } ) ; for ( let role of sortedRoles ) { allowingRoles [ role ] = this . areGrantsAllowingSync ( grants [ role ] . grants , query ) || this . areExtendingRolesAllowingSync ( grants [ role ] . $extend , allowingRoles , query ) ; } return Object . keys ( allowingRoles ) . filter ( ( role ) => { return allowingRoles [ role ] ; } ) ; } param { Any } grants param { Array < string > } roles returns { Array < String > } public static getNonExistentRoles ( grants , roles ) { let non = [ ] ; for ( let role of roles ) { if ( ! grants . hasOwnProperty ( role ) ) non . push ( role ) ; } return non ; } param { Any } grants param { String | Array < String > } roles param { String | Array < String > } extenderRoles param { ICondition } condition throws public static extendRole ( grants , roles , extenderRoles , condition ? ) { ConditionUtil . validateCondition ( condition ) ; CommonUtil . extendRoleSync ( grants , roles , extenderRoles , condition ) ; } public static extendRoleSync ( grants , roles , extenderRoles , condition ? ) { ConditionUtil . validateCondition ( condition ) ; let arrExtRoles = ArrayUtil . toStringArray ( extenderRoles ) ; if ( ! arrExtRoles ) throw new AccessControlError ( `Invalid extender role(s): ${ JSON . stringify ( extenderRoles ) }` ) ; let nonExistentExtRoles = this . getNonExistentRoles ( grants , arrExtRoles ) ; if ( nonExistentExtRoles . length > 0 ) { throw new AccessControlError ( `Cannot extend with non-existent role(s): "${ nonExistentExtRoles . join ( ', ' ) }"` ) ; } roles = ArrayUtil . toStringArray ( roles ) ; if ( ! roles ) throw new AccessControlError ( `Invalid role(s): ${ JSON . stringify ( roles ) }` ) ; const allExtendingRoles = this . getFlatRolesSync ( grants , arrExtRoles , null , true ) ; const extensionScore = allExtendingRoles . reduce ( ( total , role ) => { return total + grants [ role ] . score ; } , 0 ) ; roles . forEach ( ( role ) => { if ( allExtendingRoles . indexOf ( role ) >= 0 ) { throw new AccessControlError ( `Attempted to extend role "${ role }" by itself.` ) ; } grants [ role ] = grants [ role ] || { score : 1 } ; grants [ role ] . score += extensionScore ; grants [ role ] . $extend = grants [ role ] . $extend || { } ; arrExtRoles . forEach ( ( extRole ) => { grants [ role ] . $extend [ extRole ] = grants [ role ] . $extend [ extRole ] || { } ; grants [ role ] . $extend [ extRole ] . condition = condition ; } ) ; } ) ; } public static matchesAllElement ( values , predicateFn ) { values = ArrayUtil . toArray ( values ) ; return values . every ( predicateFn ) ; } public static matchesAnyElement ( values , predicateFn ) { values = ArrayUtil . toArray ( values ) ; return values . some ( predicateFn ) ; } public static filter ( object , attributes ) { if ( ! Array . isArray ( attributes ) || attributes . length === 0 ) { return { } ; } let notation = new Notation ( object ) ; return notation . filter ( attributes ) . value ; } public static filterAll ( arrOrObj , attributes ) { if ( ! Array . isArray ( arrOrObj ) ) { return this . filter ( arrOrObj , attributes ) ; } return arrOrObj . map ( o => { return this . filter ( o , attributes ) ; } ) ; } }