This commit is contained in:
lovebird 2026-02-17 20:34:05 +01:00
parent 17cef737e0
commit 4bb566b1b7
9 changed files with 5466 additions and 0 deletions

34
packages/acl/.gitignore vendored Normal file
View File

@ -0,0 +1,34 @@
# Deno
.deno
deno.lock
deno.json._decorators
# Dependencies
node_modules/
# Build output
build/
out/
# Environment files
.env*
!src/.env/
!src/.env/*md
# Generated files
.dts
types/
.D_Store
.vscode/!settings.json
# Logs
*.Log
*.Log.*
docs-internal
systems/code-server-defaults
systems/workspace/kbot-docs
systems/.code-server/code-server-ipc.sock
systems/.code-server/User/workspaceStorage/
systems/code-server-defaults
systems/.code-server
tests/assets/
packages/kbot/systems/gptr/gpt-researcher

4183
packages/acl/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

50
packages/acl/package.json Normal file
View File

@ -0,0 +1,50 @@
{
"name": "@polymech/acl",
"version": "0.1.5",
"type": "module",
"publishConfig": {
"access": "public"
},
"author": "Polymech",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://git.polymech.io/polymech/mono.git",
"directory": "packages/acl"
},
"bin": {
"pm-acl": "./dist-in/main.js"
},
"exports": {
".": "./dist-in/index.js"
},
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsc -p . --watch",
"lint": "eslint src --ext .ts"
},
"dependencies": {
"@polymech/cache": "file:../cache",
"@polymech/commons": "file:../commons",
"@polymech/core": "file:../core",
"@polymech/fs": "file:../fs",
"@polymech/log": "file:../log",
"p-map": "7.0.3",
"p-throttle": "7.0.0",
"ts-retry": "6.0.0",
"yargs": "17.7.2",
"zod": "3.24.3"
},
"keywords": [],
"devDependencies": {
"@repo/typescript-config": "file:../typescript-config",
"@types/node": "22.10.2",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"@vitest/coverage-v8": "^2.1.8",
"@vitest/ui": "2.1.9",
"typescript": "^5.7.2",
"zod-to-json-schema": "3.24.1"
}
}

745
packages/acl/src/ACLC.ts Normal file
View File

@ -0,0 +1,745 @@
/*
ACL System inspired on Zend_ACL.
All functions accept strings, objects or arrays unless specified otherwise.
'*' is used to express 'all'
Database structure in Redis (using default prefix 'acl')
Users:
acl_roles_{userid} = set(roles)
Roles:
acl_roles = {roleNames} // Used to remove all the permissions associated to ONE resource.
acl_parents_{roleName} = set(parents)
acl_resources_{roleName} = set(resourceNames)
Permissions:
acl_allows_{resourceName}_{roleName} = set(permissions)
Note: user ids, role names and resource names are all case sensitive.
Roadmap:
- Add support for locking resources. If a user has roles that gives him permissions to lock
a resource, then he can get exclusive write operation on the locked resource.
This lock should expire if the resource has not been accessed in some time.
*/
import * as _ from 'lodash';
import * as bluebird from 'bluebird';
import { contract } from './contract';
import { ILogger, IAcl, IBucketsOption, IBackend, Action } from './interfaces';
import { Value, Values, strings, Callback, AnyCallback, AllowedCallback } from './interfaces';
contract.debug = true;
function makeArray(arr: any | Object): Array<any> {
return Array.isArray(arr) ? arr : [arr];
}
function allowsBucket(role: string): string {
return 'allows_' + role;
}
function keyFromAllowsBucket(str: string): string {
return str.replace(/^allows_/, '');
}
export class ACL implements IAcl {
logger: any;
backend: any;
options: any;
constructor(backend: IBackend<Action[]>, logger: ILogger | any, options?: any) {
const buckets: IBucketsOption = {
meta: 'meta',
parents: 'parents',
permissions: 'permissions',
resources: 'resources',
roles: 'roles',
users: 'users'
};
options = _.extend({
buckets: buckets
}, options);
this.logger = logger;
this.backend = backend;
this.options = options;
}
public allow(roles: Values, resources: strings, permissions: strings, cb?: Callback): Promise<void> {
contract(arguments)
.params('string|array', 'string|array', 'string|array', 'function')
.params('string|array', 'string|array', 'string|array')
.params('array', 'function')
.params('array')
.end();
if ((arguments.length === 1) || ((arguments.length === 2) && _.isObject(roles) && _.isFunction(resources))) {
return this._allowEx(roles).nodeify(resources);
} else {
let _this = this;
roles = makeArray(roles);
resources = makeArray(resources);
let backend = _this.backend;
let transaction = _this.backend.begin();
backend.add(transaction, _this.options.buckets.meta, 'roles', roles);
resources.forEach(function (resource) {
_.each(roles, (role) => {
backend.add(transaction, allowsBucket(resource), role, permissions);
});
});
roles.forEach(function (role) {
_this.backend.add(transaction, _this.options.buckets.resources, role, resources);
});
return backend.endAsync(transaction).nodeify(cb);
}
}
/* addUserRoles( userId, roles, function(err) )
* Adds roles to a given user id.
@param {String|Number} User id.
@param {String|Array} Role(s) to add to the user id.
@param {Function} Callback called when finished.
@return {Promise} Promise resolved when finished
*/
public addUserRoles(userId: Value, roles: strings, cb?: Callback): Promise<void> {
contract(arguments)
.params('string|number', 'string|array', 'function')
.params('string|number', 'string|array')
.end();
let transaction = this.backend.begin();
this.backend.add(transaction, this.options.buckets.meta, 'users', userId);
this.backend.add(transaction, this.options.buckets.users, userId, roles);
if (Array.isArray(roles)) {
let _this = this;
roles.forEach(function (role) {
_this.backend.add(transaction, _this.options.buckets.roles, role, userId);
});
}
else {
this.backend.add(transaction, this.options.buckets.roles, roles, userId);
}
return this.backend.endAsync(transaction).nodeify(cb);
}
/*
removeUserRoles( userId, roles, function(err) )
Remove roles from a given user.
@param {String|Number} User id.
@param {String|Array} Role(s) to remove to the user id.
@param {Function} Callback called when finished.
@return {Promise} Promise resolved when finished
*/
public removeUserRoles(userId: Value, roles: strings, cb?: Callback): Promise<void> {
contract(arguments)
.params('string|number', 'string|array', 'function')
.params('string|number', 'string|array')
.end();
let transaction = this.backend.begin();
this.backend.remove(transaction, this.options.buckets.users, userId, roles);
if (Array.isArray(roles)) {
let _this = this;
roles.forEach(function (role) {
_this.backend.remove(transaction, _this.options.buckets.roles, role, userId);
});
}
else {
this.backend.remove(transaction, this.options.buckets.roles, roles, userId);
}
return this.backend.endAsync(transaction).nodeify(cb);
}
/*
userRoles( userId, function(err, roles) )
Return all the roles from a given user.
@param {String|Number} User id.
@param {Function} Callback called when finished.
@return {Promise} Promise resolved with an array of user roles
*/
public userRoles(userId: Value, cb?: (err: Error, roles: string[]) => any): bluebird<string[]> {
return this.backend.getAsync(this.options.buckets.users, userId).nodeify(cb);
}
/*
roleUsers( roleName, function(err, users) )
Return all users who has a given role.
@param {String|Number} rolename.
@param {Function} Callback called when finished.
@return {Promise} Promise resolved with an array of users
*/
public roleUsers(role: Value, cb?: (err: Error, users: Values) => any): Promise<any> {
return this.backend.getAsync(this.options.buckets.roles, role).nodeify(cb);
}
/*
hasRole( userId, rolename, function(err, is_in_role) )
Return boolean whether user is in the role
@param {String|Number} User id.
@param {String|Number} rolename.
@param {Function} Callback called when finished.
@return {Promise} Promise resolved with boolean of whether user is in role
*/
public hasRole(userId: Value, role: string, cb?: (err: Error, isInRole: boolean) => any): bluebird<boolean> {
return this.userRoles(userId).then(function (roles) {
return roles.indexOf(role) !== -1;
}).nodeify(cb);
}
/*
addRoleParents( role, parents, function(err) )
Adds a parent or parent list to role.
@param {String} Child role.
@param {String|Array} Parent role(s) to be added.
@param {Function} Callback called when finished.
@return {Promise} Promise resolved when finished
*/
public addRoleParents(role: string, parents: Values, cb?: Callback): Promise<void> {
contract(arguments)
.params('string|number', 'string|array', 'function')
.params('string|number', 'string|array')
.end();
let transaction = this.backend.begin();
this.backend.add(transaction, this.options.buckets.meta, 'roles', role);
this.backend.add(transaction, this.options.buckets.parents, role, parents);
return this.backend.endAsync(transaction).nodeify(cb);
};
/*
removeRoleParents( role, parents, function(err) )
Removes a parent or parent list from role.
If `parents` is not specified, removes all parents.
@param {String} Child role.
@param {String|Array} Parent role(s) to be removed [optional].
@param {Function} Callback called when finished [optional].
@return {Promise} Promise resolved when finished.
*/
public removeRoleParents(role: any, parents: Function, cb: Function) {
contract(arguments)
.params('string', 'string|array', 'function')
.params('string', 'string|array')
.params('string', 'function')
.params('string')
.end();
if (!cb && _.isFunction(parents)) {
cb = parents;
parents = null;
}
let transaction = this.backend.begin();
if (parents) {
this.backend.remove(transaction, this.options.buckets.parents, role, parents);
} else {
this.backend.del(transaction, this.options.buckets.parents, role);
}
return this.backend.endAsync(transaction).nodeify(cb);
}
/*
removeRole( role, function(err) )
Removes a role from the system.
@param {String} Role to be removed
@param {Function} Callback called when finished.
*/
public removeRole(role: string, cb?: Callback): Promise<void> {
contract(arguments)
.params('string', 'function')
.params('string').end();
let _this = this;
// Note that this is not fully transactional.
return this.backend.getAsync(this.options.buckets.resources, role).then((resources) => {
let transaction = _this.backend.begin();
resources.forEach((resource) => {
let bucket = allowsBucket(resource);
_this.backend.del(transaction, bucket, role);
});
_this.backend.del(transaction, _this.options.buckets.resources, role);
_this.backend.del(transaction, _this.options.buckets.parents, role);
_this.backend.del(transaction, _this.options.buckets.roles, role);
_this.backend.remove(transaction, _this.options.buckets.meta, 'roles', role);
// `users` collection keeps the removed role
// because we don't know what users have `role` assigned.
return _this.backend.endAsync(transaction);
}).nodeify(cb);
};
/*
removeResource( resource, function(err) )
Removes a resource from the system
@param {String} Resource to be removed
@param {Function} Callback called when finished.
@return {Promise} Promise resolved when finished
*/
public removeResource(resource: string, cb?: Callback): Promise<void> {
contract(arguments)
.params('string', 'function')
.params('string')
.end();
let _this = this;
return this.backend.getAsync(this.options.buckets.meta, 'roles').then(function (roles: Array<String>) {
let transaction = _this.backend.begin();
_this.backend.del(transaction, allowsBucket(resource), roles);
roles.forEach((role) => {
_this.backend.remove(transaction, _this.options.buckets.resources, role, resource);
});
return _this.backend.endAsync(transaction);
}).nodeify(cb);
}
/*
allow( roles, resources, permissions, function(err) )
Adds the given permissions to the given roles over the given resources.
@param {String|Array} role(s) to add permissions to.
@param {String|Array} resource(s) to add permisisons to.
@param {String|Array} permission(s) to add to the roles over the resources.
@param {Function} Callback called when finished.
allow( permissionsArray, function(err) )
@param {Array} Array with objects expressing what permissions to give.
[{roles:{String|Array}, allows:[{resources:{String|Array}, permissions:{String|Array}]]
@param {Function} Callback called when finished.
@return {Promise} Promise resolved when finished
*/
public removeAllow(role: string, resources: strings, permissions: strings, cb?: Callback): Promise<void> {
contract(arguments)
.params('string', 'string|array', 'string|array', 'function')
.params('string', 'string|array', 'string|array')
.params('string', 'string|array', 'function')
.params('string', 'string|array')
.end();
resources = makeArray(resources);
if (cb || (permissions && !_.isFunction(permissions))) {
permissions = makeArray(permissions);
} else {
cb = (permissions as any);
permissions = null;
}
return this.removePermissions(role, resources, permissions, cb);
};
/*
removePermissions( role, resources, permissions)
Remove permissions from the given roles owned by the given role.
Note: we loose atomicity when removing empty role_resources.
@param {String}
@param {String|Array}
@param {String|Array}
*/
public removePermissions(role: string, resources: strings, permissions: strings, cb?: Function): Promise<void> {
let _this = this;
let transaction = _this.backend.begin();
_.each(resources, resource => {
let bucket = allowsBucket(resource);
if (permissions) {
_this.backend.remove(transaction, bucket, role, permissions);
} else {
_this.backend.del(transaction, bucket, role);
_this.backend.remove(transaction, _this.options.buckets.resources, role, resource);
}
});
// Remove resource from role if no rights for that role exists.
// Not fully atomic...
return _this.backend.endAsync(transaction).then(function () {
let transaction = _this.backend.begin();
return bluebird.all(_.map(resources, resource => {
let bucket = allowsBucket(resource);
return _this.backend.getAsync(bucket, role).then(function (permissions) {
if (permissions.length === 0) {
_this.backend.remove(transaction, _this.options.buckets.resources, role, resource);
}
});
})).then(function () {
return _this.backend.endAsync(transaction);
});
}).nodeify(cb);
};
/*
allowedPermissions( userId, resources, function(err, obj) )
Returns all the allowable permissions a given user have to
access the given resources.
It returns an array of objects where every object maps a
resource name to a list of permissions for that resource.
@param {String|Number} User id.
@param {String|Array} resource(s) to ask permissions for.
@param {Function} Callback called when finished.
*/
public allowedPermissions(userId: Value, resources: strings, cb?: AnyCallback): bluebird<any> {
if (!userId) {
return cb(null, {});
}
contract(arguments)
.params('string|number', 'string|array', 'function')
.params('string|number', 'string|array')
.end();
if (this.backend.unionsAsync) {
return this.optimizedAllowedPermissions(userId, resources, cb);
}
let _this = this;
resources = makeArray(resources);
return this.userRoles(userId).then(roles => {
let result = {};
return bluebird.all(_.map(resources, resource => {
return _this._resourcePermissions(roles, resource).then(function (permissions) {
result[resource] = permissions;
});
})).then(function () {
return result;
});
}).nodeify(cb);
}
/*
optimizedAllowedPermissions( userId, resources, function(err, obj) )
Returns all the allowable permissions a given user have to
access the given resources.
It returns a map of resource name to a list of permissions for that resource.
This is the same as allowedPermissions, it just takes advantage of the unions
function if available to reduce the number of backend queries.
@param {String|Number} User id.
@param {String|Array} resource(s) to ask permissions for.
@param {Function} Callback called when finished.
*/
public optimizedAllowedPermissions(userId, resources, cb) {
if (!userId) {
return cb(null, {});
}
contract(arguments)
.params('string|number', 'string|array', 'function|undefined')
.params('string|number', 'string|array')
.end();
resources = makeArray(resources);
let self = this;
return this._allUserRoles(userId).then(function (roles) {
let buckets = resources.map(allowsBucket);
if (roles.length === 0) {
let emptyResult = {};
buckets.forEach(function (bucket) {
emptyResult[bucket] = [];
});
return bluebird.resolve(emptyResult);
}
return self.backend.unionsAsync(buckets, roles);
}).then(function (response) {
let result = {};
Object.keys(response).forEach(function (bucket) {
result[keyFromAllowsBucket(bucket)] = response[bucket];
});
return result;
}).nodeify(cb);
}
/*
isAllowed( userId, resource, permissions, function(err, allowed) )
Checks if the given user is allowed to access the resource for the given
permissions (note: it must fulfill all the permissions).
@param {String|Number} User id.
@param {String|Array} resource(s) to ask permissions for.
@param {String|Array} asked permissions.
@param {Function} Callback called wish the result.
*/
public isAllowed(userId: Value, resource: strings, permissions: strings, cb?: AllowedCallback): Promise<boolean> {
contract(arguments)
.params('string|number', 'string', 'string|array', 'function')
.params('string|number', 'string', 'string|array')
.end();
let _this = this;
return this.backend.getAsync(this.options.buckets.users, userId).then(function (roles) {
if (roles.length) {
return _this.areAnyRolesAllowed(roles, resource, permissions);
} else {
return false;
}
}).nodeify(cb);
}
/*
areAnyRolesAllowed( roles, resource, permissions, function(err, allowed) )
Returns true if any of the given roles have the right permissions.
@param {String|Array} Role(s) to check the permissions for.
@param {String} resource(s) to ask permissions for.
@param {String|Array} asked permissions.
@param {Function} Callback called with the result.
*/
public areAnyRolesAllowed(roles: strings, resource: strings, permissions: strings, cb?: AllowedCallback): Promise<boolean> {
contract(arguments)
.params('string|array', 'string', 'string|array', 'function')
.params('string|array', 'string', 'string|array')
.end();
roles = makeArray(roles);
permissions = makeArray(permissions);
if (roles.length === 0) {
return (bluebird as any).resolve(false).nodeify(cb);
} else {
return this._checkPermissions(roles, resource, permissions).nodeify(cb);
}
}
/*
whatResources(role, function(err, {resourceName: [permissions]})
Returns what resources a given role or roles have permissions over.
whatResources(role, permissions, function(err, resources) )
Returns what resources a role has the given permissions over.
@param {String|Array} Roles
@param {String[Array} Permissions
@param {Function} Callback called wish the result.
*/
public whatResources(roles: strings, permissions: strings, cb?: AnyCallback): Promise<any> {
contract(arguments)
.params('string|array')
.params('string|array', 'string|array')
.params('string|array', 'function')
.params('string|array', 'string|array', 'function')
.end();
roles = makeArray(roles);
if (_.isFunction(permissions)) {
cb = (<any>permissions);
permissions = undefined;
} else if (permissions) {
permissions = makeArray(permissions);
}
return this.permittedResources(roles, permissions, cb);
}
public permittedResources(roles: strings, permissions: strings, cb?: Function): Promise<void> {
let _this = this;
let result: any = _.isUndefined(permissions) ? {} : [];
return this._rolesResources(roles).then(function (resources) {
return bluebird.all(resources.map(function (resource) {
return _this._resourcePermissions(roles, resource).then(function (p) {
if (permissions) {
let commonPermissions = _.intersection(permissions, p);
if (commonPermissions.length > 0) {
result.push(resource);
}
} else {
result[resource] = p;
}
});
})).then(function () {
return result;
});
}).nodeify(cb);
}
// -----------------------------------------------------------------------------
//
// Private methods
//
// -----------------------------------------------------------------------------
//
// Same as allow but accepts a more compact input.
//
private _allowEx(objs): any {
let _this = this;
objs = makeArray(objs);
let demuxed = [];
objs.forEach(function (obj) {
let roles = obj.roles;
obj.allows.forEach(function (allow) {
demuxed.push({
roles: roles,
resources: allow.resources,
permissions: allow.permissions
});
});
});
return bluebird.reduce(demuxed, function (values, obj) {
const roles: Values = obj.roles;
const resources: strings = obj.resources;
const permissions: strings = obj.permissions;
return _this.allow(roles, resources, permissions, null);
}, null);
}
//
// Returns the parents of the given roles
//
private _rolesParents(roles) {
return this.backend.unionAsync(this.options.buckets.parents, roles);
}
//
// Return all roles in the hierarchy including the given roles.
//
/*
Acl.prototype._allRoles = function(roleNames, cb){
var _this = this, roles;
_this._rolesParents(roleNames, function(err, parents){
roles = _.union(roleNames, parents);
async.whilst(
function (){
return parents.length >0;
},
function (cb) {
_this._rolesParents(parents, function(err, result){
if(!err){
roles = _.union(roles, parents);
parents = result;
}
cb(err);
});
},
function(err){
cb(err, roles);
}
);
});
};
*/
//
// Return all roles in the hierarchy including the given roles.
//
private _allRoles(roleNames) {
let _this = this;
return this._rolesParents(roleNames).then(function (parents) {
if (parents.length > 0) {
return _this._allRoles(parents).then(function (parentRoles) {
return _.union(roleNames, parentRoles);
});
} else {
return roleNames;
}
});
}
//
// Return all roles in the hierarchy of the given user.
//
private _allUserRoles(userId) {
let _this = this;
return this.userRoles(userId).then(function (roles) {
if (roles && roles.length > 0) {
return _this._allRoles(roles);
} else {
return [];
}
});
}
//
// Returns an array with resources for the given roles.
//
private _rolesResources(roles) {
let _this = this;
roles = makeArray(roles);
return this._allRoles(roles).then(function (allRoles) {
let result = [];
// check if bluebird.map simplifies this code
return bluebird.all(allRoles.map(function (role) {
return _this.backend.getAsync(_this.options.buckets.resources, role).then(function (resources) {
result = result.concat(resources);
});
})).then(function () {
return result;
});
});
}
//
// Returns the permissions for the given resource and set of roles
//
private _resourcePermissions(roles, resource): any {
let _this = this;
if (roles.length === 0) {
return bluebird.resolve([]);
} else {
return this.backend.unionAsync(allowsBucket(resource), roles).then(function (resourcePermissions) {
return _this._rolesParents(roles).then(function (parents) {
if (parents && parents.length) {
return _this._resourcePermissions(parents, resource).then(function (morePermissions) {
return _.union(resourcePermissions, morePermissions);
});
} else {
return resourcePermissions;
}
});
});
}
}
//
// NOTE: This function will not handle circular dependencies and result in a crash.
//
private _checkPermissions(roles, resource, permissions): any {
let _this = this;
return this.backend.unionAsync(allowsBucket(resource), roles).then(function (resourcePermissions) {
if (resourcePermissions.indexOf('*') !== -1) {
return true;
} else {
permissions = permissions.filter(function (p) {
return resourcePermissions.indexOf(p) === -1;
});
if (permissions.length === 0) {
return true;
} else {
return _this.backend.unionAsync(_this.options.buckets.parents, roles).then(function (parents) {
if (parents && parents.length) {
return _this._checkPermissions(parents, resource, permissions);
} else {
return false;
}
});
}
}
});
}
};
// -----------------------------------------------------------------------------
//
// Helpers
//
// -----------------------------------------------------------------------------

