@aooth/arbac-core 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 aoothjs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,37 @@
1
+ <p align="center">
2
+ <a href="https://aooth.moost.org">
3
+ <img src="https://aooth.moost.org/logo.svg" alt="aoothjs" width="120" />
4
+ </a>
5
+ </p>
6
+
7
+ <h1 align="center">@aooth/arbac-core</h1>
8
+
9
+ <p align="center">
10
+ Zero-dependency RBAC engine — role evaluation with resource/action pattern matching, scope merging, and a minimal public API for framework integrators.
11
+ </p>
12
+
13
+ <p align="center">
14
+ <a href="https://aooth.moost.org/arbac/"><strong>Documentation →</strong></a>
15
+ </p>
16
+
17
+ ---
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ pnpm add @aooth/arbac-core
23
+ ```
24
+
25
+ Most apps want [`@aooth/arbac`](../arbac) (builder API, privilege factories, scope utilities) or [`@aooth/arbac-moost`](../arbac-moost) (Moost integration) instead of consuming the core engine directly.
26
+
27
+ ## Documentation
28
+
29
+ Full docs and API reference: **https://aooth.moost.org/arbac/**
30
+
31
+ - [Concepts](https://aooth.moost.org/arbac/concepts)
32
+ - [Core engine](https://aooth.moost.org/arbac/core)
33
+ - [API reference](https://aooth.moost.org/api/arbac-core)
34
+
35
+ ## License
36
+
37
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,124 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/utils.ts
3
+ function arbacPatternToRegex(input) {
4
+ let pattern = input.replace(/[$()+\-./?[\\\]^{|}]/gu, "\\$&");
5
+ pattern = pattern.replace(/\*\*/gu, "⁑");
6
+ pattern = pattern.replace(/\*/gu, "[^.]*");
7
+ pattern = pattern.replace(/⁑/gu, ".*");
8
+ return new RegExp(`^${pattern}$`);
9
+ }
10
+ //#endregion
11
+ //#region src/arbac.ts
12
+ /**
13
+ * Implements Advanced Role-Based Access Control (ARBAC) system.
14
+ * This class allows registering roles and resources, and evaluating access permissions based on user attributes and roles.
15
+ *
16
+ * @template TUserAttrs The type of user attributes.
17
+ * @template TScope The type of scope that access rules can define.
18
+ */
19
+ var Arbac = class {
20
+ roles = {};
21
+ resources = {};
22
+ warnedRoles = /* @__PURE__ */ new Set();
23
+ /**
24
+ * Registers a new role with the ARBAC system.
25
+ *
26
+ * @param {TArbacRole<TUserAttrs, TScope>} role The role to register.
27
+ */
28
+ registerRole(role) {
29
+ this.roles[role.id] = role;
30
+ for (const key of Object.keys(this.resources)) this.evalRoleForResource(role.id, key);
31
+ return this;
32
+ }
33
+ /**
34
+ * Registers a new resource in the ARBAC system. If the resource already exists, this method does nothing.
35
+ *
36
+ * @param {string} resource The resource to register.
37
+ */
38
+ registerResource(resource) {
39
+ if (!this.resources[resource]) {
40
+ this.resources[resource] = {};
41
+ for (const key of Object.keys(this.roles)) this.evalRoleForResource(key, resource);
42
+ }
43
+ return this;
44
+ }
45
+ /**
46
+ * Evaluates the role for a specific resource, updating the internal state with allow/deny rules.
47
+ *
48
+ * @protected
49
+ * @param {string} roleId The ID of the role to evaluate.
50
+ * @param {string} resourceId The ID of the resource to evaluate against.
51
+ */
52
+ evalRoleForResource(roleId, resourceId) {
53
+ const resource = this.resources[resourceId];
54
+ const role = this.roles[roleId];
55
+ resource[roleId] = {
56
+ id: roleId,
57
+ allow: [],
58
+ deny: []
59
+ };
60
+ const target = resource[roleId];
61
+ for (const rule of role.rules) {
62
+ let rg = rule._resourceRegex;
63
+ if (!rg) {
64
+ rg = arbacPatternToRegex(rule.resource);
65
+ rule._resourceRegex = rg;
66
+ }
67
+ const effect = rule.effect || "allow";
68
+ if (rg.test(resourceId)) {
69
+ let ag = rule._actionRegex;
70
+ if (!ag) {
71
+ ag = arbacPatternToRegex(rule.action);
72
+ rule._actionRegex = ag;
73
+ }
74
+ target[effect].push({
75
+ action: rule.action,
76
+ _actionRegex: ag,
77
+ scope: rule.scope
78
+ });
79
+ }
80
+ }
81
+ return this;
82
+ }
83
+ /**
84
+ * Evaluates whether a given action on a resource is allowed for a user with specified roles.
85
+ *
86
+ * @param {{
87
+ * roleIds: string[];
88
+ * userId: string;
89
+ * resource: string;
90
+ * action: string;
91
+ * }} res The options for the evaluation.
92
+ * @returns {Promise<TArbacEvalResult<TScope>>} The result of the evaluation, including whether the action is allowed and any applicable scopes.
93
+ */
94
+ async evaluate(res, user) {
95
+ this.registerResource(res.resource);
96
+ const roles = user.roles.map((r) => {
97
+ const role = this.resources[res.resource][r];
98
+ if (!role && !this.roles[r] && !this.warnedRoles.has(r)) {
99
+ this.warnedRoles.add(r);
100
+ console.warn(`Role "${r}" assigned to user "${user.id}" does not exist.`);
101
+ }
102
+ return role;
103
+ }).filter(Boolean);
104
+ if (roles.length === 0) return { allowed: false };
105
+ for (const role of roles) for (const rule of role.deny) if (rule._actionRegex.test(res.action)) return { allowed: false };
106
+ let userAttrs;
107
+ const scopes = [];
108
+ let allowed = false;
109
+ for (const role of roles) for (const rule of role.allow) if (rule._actionRegex.test(res.action)) {
110
+ allowed = true;
111
+ if (rule.scope) {
112
+ if (!userAttrs) userAttrs = typeof user.attrs === "function" ? await user.attrs(user.id) : user.attrs;
113
+ scopes.push(rule.scope(userAttrs, String(user.id)));
114
+ } else scopes.push({});
115
+ }
116
+ return allowed ? {
117
+ allowed,
118
+ scopes
119
+ } : { allowed };
120
+ }
121
+ };
122
+ //#endregion
123
+ exports.Arbac = Arbac;
124
+ exports.arbacPatternToRegex = arbacPatternToRegex;
@@ -0,0 +1,88 @@
1
+ //#region src/types.d.ts
2
+ interface TArbacEvalResult<TScope> {
3
+ allowed: boolean;
4
+ scopes?: TScope[];
5
+ }
6
+ interface TArbacRole<TUserAttrs, TScope> {
7
+ id: string;
8
+ name?: string;
9
+ description?: string;
10
+ rules: Array<TArbacRule<TUserAttrs, TScope>>;
11
+ }
12
+ type TArbacCompiledRule<TUserAttrs, TScope> = Omit<TArbacRule<TUserAttrs, TScope>, "resource" | "effect" | "_resourceRegex"> & {
13
+ _actionRegex: RegExp;
14
+ };
15
+ interface TArbacRoleForResource<TUserAttrs, TScope> {
16
+ id: string;
17
+ allow: Array<TArbacCompiledRule<TUserAttrs, TScope>>;
18
+ deny: Array<TArbacCompiledRule<TUserAttrs, TScope>>;
19
+ }
20
+ type TArbacRule<TUserAttrs, TScope> = {
21
+ resource: string;
22
+ action: string;
23
+ scope?: (userAttrs: TUserAttrs, userId: string) => TScope;
24
+ effect?: never;
25
+ } | {
26
+ resource: string;
27
+ action: string;
28
+ effect: "deny";
29
+ scope?: never;
30
+ };
31
+ //#endregion
32
+ //#region src/arbac.d.ts
33
+ /**
34
+ * Implements Advanced Role-Based Access Control (ARBAC) system.
35
+ * This class allows registering roles and resources, and evaluating access permissions based on user attributes and roles.
36
+ *
37
+ * @template TUserAttrs The type of user attributes.
38
+ * @template TScope The type of scope that access rules can define.
39
+ */
40
+ declare class Arbac<TUserAttrs extends object, TScope extends object> {
41
+ protected roles: Record<string, TArbacRole<TUserAttrs, TScope> | undefined>;
42
+ protected resources: Record<string, Record<string, TArbacRoleForResource<TUserAttrs, TScope> | undefined> | undefined>;
43
+ private warnedRoles;
44
+ /**
45
+ * Registers a new role with the ARBAC system.
46
+ *
47
+ * @param {TArbacRole<TUserAttrs, TScope>} role The role to register.
48
+ */
49
+ registerRole(role: TArbacRole<TUserAttrs, TScope>): Arbac<TUserAttrs, TScope>;
50
+ /**
51
+ * Registers a new resource in the ARBAC system. If the resource already exists, this method does nothing.
52
+ *
53
+ * @param {string} resource The resource to register.
54
+ */
55
+ registerResource(resource: string): Arbac<TUserAttrs, TScope>;
56
+ /**
57
+ * Evaluates the role for a specific resource, updating the internal state with allow/deny rules.
58
+ *
59
+ * @protected
60
+ * @param {string} roleId The ID of the role to evaluate.
61
+ * @param {string} resourceId The ID of the resource to evaluate against.
62
+ */
63
+ protected evalRoleForResource(roleId: string, resourceId: string): Arbac<TUserAttrs, TScope>;
64
+ /**
65
+ * Evaluates whether a given action on a resource is allowed for a user with specified roles.
66
+ *
67
+ * @param {{
68
+ * roleIds: string[];
69
+ * userId: string;
70
+ * resource: string;
71
+ * action: string;
72
+ * }} res The options for the evaluation.
73
+ * @returns {Promise<TArbacEvalResult<TScope>>} The result of the evaluation, including whether the action is allowed and any applicable scopes.
74
+ */
75
+ evaluate<T extends string | undefined>(res: {
76
+ resource: string;
77
+ action: string;
78
+ }, user: {
79
+ id: T;
80
+ roles: string[];
81
+ attrs: TUserAttrs | ((userId: T) => TUserAttrs | Promise<TUserAttrs>);
82
+ }): Promise<TArbacEvalResult<TScope>>;
83
+ }
84
+ //#endregion
85
+ //#region src/utils.d.ts
86
+ declare function arbacPatternToRegex(input: string): RegExp;
87
+ //#endregion
88
+ export { Arbac, TArbacCompiledRule, TArbacEvalResult, TArbacRole, TArbacRoleForResource, TArbacRule, arbacPatternToRegex };
@@ -0,0 +1,88 @@
1
+ //#region src/types.d.ts
2
+ interface TArbacEvalResult<TScope> {
3
+ allowed: boolean;
4
+ scopes?: TScope[];
5
+ }
6
+ interface TArbacRole<TUserAttrs, TScope> {
7
+ id: string;
8
+ name?: string;
9
+ description?: string;
10
+ rules: Array<TArbacRule<TUserAttrs, TScope>>;
11
+ }
12
+ type TArbacCompiledRule<TUserAttrs, TScope> = Omit<TArbacRule<TUserAttrs, TScope>, "resource" | "effect" | "_resourceRegex"> & {
13
+ _actionRegex: RegExp;
14
+ };
15
+ interface TArbacRoleForResource<TUserAttrs, TScope> {
16
+ id: string;
17
+ allow: Array<TArbacCompiledRule<TUserAttrs, TScope>>;
18
+ deny: Array<TArbacCompiledRule<TUserAttrs, TScope>>;
19
+ }
20
+ type TArbacRule<TUserAttrs, TScope> = {
21
+ resource: string;
22
+ action: string;
23
+ scope?: (userAttrs: TUserAttrs, userId: string) => TScope;
24
+ effect?: never;
25
+ } | {
26
+ resource: string;
27
+ action: string;
28
+ effect: "deny";
29
+ scope?: never;
30
+ };
31
+ //#endregion
32
+ //#region src/arbac.d.ts
33
+ /**
34
+ * Implements Advanced Role-Based Access Control (ARBAC) system.
35
+ * This class allows registering roles and resources, and evaluating access permissions based on user attributes and roles.
36
+ *
37
+ * @template TUserAttrs The type of user attributes.
38
+ * @template TScope The type of scope that access rules can define.
39
+ */
40
+ declare class Arbac<TUserAttrs extends object, TScope extends object> {
41
+ protected roles: Record<string, TArbacRole<TUserAttrs, TScope> | undefined>;
42
+ protected resources: Record<string, Record<string, TArbacRoleForResource<TUserAttrs, TScope> | undefined> | undefined>;
43
+ private warnedRoles;
44
+ /**
45
+ * Registers a new role with the ARBAC system.
46
+ *
47
+ * @param {TArbacRole<TUserAttrs, TScope>} role The role to register.
48
+ */
49
+ registerRole(role: TArbacRole<TUserAttrs, TScope>): Arbac<TUserAttrs, TScope>;
50
+ /**
51
+ * Registers a new resource in the ARBAC system. If the resource already exists, this method does nothing.
52
+ *
53
+ * @param {string} resource The resource to register.
54
+ */
55
+ registerResource(resource: string): Arbac<TUserAttrs, TScope>;
56
+ /**
57
+ * Evaluates the role for a specific resource, updating the internal state with allow/deny rules.
58
+ *
59
+ * @protected
60
+ * @param {string} roleId The ID of the role to evaluate.
61
+ * @param {string} resourceId The ID of the resource to evaluate against.
62
+ */
63
+ protected evalRoleForResource(roleId: string, resourceId: string): Arbac<TUserAttrs, TScope>;
64
+ /**
65
+ * Evaluates whether a given action on a resource is allowed for a user with specified roles.
66
+ *
67
+ * @param {{
68
+ * roleIds: string[];
69
+ * userId: string;
70
+ * resource: string;
71
+ * action: string;
72
+ * }} res The options for the evaluation.
73
+ * @returns {Promise<TArbacEvalResult<TScope>>} The result of the evaluation, including whether the action is allowed and any applicable scopes.
74
+ */
75
+ evaluate<T extends string | undefined>(res: {
76
+ resource: string;
77
+ action: string;
78
+ }, user: {
79
+ id: T;
80
+ roles: string[];
81
+ attrs: TUserAttrs | ((userId: T) => TUserAttrs | Promise<TUserAttrs>);
82
+ }): Promise<TArbacEvalResult<TScope>>;
83
+ }
84
+ //#endregion
85
+ //#region src/utils.d.ts
86
+ declare function arbacPatternToRegex(input: string): RegExp;
87
+ //#endregion
88
+ export { Arbac, TArbacCompiledRule, TArbacEvalResult, TArbacRole, TArbacRoleForResource, TArbacRule, arbacPatternToRegex };
package/dist/index.mjs ADDED
@@ -0,0 +1,122 @@
1
+ //#region src/utils.ts
2
+ function arbacPatternToRegex(input) {
3
+ let pattern = input.replace(/[$()+\-./?[\\\]^{|}]/gu, "\\$&");
4
+ pattern = pattern.replace(/\*\*/gu, "⁑");
5
+ pattern = pattern.replace(/\*/gu, "[^.]*");
6
+ pattern = pattern.replace(/⁑/gu, ".*");
7
+ return new RegExp(`^${pattern}$`);
8
+ }
9
+ //#endregion
10
+ //#region src/arbac.ts
11
+ /**
12
+ * Implements Advanced Role-Based Access Control (ARBAC) system.
13
+ * This class allows registering roles and resources, and evaluating access permissions based on user attributes and roles.
14
+ *
15
+ * @template TUserAttrs The type of user attributes.
16
+ * @template TScope The type of scope that access rules can define.
17
+ */
18
+ var Arbac = class {
19
+ roles = {};
20
+ resources = {};
21
+ warnedRoles = /* @__PURE__ */ new Set();
22
+ /**
23
+ * Registers a new role with the ARBAC system.
24
+ *
25
+ * @param {TArbacRole<TUserAttrs, TScope>} role The role to register.
26
+ */
27
+ registerRole(role) {
28
+ this.roles[role.id] = role;
29
+ for (const key of Object.keys(this.resources)) this.evalRoleForResource(role.id, key);
30
+ return this;
31
+ }
32
+ /**
33
+ * Registers a new resource in the ARBAC system. If the resource already exists, this method does nothing.
34
+ *
35
+ * @param {string} resource The resource to register.
36
+ */
37
+ registerResource(resource) {
38
+ if (!this.resources[resource]) {
39
+ this.resources[resource] = {};
40
+ for (const key of Object.keys(this.roles)) this.evalRoleForResource(key, resource);
41
+ }
42
+ return this;
43
+ }
44
+ /**
45
+ * Evaluates the role for a specific resource, updating the internal state with allow/deny rules.
46
+ *
47
+ * @protected
48
+ * @param {string} roleId The ID of the role to evaluate.
49
+ * @param {string} resourceId The ID of the resource to evaluate against.
50
+ */
51
+ evalRoleForResource(roleId, resourceId) {
52
+ const resource = this.resources[resourceId];
53
+ const role = this.roles[roleId];
54
+ resource[roleId] = {
55
+ id: roleId,
56
+ allow: [],
57
+ deny: []
58
+ };
59
+ const target = resource[roleId];
60
+ for (const rule of role.rules) {
61
+ let rg = rule._resourceRegex;
62
+ if (!rg) {
63
+ rg = arbacPatternToRegex(rule.resource);
64
+ rule._resourceRegex = rg;
65
+ }
66
+ const effect = rule.effect || "allow";
67
+ if (rg.test(resourceId)) {
68
+ let ag = rule._actionRegex;
69
+ if (!ag) {
70
+ ag = arbacPatternToRegex(rule.action);
71
+ rule._actionRegex = ag;
72
+ }
73
+ target[effect].push({
74
+ action: rule.action,
75
+ _actionRegex: ag,
76
+ scope: rule.scope
77
+ });
78
+ }
79
+ }
80
+ return this;
81
+ }
82
+ /**
83
+ * Evaluates whether a given action on a resource is allowed for a user with specified roles.
84
+ *
85
+ * @param {{
86
+ * roleIds: string[];
87
+ * userId: string;
88
+ * resource: string;
89
+ * action: string;
90
+ * }} res The options for the evaluation.
91
+ * @returns {Promise<TArbacEvalResult<TScope>>} The result of the evaluation, including whether the action is allowed and any applicable scopes.
92
+ */
93
+ async evaluate(res, user) {
94
+ this.registerResource(res.resource);
95
+ const roles = user.roles.map((r) => {
96
+ const role = this.resources[res.resource][r];
97
+ if (!role && !this.roles[r] && !this.warnedRoles.has(r)) {
98
+ this.warnedRoles.add(r);
99
+ console.warn(`Role "${r}" assigned to user "${user.id}" does not exist.`);
100
+ }
101
+ return role;
102
+ }).filter(Boolean);
103
+ if (roles.length === 0) return { allowed: false };
104
+ for (const role of roles) for (const rule of role.deny) if (rule._actionRegex.test(res.action)) return { allowed: false };
105
+ let userAttrs;
106
+ const scopes = [];
107
+ let allowed = false;
108
+ for (const role of roles) for (const rule of role.allow) if (rule._actionRegex.test(res.action)) {
109
+ allowed = true;
110
+ if (rule.scope) {
111
+ if (!userAttrs) userAttrs = typeof user.attrs === "function" ? await user.attrs(user.id) : user.attrs;
112
+ scopes.push(rule.scope(userAttrs, String(user.id)));
113
+ } else scopes.push({});
114
+ }
115
+ return allowed ? {
116
+ allowed,
117
+ scopes
118
+ } : { allowed };
119
+ }
120
+ };
121
+ //#endregion
122
+ export { Arbac, arbacPatternToRegex };
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@aooth/arbac-core",
3
+ "version": "0.1.1",
4
+ "description": "Advanced Role-Based Access Control (ARBAC) engine",
5
+ "keywords": [
6
+ "access-control",
7
+ "aoothjs",
8
+ "arbac",
9
+ "authorization",
10
+ "rbac"
11
+ ],
12
+ "homepage": "https://github.com/moostjs/aoothjs/tree/main/packages/arbac-core#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/moostjs/aoothjs/issues"
15
+ },
16
+ "license": "MIT",
17
+ "author": "Artem Maltsev",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/moostjs/aoothjs.git",
21
+ "directory": "packages/arbac-core"
22
+ },
23
+ "files": [
24
+ "dist"
25
+ ],
26
+ "type": "module",
27
+ "sideEffects": false,
28
+ "main": "dist/index.mjs",
29
+ "module": "./dist/index.mjs",
30
+ "types": "dist/index.d.mts",
31
+ "exports": {
32
+ ".": {
33
+ "types": "./dist/index.d.mts",
34
+ "import": "./dist/index.mjs",
35
+ "require": "./dist/index.cjs"
36
+ },
37
+ "./package.json": "./package.json"
38
+ },
39
+ "publishConfig": {
40
+ "access": "public"
41
+ },
42
+ "scripts": {
43
+ "build": "vp pack",
44
+ "dev": "vp pack --watch",
45
+ "test": "vp test",
46
+ "check": "vp check"
47
+ }
48
+ }