mirror of
https://gitlab.com/wgp/dougal/software.git
synced 2025-12-06 10:07:08 +00:00
Add @dougal/organisations NodeJS module.
Abstracts the concept of Organisations in the new permissions model.
This commit is contained in:
75
lib/modules/@dougal/organisations/Organisation.js
Normal file
75
lib/modules/@dougal/organisations/Organisation.js
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
|
||||||
|
class Organisation {
|
||||||
|
|
||||||
|
constructor (data) {
|
||||||
|
|
||||||
|
this.read = !!data?.read;
|
||||||
|
this.write = !!data?.write;
|
||||||
|
this.edit = !!data?.edit;
|
||||||
|
|
||||||
|
this.other = {};
|
||||||
|
|
||||||
|
return new Proxy(this, {
|
||||||
|
get (target, prop) {
|
||||||
|
if (prop in target) {
|
||||||
|
return target[prop]
|
||||||
|
} else {
|
||||||
|
return target.other[prop];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
set (target, prop, value) {
|
||||||
|
const oldValue = target[prop] !== undefined ? target[prop] : target.other[prop];
|
||||||
|
const newValue = Boolean(value);
|
||||||
|
|
||||||
|
if (["read", "write", "edit"].includes(prop)) {
|
||||||
|
target[prop] = newValue;
|
||||||
|
} else {
|
||||||
|
target.other[prop] = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON () {
|
||||||
|
return {
|
||||||
|
read: this.read,
|
||||||
|
write: this.write,
|
||||||
|
edit: this.edit,
|
||||||
|
...this.other
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toString (replacer, space) {
|
||||||
|
return JSON.stringify(this.toJSON(), replacer, space);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Limit the operations to only those allowed by `other`
|
||||||
|
*/
|
||||||
|
filter (other) {
|
||||||
|
const filteredOrganisation = new Organisation();
|
||||||
|
|
||||||
|
filteredOrganisation.read = this.read && other.read;
|
||||||
|
filteredOrganisation.write = this.write && other.write;
|
||||||
|
filteredOrganisation.edit = this.edit && other.edit;
|
||||||
|
|
||||||
|
return filteredOrganisation;
|
||||||
|
}
|
||||||
|
|
||||||
|
intersect (other) {
|
||||||
|
return this.filter(other);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (typeof module !== 'undefined' && module.exports) {
|
||||||
|
module.exports = Organisation; // CJS export
|
||||||
|
}
|
||||||
|
|
||||||
|
// ESM export
|
||||||
|
if (typeof exports !== 'undefined' && !exports.default) {
|
||||||
|
exports.default = Organisation; // ESM export
|
||||||
|
}
|
||||||
225
lib/modules/@dougal/organisations/Organisations.js
Normal file
225
lib/modules/@dougal/organisations/Organisations.js
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
const Organisation = require('./Organisation');
|
||||||
|
|
||||||
|
class Organisations {
|
||||||
|
|
||||||
|
#values = {}
|
||||||
|
|
||||||
|
#overlord
|
||||||
|
|
||||||
|
static entries (orgs) {
|
||||||
|
return orgs.names().map(name => [name, orgs.get(name)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor (data, overlord) {
|
||||||
|
if (data instanceof Organisations) {
|
||||||
|
for (const [name, value] of Organisations.entries(data)) {
|
||||||
|
this.set(name, new Organisation(value));
|
||||||
|
}
|
||||||
|
} else if (data instanceof Object) {
|
||||||
|
for (const [name, value] of Object.entries(data)) {
|
||||||
|
this.set(name, new Organisation(value));
|
||||||
|
}
|
||||||
|
} else if (data instanceof String) {
|
||||||
|
this.set(data, new Organisation());
|
||||||
|
} else if (typeof data !== "undefined") {
|
||||||
|
throw new Error("Invalid constructor argument");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (overlord) {
|
||||||
|
this.#overlord = overlord;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get values () {
|
||||||
|
return this.#values;
|
||||||
|
}
|
||||||
|
|
||||||
|
get length () {
|
||||||
|
return this.names().length;
|
||||||
|
}
|
||||||
|
|
||||||
|
get overlord () {
|
||||||
|
return this.#overlord;
|
||||||
|
}
|
||||||
|
|
||||||
|
set overlord (v) {
|
||||||
|
this.#overlord = new Organisations(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get the operations for `name`
|
||||||
|
*/
|
||||||
|
get (name) {
|
||||||
|
const key = Object.keys(this.values).find( k => k.toLowerCase() == name.toLowerCase() ) ?? name;
|
||||||
|
return this.values[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Set the operations for `name` to `value`
|
||||||
|
*
|
||||||
|
* If we have an overlord, ensure we cannot:
|
||||||
|
*
|
||||||
|
* 1. Add new organisations which the overlord
|
||||||
|
* is not a member of
|
||||||
|
* 2. Access operations that the overlord is not
|
||||||
|
* allowed to access
|
||||||
|
*/
|
||||||
|
set (name, value) {
|
||||||
|
name = String(name).trim();
|
||||||
|
const key = Object.keys(this.values).find( k => k.toLowerCase() == name.toLowerCase() ) ?? name;
|
||||||
|
const org = new Organisation(value);
|
||||||
|
|
||||||
|
if (this.overlord) {
|
||||||
|
const parent = this.overlord.get(key) ?? this.overlord.get("*");
|
||||||
|
if (parent) {
|
||||||
|
this.values[key] = parent.filter(org);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.values[key] = new Organisation(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Enable the operation `op` in all organisations
|
||||||
|
*/
|
||||||
|
enableOperation (op) {
|
||||||
|
if (this.overlord) {
|
||||||
|
Object.keys(this.#values)
|
||||||
|
.filter( key => (this.overlord.get(key) ?? this.overlord.get("*"))?.[op] )
|
||||||
|
.forEach( key => this.#values[key][op] = true );
|
||||||
|
} else {
|
||||||
|
Object.values(this.#values).forEach( org => org[op] = true );
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Disable the operation `op` in all organisations
|
||||||
|
*/
|
||||||
|
disableOperation (op) {
|
||||||
|
Object.values(this.#values).forEach( org => org[op] = false );
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create a new organisation object limited by the caller's rights
|
||||||
|
*
|
||||||
|
* The spawned Organisations instance will have the same organisations
|
||||||
|
* and rights as the caller minus the applied `mask`. With the default
|
||||||
|
* mask, the spawned object will inherit all rights except for `edit`
|
||||||
|
* rights.
|
||||||
|
*
|
||||||
|
* The "*" organisation must be explicitly assigned. It is not inherited.
|
||||||
|
*/
|
||||||
|
spawn (mask = {read: true, write: true, edit: false}) {
|
||||||
|
|
||||||
|
const parent = new Organisations();
|
||||||
|
const wildcard = this.get("*").edit; // If true, we can spawn everywhere
|
||||||
|
|
||||||
|
this.entries().forEach( ([k, v]) => {
|
||||||
|
// if (k != "*") { // This organisation is not inherited
|
||||||
|
if (v.edit || wildcard) { // We have the right to spawn in this organisation
|
||||||
|
const o = new Organisation({
|
||||||
|
read: v.read && mask.read,
|
||||||
|
write: v.write && mask.write,
|
||||||
|
edit: v.edit && mask.edit
|
||||||
|
});
|
||||||
|
parent.set(k, o);
|
||||||
|
}
|
||||||
|
// }
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Organisations({}, parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
remove (name) {
|
||||||
|
const key = Object.keys(this.values).find( k => k.toLowerCase() == name.toLowerCase() ) ?? name;
|
||||||
|
delete this.values[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Return the list of organisation names
|
||||||
|
*/
|
||||||
|
names () {
|
||||||
|
return Object.keys(this.values);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Same as this.get(name)
|
||||||
|
*/
|
||||||
|
value (name) {
|
||||||
|
return this.values[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Same as Object.prototype.entries
|
||||||
|
*/
|
||||||
|
entries () {
|
||||||
|
return this.names().map( name => [ name, this.value(name) ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Return true if the named organisation is present
|
||||||
|
*/
|
||||||
|
has (name) {
|
||||||
|
return Boolean(this.value(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Return only those of our organisations
|
||||||
|
* and operations present in `other`
|
||||||
|
*/
|
||||||
|
filter (other) {
|
||||||
|
const filteredOrganisations = new Organisations();
|
||||||
|
|
||||||
|
const wildcard = other.value("*");
|
||||||
|
|
||||||
|
for (const [name, org] of this.entries()) {
|
||||||
|
const ownOrg = other.value(name) ?? wildcard;
|
||||||
|
if (ownOrg) {
|
||||||
|
filteredOrganisations.set(name, org.filter(ownOrg))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredOrganisations;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Return only those organisations
|
||||||
|
* that have access to the required
|
||||||
|
* operation
|
||||||
|
*/
|
||||||
|
accessToOperation (op) {
|
||||||
|
const filteredOrganisations = new Organisations();
|
||||||
|
|
||||||
|
for (const [name, org] of this.entries()) {
|
||||||
|
if (org[op]) {
|
||||||
|
filteredOrganisations.set(name, org);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredOrganisations;
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON () {
|
||||||
|
const obj = {};
|
||||||
|
for (const key in this.values) {
|
||||||
|
obj[key] = this.values[key].toJSON();
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
toString (replacer, space) {
|
||||||
|
return JSON.stringify(this.toJSON(), replacer, space);
|
||||||
|
}
|
||||||
|
|
||||||
|
*[Symbol.iterator] () {
|
||||||
|
for (const [name, operations] of this.entries()) {
|
||||||
|
yield {name, operations};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (typeof module !== 'undefined' && module.exports) {
|
||||||
|
module.exports = Organisations; // CJS export
|
||||||
|
}
|
||||||
|
|
||||||
|
// ESM export
|
||||||
|
if (typeof exports !== 'undefined' && !exports.default) {
|
||||||
|
exports.default = Organisations; // ESM export
|
||||||
|
}
|
||||||
5
lib/modules/@dougal/organisations/index.js
Normal file
5
lib/modules/@dougal/organisations/index.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
|
||||||
|
module.exports = {
|
||||||
|
Organisation: require('./Organisation'),
|
||||||
|
Organisations: require('./Organisations')
|
||||||
|
}
|
||||||
12
lib/modules/@dougal/organisations/package.json
Normal file
12
lib/modules/@dougal/organisations/package.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "@dougal/organisations",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user