View File

@ -0,0 +1,87 @@
/*
Backend Interface.
Implement this API for providing a backend for the acl module.
*/
import { contract } from './contract';
import { IBucketsOption, Action, Value, Values, IBackend } from './interfaces';
export let Backend = {
/*
Begins a transaction.
*/
begin: function () {
// returns a transaction object
},
/*
Ends a transaction (and executes it)
*/
end: function (transaction, cb) {
contract(arguments).params('object', 'function').end();
// Execute transaction
},
/*
Cleans the whole storage.
*/
clean: function (cb) {
contract(arguments).params('function').end();
},
/*
Gets the contents at the bucket's key.
*/
get: function (bucket, key, cb) {
contract(arguments)
.params('string', 'string|number', 'function')
.end();
},
/*
Gets the union of contents of the specified keys in each of the specified buckets and returns
a mapping of bucket to union.
*/
unions: function (bucket, keys, cb) {
contract(arguments)
.params('array', 'array', 'function')
.end();
},
/*
Returns the union of the values in the given keys.
*/
union: function (bucket, keys, cb) {
contract(arguments)
.params('string', 'array', 'function')
.end();
},
/*
Adds values to a given key inside a bucket.
*/
add: function (transaction, bucket, key, values) {
contract(arguments)
.params('object', 'string', 'string|number', 'string|array|number')
.end();
},
/*
Delete the given key(s) at the bucket
*/
del: function (transaction, bucket, keys) {
contract(arguments)
.params('object', 'string', 'string|array')
.end();
},
/*
Removes values from a given key inside a bucket.
*/
remove: function (transaction, bucket, key, values) {
contract(arguments)
.params('object', 'string', 'string|number', 'string|array|number')
.end();
}
};

