@superawesome/permissions
Version:
Fine grained permissions / access control with ownerships & attribute picking, done right.
140 lines (123 loc) • 5.04 kB
text/typescript
// 3rd
import * as _ from 'lodash';
import * as _f from 'lodash/fp';
import { AccessControl } from 'accesscontrol';
// own
import { buildAccessControl, hasSomeOwnGrant, isArraySetEqual, isLike } from './utils';
import { PermissionDefinitionInternal } from './PermissionDefinitions';
import { getLogger } from './logger';
export const mergeTwoPermissions = (
receivingPD: PermissionDefinitionInternal,
pd: PermissionDefinitionInternal
): PermissionDefinitionInternal => {
// @todo: throw if !canPermissionsBeMerged
const result = {
...receivingPD,
...pd,
grant: {
...receivingPD.grant,
...pd.grant,
},
};
return result;
};
export const areCompatibleOwnHooks = (
pd1: PermissionDefinitionInternal,
pd2: PermissionDefinitionInternal
) =>
(pd1.isOwner === pd2.isOwner || !pd1.isOwner || !pd2.isOwner) &&
(pd1.listOwned === pd2.listOwned || !pd1.listOwned || !pd2.listOwned);
export const consolidatePermissions = _f.reduce(
(consolidatedCPDs: PermissionDefinitionInternal[], cpd: PermissionDefinitionInternal) => {
const matchingPds = _.filter(
consolidatedCPDs,
(consolidatedCpd: PermissionDefinitionInternal) =>
isArraySetEqual(consolidatedCpd.roles, cpd.roles, _.isEqual) &&
consolidatedCpd.resource === cpd.resource &&
areCompatibleOwnHooks(consolidatedCpd, cpd)
);
if (_.isEmpty(matchingPds)) {
consolidatedCPDs.push(cpd);
} else if (matchingPds.length === 1) {
// consolidate the two
const idx = _.indexOf(consolidatedCPDs, matchingPds[0]);
consolidatedCPDs[idx] = mergeTwoPermissions(consolidatedCPDs[idx] as any, cpd as any);
} else
throw new Error(
'Something is wrong with _internalPermissionDefinitions - we have duplicated consolidated PDs'
);
return consolidatedCPDs;
}
);
export const deleteDefinedGrants = _f.reduce(
// refactor, not needs to be reducer, just a visitor to delete grant props
(consolidatedCPDs: PermissionDefinitionInternal[], cpd: PermissionDefinitionInternal) => {
const [accessControl] = _.isEmpty(consolidatedCPDs)
? [new AccessControl()] // dummy
: buildAccessControl(consolidatedCPDs);
// check if grant for all roles is already defined and delete it!
_.each(cpd.grant, (attributes, action) => {
const grantExists = _.every(cpd.roles, (role) => {
try {
const perm = accessControl.permission({
action,
resource: cpd.resource,
role,
});
return perm.granted && _.isEqual(perm.attributes, attributes);
} catch (error) {
return false; // ignore errors of roles missing etc, simply means its not defined
}
});
if (grantExists) delete cpd.grant[action];
});
// add anyway, it will be eliminated if it ends up empty
consolidatedCPDs.push(cpd);
return consolidatedCPDs;
}
);
export const mergeCompatibleGrants = (pds: PermissionDefinitionInternal[]) => {
// for every parent PD, find ones below it that have the a super set with the same exact grants
for (let parentIdx = 0; parentIdx < pds.length; parentIdx++) {
const parentPd = pds[parentIdx];
for (let childIdx = parentIdx + 1; childIdx < pds.length; childIdx++) {
const childPd = pds[childIdx];
if (parentPd.resource === childPd.resource && isLike(parentPd.grant, childPd.grant)) {
// delete ALL child grants that exist in parent (and are seen as moved there)
// add all roles of child to parent
parentPd.roles = _.uniq([...parentPd.roles, ...childPd.roles]);
_.each(_.keys(parentPd.grant), (actionKey) => delete childPd.grant[actionKey]);
}
}
}
return pds;
};
export const consolidatePermissionDefinitions = (filter, consolidateFlag: boolean | 'force') =>
_f.flow(
_f.filter(filter),
_f.tap((ipds: PermissionDefinitionInternal[]) => {
if (_.some(ipds, hasSomeOwnGrant)) {
const msg = `getDefinitions() with consolidate = true PermissionDefinitions is **experimental** and NOT compatible when Possession.own is used ('force' is needed)!
Use "getDefinitions()" with consolidate = false to avoid consolidations.`;
if (consolidateFlag === true)
throw new Error(
`SA-Permissions: ${msg} Use "getDefinitions()" with consolidate = 'force' to proceed with consolidations of own, at your own risk.`
);
if (consolidateFlag === 'force') getLogger().warn(msg);
}
}),
consolidatePermissions([]) as any,
deleteDefinedGrants([]) as any,
mergeCompatibleGrants,
_f.map(
_f.omitBy(
// omit keys of filter & empty ones
(val: any, key) =>
(_.isEmpty(val) && !_.isFunction(val)) ||
(_.isObjectLike(filter) &&
(_.isEqual(filter[key], val) || isArraySetEqual(filter[key], val)))
)
),
// eliminate PDs with empty grants
_f.reject((pd: PermissionDefinitionInternal) => _.isEmpty(pd.grant))
);