@bemedev/permissions
Version:
A library for managing permissions
209 lines (206 loc) • 7.86 kB
JavaScript
import { common } from '@bemedev/core';
import { DELIMITER } from './constants.js';
class Machine {
__config;
#ressources;
#implementation;
#roles;
constructor(__config) {
this.__config = __config;
this.#ressources = Object.freeze(this.__config.ressources);
this.#roles = Object.freeze(this.__config.roles);
}
get config() {
return Object.freeze(this.__config);
}
get roles() {
return Object.freeze(this.#roles);
}
get implementation() {
return Object.freeze(this.#implementation);
}
get __user() {
return common.typings.dynamic();
}
get ressources() {
return this.#ressources;
}
getPriority(role) {
return this.#roles[role];
}
#sortRoles = (...roles) => {
return roles.sort((a, b) => {
return this.getPriority(a) - this.getPriority(b);
});
};
#reverseRoles = (...roles) => {
return roles.sort((a, b) => {
return this.getPriority(b) - this.getPriority(a);
});
};
sortRoles = (order, ...roles) => {
if (order === 'asc')
return this.#sortRoles(...roles);
return this.#reverseRoles(...roles);
};
#implements = (implementation) => {
return (this.#implementation = implementation);
};
/**
* @deprecated
*
* Implements the machine with the provided implementation.
* This method is used to set the implementation for the machine, allowing it to handle permissions
* based on the provided implementation logic.
* @param implementation The implementation logic to be set for the machine.
* @returns A new instance of the machine with the provided implementation.
*/
__implements = (implementation) => {
const _new = new Machine(this.__config);
_new.#implements(implementation);
return _new;
};
#hasUserPermissions = ({ performer, owner, data, action, ressource, }) => {
const sortedRoles = this.#reverseRoles(...performer.roles);
const collecteds = [];
sortedRoles.forEach(role => {
const key = `${String(role)}${DELIMITER}${ressource}${DELIMITER}${action}`;
const permission = this.#implementation[key];
if (common.castings.is.undefined(permission))
return;
if (typeof permission === 'function') {
const result = permission({
owner,
performer,
data,
});
return collecteds.push(result);
}
return collecteds.push(permission);
});
if (collecteds.length === 0)
return false;
return Machine.reduceCollection('or', ...collecteds);
};
#hasDataPermissions = (performer, action, extra) => {
const permissions = this.#extractDataPermissions(extra);
const sortedRoles = this.#reverseRoles(...performer.roles);
if (permissions === true)
return true;
const result1 = permissions[`user:${performer.__id}`]?.[action];
const alreadyTrue = result1 === true;
if (alreadyTrue)
return true;
const collecteds = [];
if (common.castings.is.defined(result1))
collecteds.push(result1);
sortedRoles.forEach(role => {
const _role = String(role);
const value1 = permissions[`role:${_role}`]?.[action];
if (value1)
collecteds.push(value1);
});
if (collecteds.length === 0)
return true;
return Machine.reduceCollection('or', ...collecteds);
};
hasPermisions = args => {
const userPermissions = this.#hasUserPermissions(args);
const dataPermissions = this.#hasDataPermissions(args.performer, args.action, args.data?.__extraPermissions);
const strategy = this.#ressources[args.ressource].__strategy || 'bypass';
return Machine.reduceCollection(strategy, userPermissions, dataPermissions);
};
#extractDataPermissions = (permissions) => {
if (!permissions)
return true;
const entries = common.castings.unknown(Object.entries(permissions));
const allValuesAreNotDefineds = entries.every(([, value]) => common.castings.is.undefined(value));
if (allValuesAreNotDefineds)
return true;
const out = {};
entries.forEach(([action, _permissions]) => {
if (common.castings.is.undefined(_permissions))
return;
const { allow, disallow } = _permissions;
if (allow) {
const allowEntries = Object.entries(allow);
allowEntries.forEach(([dataKey, userIds]) => {
userIds.forEach(userId => {
if (!out[userId]) {
out[userId] = {};
}
if (!out[userId][action]) {
out[userId][action] = [];
}
const currentValue = out[userId][action];
if (dataKey === '**') {
out[userId][action] = true;
}
else if (Array.isArray(currentValue)) {
if (!currentValue.includes(dataKey)) {
currentValue.push(dataKey);
}
}
});
});
}
if (disallow) {
const disallowEntries = Object.entries(disallow);
disallowEntries.forEach(([dataKey, userIds]) => {
if (userIds.length === 0)
return;
userIds.forEach(userId => {
if (!out[userId]) {
out[userId] = {};
}
if (!out[userId][action]) {
out[userId][action] = [];
}
const currentValue = out[userId][action];
if (dataKey === '**') {
out[userId][action] = false;
}
else if (Array.isArray(currentValue)) {
// Remove from allowed if it was there (disallow takes precedence)
const index = currentValue.indexOf(dataKey);
if (index > -1) {
currentValue.splice(index, 1);
}
}
});
});
}
});
return out;
};
static reduceCollection = (strategy, ...collecteds) => {
if (!strategy || strategy === 'bypass')
return collecteds[0];
const isOr = strategy === 'or';
let out;
for (const result of collecteds) {
if (Array.isArray(out)) {
if (Array.isArray(result)) {
if (isOr)
out = [...new Set([...out, ...result])];
else {
out = out.filter(value => result.includes(value));
}
}
if (!isOr && result === false)
return false;
continue;
}
if (result === isOr)
return isOr;
out = result;
}
return out;
};
}
const createMachine = (config, implementation) => {
const machine = new Machine(config).__implements(implementation);
return machine;
};
export { createMachine };
//# sourceMappingURL=machine.js.map