View File

@ -0,0 +1,51 @@
import { IObjectLiteral } from '../../interfaces/index';
import { IStoreIO } from '../../interfaces/Store';
import { Memory } from './Memory';
import * as fs from 'fs';
import * as mkdirp from 'mkdirp';
import * as _path from 'path';
const writeFileAtomic = require('write-file-atomic');
const permissionError = 'You don\'t have access to this file.';
const defaultPathMode: number = parseInt('0700', 8);
const writeFileOptions: IObjectLiteral = { mode: parseInt('0600', 8) };
export class File extends Memory implements IStoreIO {
configPath: string;
read(path?: string): any {
path = path || this.configPath;
try {
this._buckets = JSON.parse(fs.readFileSync(path, 'utf8'));
} catch (err) {
// create dir if it doesn't exist
if (err.code === 'ENOENT') {
mkdirp.sync(_path.dirname(path), defaultPathMode);
return {};
}
// improve the message of permission errors
if (err.code === 'EACCES') {
err.message = err.message + '\n' + permissionError + '\n';
}
// empty the file if it encounters invalid JSON
if (err.name === 'SyntaxError') {
writeFileAtomic.sync(path, '', writeFileOptions);
return {};
}
throw err;
}
}
write(path?: string): any {
path = path || this.configPath;
const data = this.data();
try {
// make sure the folder exists as it
// could have been deleted in the meantime
mkdirp.sync(_path.dirname(path), defaultPathMode);
writeFileAtomic.sync(path, JSON.stringify(data, null, 4), writeFileOptions);
} catch (err) {
// improve the message of permission errors
if (err.code === 'EACCES') {
err.message = err.message + '\n' + permissionError + '\n';
}
throw err;
}
}
}

View File

@ -0,0 +1,178 @@
/*
Memory Backend.
In-memory implementation of the storage.
*/
import * as _ from 'lodash';
import { contract } from '../contract';
import { Action, IBackend, Value, Values } from '../interfaces';
import { IObjectLiteral, List } from '../../interfaces/index';
import * as bluebird from 'bluebird';
function makeArray(arr) {
return Array.isArray(arr) ? arr : [arr];
}
export class Memory implements IBackend<Action[]> {
endAsync = bluebird.promisify(this.end);
getAsync = bluebird.promisify(this.get);
cleanAsync = bluebird.promisify(this.clean);
unionAsync = bluebird.promisify(this.union);
unionsAsync = bluebird.promisify(this.unions);
_buckets: IObjectLiteral;
data() {
return this._buckets as Action[];
}
constructor() {
this._buckets = {};
}
/*
Begins a transaction.
*/
begin() {
// returns a transaction object(just an array of functions will do here.)
return [];
};
/*
Ends a transaction (and executes it)
*/
end(transaction: Action[], cb: Action) {
contract(arguments).params('array', 'function').end();
// Execute transaction
for (let i = 0, len = transaction.length; i < len; i++) {
transaction[i]();
}
cb();
}
/*
Cleans the whole storage.
*/
clean(cb: Action) {
contract(arguments).params('function').end();
this._buckets = {};
cb();
}
/*
Gets the contents at the bucket's key.
*/
get(bucket: string, key: Value, cb: Action) {
contract(arguments)
.params('string', 'string|number', 'function')
.end();
if (this._buckets[bucket]) {
(cb as Function)(null, this._buckets[bucket][key] || []);
} else {
(cb as Function)(null, []);
}
}
/*
Gets the union of the keys in each of the specified buckets
*/
unions(buckets, keys, cb) {
contract(arguments)
.params('array', 'array', 'function')
.end();
const self = this;
let results = {};
buckets.forEach(function (bucket) {
if (self._buckets[bucket]) {
results[bucket] = _.uniq(_.flatten(_.values(_.pick(self._buckets[bucket], keys))));
} else {
results[bucket] = [];
}
});
cb(null, results);
}
/*
Returns the union of the values in the given keys.
*/
union(bucket: string, keys: Values[], cb: Action): void {
contract(arguments)
.params('string', 'array', 'function')
.end();
let match;
let re;
if (!this._buckets[bucket]) {
Object.keys(this._buckets).some(function (b) {
re = new RegExp("^" + b + "$");
match = re.test(bucket);
if (match) { bucket = b; }
return match;
});
}
if (this._buckets[bucket]) {
const keyArrays = [];
for (let i = 0, len = keys.length; i < len; i++) {
if (this._buckets[bucket][keys[i] as Value]) {
keyArrays.push.apply(keyArrays, this._buckets[bucket][keys[i] as Value]);
}
}
(cb as Function)(undefined, _.union(keyArrays));
} else {
(cb as Function)(undefined, []);
}
}
/*
Adds values to a given key inside a bucket.
*/
add(transaction: Action[], bucket: string, key: Value, values: Values) {
contract(arguments)
.params('array', 'string', 'string|number', 'string|array|number')
.end();
const self = this;
values = makeArray(values);
transaction.push(function () {
if (!self._buckets[bucket]) {
self._buckets[bucket] = {};
}
if (!self._buckets[bucket][key]) {
self._buckets[bucket][key] = values;
} else {
self._buckets[bucket][key] = _.union(values as List<Value>, self._buckets[bucket][key]);
}
});
}
/*
Delete the given key(s) at the bucket
*/
del(transaction: Action[], bucket: string, keys: Values) {
contract(arguments)
.params('array', 'string', 'string|array')
.end();
const self = this;
keys = makeArray(keys);
transaction.push(function () {
if (self._buckets[bucket]) {
for (let i = 0, len = (keys as List<Value>).length; i < len; i++) {
delete self._buckets[bucket][keys[i]];
}
}
});
}
/*
Removes values from a given key inside a bucket.
*/
remove(transaction: Action[], bucket: string, key: Value, values: Values) {
contract(arguments)
.params('array', 'string', 'string|number', 'string|array|number')
.end();
const self = this;
values = makeArray(values);
transaction.push(function () {
let old;
if (self._buckets[bucket] && (old = self._buckets[bucket][key])) {
self._buckets[bucket][key] = _.difference(old, values as List<Value>);
}
});
}
}

View File

@ -0,0 +1,109 @@
/**
* @polymech/acl Type definitions
*
* Pure ESM, zero external dependencies.
* All methods are async (native Promise).
*/
// ---------------------------------------------------------------------------
// Primitives
// ---------------------------------------------------------------------------
export type Value = string | number;
export type Values = Value | Value[];
// ---------------------------------------------------------------------------
// Bucket naming
// ---------------------------------------------------------------------------
export interface BucketNames {
readonly meta: string;
readonly parents: string;
readonly permissions: string;
readonly resources: string;
readonly roles: string;
readonly users: string;
}
// ---------------------------------------------------------------------------
// ACL Options
// ---------------------------------------------------------------------------
export interface AclOptions {
buckets?: Partial<BucketNames>;
}
// ---------------------------------------------------------------------------
// Backend interface — purely async
// ---------------------------------------------------------------------------
/**
* Transaction-based storage backend.
*
* `T` is the transaction type (e.g. `(() => void)[]` for in-memory).
*/
export interface IBackend<T = unknown> {
begin(): T;
end(transaction: T): Promise<void>;
clean(): Promise<void>;
get(bucket: string, key: Value): Promise<string[]>;
union(bucket: string, keys: Value[]): Promise<string[]>;
unions(buckets: string[], keys: Value[]): Promise<Record<string, string[]>>;
add(transaction: T, bucket: string, key: Value, values: Values): void;
del(transaction: T, bucket: string, keys: Values): void;
remove(transaction: T, bucket: string, key: Value, values: Values): void;
}
// ---------------------------------------------------------------------------
// ACL public interface
// ---------------------------------------------------------------------------
export interface IAcl {
allow(roles: Values, resources: Values, permissions: Values): Promise<void>;
allow(grants: AclGrant[]): Promise<void>;
addUserRoles(userId: Value, roles: Values): Promise<void>;
removeUserRoles(userId: Value, roles: Values): Promise<void>;
userRoles(userId: Value): Promise<string[]>;
roleUsers(role: Value): Promise<string[]>;
hasRole(userId: Value, role: string): Promise<boolean>;
addRoleParents(role: string, parents: Values): Promise<void>;
removeRoleParents(role: string, parents?: Values): Promise<void>;
removeRole(role: string): Promise<void>;
removeResource(resource: string): Promise<void>;
removeAllow(role: string, resources: Values, permissions?: Values): Promise<void>;
allowedPermissions(userId: Value, resources: Values): Promise<Record<string, string[]>>;
isAllowed(userId: Value, resource: string, permissions: Values): Promise<boolean>;
areAnyRolesAllowed(roles: Values, resource: string, permissions: Values): Promise<boolean>;
whatResources(roles: Values): Promise<Record<string, string[]>>;
whatResources(roles: Values, permissions: Values): Promise<string[]>;
}
// ---------------------------------------------------------------------------
// Grant helpers
// ---------------------------------------------------------------------------
export interface AclGrant {
roles: Values;
allows: AclAllow[];
}
export interface AclAllow {
resources: Values;
permissions: Values;
}
// ---------------------------------------------------------------------------
// File store (optional, for FileBackend)
// ---------------------------------------------------------------------------
export interface IFileStore {
read(path?: string): void;
write(path?: string): void;
}

View File

@ -0,0 +1,29 @@
{
"extends": "../typescript-config/base.json",
"compilerOptions": {
"outDir": "./dist-in",
"rootDir": "src",
"baseUrl": ".",
"allowJs": true,
"esModuleInterop": true,
"composite": false,
"importHelpers": false,
"inlineSourceMap": true,
"paths": {
"@/*": [
"src/*"
]
}
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist",
"dist-in"
],
"files": [
"src/index.ts"
]
}