@aooth/arbac-core 0.1.76 → 0.1.78

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/dist/index.cjs CHANGED
@@ -9,6 +9,8 @@ function arbacPatternToRegex(input) {
9
9
  }
10
10
  //#endregion
11
11
  //#region src/arbac.ts
12
+ /** Most memoized action matches per compiled (role, resource); past it, matched per call. */
13
+ const MAX_MEMOIZED_ACTIONS = 512;
12
14
  /**
13
15
  * Implements Advanced Role-Based Access Control (ARBAC) system.
14
16
  * This class allows registering roles and resources, and evaluating access permissions based on user attributes and roles.
@@ -20,6 +22,7 @@ var Arbac = class {
20
22
  roles = {};
21
23
  resources = {};
22
24
  warnedRoles = /* @__PURE__ */ new Set();
25
+ actionMatches = /* @__PURE__ */ new WeakMap();
23
26
  /**
24
27
  * Registers a new role with the ARBAC system.
25
28
  *
@@ -119,28 +122,60 @@ var Arbac = class {
119
122
  * runs it once (no attenuation) or twice (ceiling + narrowed credential view).
120
123
  */
121
124
  async evaluateForRoles(res, roleIds, getAttrs, id) {
122
- const roles = roleIds.map((r) => {
123
- const role = this.resources[res.resource][r];
124
- if (!role && !this.roles[r] && !this.warnedRoles.has(r)) {
125
+ const perResource = this.resources[res.resource];
126
+ const roles = [];
127
+ for (const r of roleIds) {
128
+ const role = perResource[r];
129
+ if (role) roles.push(role);
130
+ else if (!this.roles[r] && !this.warnedRoles.has(r)) {
125
131
  this.warnedRoles.add(r);
126
132
  console.warn(`Role "${r}" assigned to user "${id}" does not exist.`);
127
133
  }
128
- return role;
129
- }).filter(Boolean);
134
+ }
130
135
  if (roles.length === 0) return { allowed: false };
131
- for (const role of roles) for (const rule of role.deny) if (rule._actionRegex.test(res.action)) return { allowed: false };
136
+ const matches = roles.map((role) => this.matchAction(role, res.action));
137
+ for (const match of matches) if (match.deny) return { allowed: false };
132
138
  const scopes = [];
133
139
  let allowed = false;
134
- for (const role of roles) for (const rule of role.allow) if (rule._actionRegex.test(res.action)) {
140
+ let attrs;
141
+ let haveAttrs = false;
142
+ for (const match of matches) for (const rule of match.allow) {
135
143
  allowed = true;
136
- if (rule.scope) scopes.push(rule.scope(await getAttrs(), String(id)));
137
- else scopes.push({});
144
+ if (rule.scope) {
145
+ if (!haveAttrs) {
146
+ attrs = await getAttrs();
147
+ haveAttrs = true;
148
+ }
149
+ scopes.push(rule.scope(attrs, String(id)));
150
+ } else scopes.push({});
138
151
  }
139
152
  return allowed ? {
140
153
  allowed,
141
154
  scopes
142
155
  } : { allowed };
143
156
  }
157
+ /**
158
+ * The rules of one compiled (role, resource) that match `action`: whether
159
+ * any deny rule does, and the matching allow rules in declaration order.
160
+ * Memoized per compiled object (a re-registered role or resource compiles
161
+ * a fresh one, so the memo resets with it), bounded per object.
162
+ */
163
+ matchAction(role, action) {
164
+ let perAction = this.actionMatches.get(role);
165
+ if (!perAction) {
166
+ perAction = /* @__PURE__ */ new Map();
167
+ this.actionMatches.set(role, perAction);
168
+ }
169
+ let match = perAction.get(action);
170
+ if (!match) {
171
+ match = {
172
+ deny: role.deny.some((rule) => rule._actionRegex.test(action)),
173
+ allow: role.allow.filter((rule) => rule._actionRegex.test(action))
174
+ };
175
+ if (perAction.size < MAX_MEMOIZED_ACTIONS) perAction.set(action, match);
176
+ }
177
+ return match;
178
+ }
144
179
  };
145
180
  //#endregion
146
181
  exports.Arbac = Arbac;
package/dist/index.d.cts CHANGED
@@ -51,6 +51,7 @@ declare class Arbac<TUserAttrs extends object, TScope extends object> {
51
51
  protected roles: Record<string, TArbacRole<TUserAttrs, TScope> | undefined>;
52
52
  protected resources: Record<string, Record<string, TArbacRoleForResource<TUserAttrs, TScope> | undefined> | undefined>;
53
53
  private warnedRoles;
54
+ private actionMatches;
54
55
  /**
55
56
  * Registers a new role with the ARBAC system.
56
57
  *
@@ -123,6 +124,13 @@ declare class Arbac<TUserAttrs extends object, TScope extends object> {
123
124
  resource: string;
124
125
  action: string;
125
126
  }, roleIds: string[], getAttrs: () => Promise<TUserAttrs>, id: string | undefined): Promise<TArbacEvalResult<TScope>>;
127
+ /**
128
+ * The rules of one compiled (role, resource) that match `action`: whether
129
+ * any deny rule does, and the matching allow rules in declaration order.
130
+ * Memoized per compiled object (a re-registered role or resource compiles
131
+ * a fresh one, so the memo resets with it), bounded per object.
132
+ */
133
+ private matchAction;
126
134
  }
127
135
  //#endregion
128
136
  //#region src/utils.d.ts
package/dist/index.d.mts CHANGED
@@ -51,6 +51,7 @@ declare class Arbac<TUserAttrs extends object, TScope extends object> {
51
51
  protected roles: Record<string, TArbacRole<TUserAttrs, TScope> | undefined>;
52
52
  protected resources: Record<string, Record<string, TArbacRoleForResource<TUserAttrs, TScope> | undefined> | undefined>;
53
53
  private warnedRoles;
54
+ private actionMatches;
54
55
  /**
55
56
  * Registers a new role with the ARBAC system.
56
57
  *
@@ -123,6 +124,13 @@ declare class Arbac<TUserAttrs extends object, TScope extends object> {
123
124
  resource: string;
124
125
  action: string;
125
126
  }, roleIds: string[], getAttrs: () => Promise<TUserAttrs>, id: string | undefined): Promise<TArbacEvalResult<TScope>>;
127
+ /**
128
+ * The rules of one compiled (role, resource) that match `action`: whether
129
+ * any deny rule does, and the matching allow rules in declaration order.
130
+ * Memoized per compiled object (a re-registered role or resource compiles
131
+ * a fresh one, so the memo resets with it), bounded per object.
132
+ */
133
+ private matchAction;
126
134
  }
127
135
  //#endregion
128
136
  //#region src/utils.d.ts
package/dist/index.mjs CHANGED
@@ -8,6 +8,8 @@ function arbacPatternToRegex(input) {
8
8
  }
9
9
  //#endregion
10
10
  //#region src/arbac.ts
11
+ /** Most memoized action matches per compiled (role, resource); past it, matched per call. */
12
+ const MAX_MEMOIZED_ACTIONS = 512;
11
13
  /**
12
14
  * Implements Advanced Role-Based Access Control (ARBAC) system.
13
15
  * This class allows registering roles and resources, and evaluating access permissions based on user attributes and roles.
@@ -19,6 +21,7 @@ var Arbac = class {
19
21
  roles = {};
20
22
  resources = {};
21
23
  warnedRoles = /* @__PURE__ */ new Set();
24
+ actionMatches = /* @__PURE__ */ new WeakMap();
22
25
  /**
23
26
  * Registers a new role with the ARBAC system.
24
27
  *
@@ -118,28 +121,60 @@ var Arbac = class {
118
121
  * runs it once (no attenuation) or twice (ceiling + narrowed credential view).
119
122
  */
120
123
  async evaluateForRoles(res, roleIds, getAttrs, id) {
121
- const roles = roleIds.map((r) => {
122
- const role = this.resources[res.resource][r];
123
- if (!role && !this.roles[r] && !this.warnedRoles.has(r)) {
124
+ const perResource = this.resources[res.resource];
125
+ const roles = [];
126
+ for (const r of roleIds) {
127
+ const role = perResource[r];
128
+ if (role) roles.push(role);
129
+ else if (!this.roles[r] && !this.warnedRoles.has(r)) {
124
130
  this.warnedRoles.add(r);
125
131
  console.warn(`Role "${r}" assigned to user "${id}" does not exist.`);
126
132
  }
127
- return role;
128
- }).filter(Boolean);
133
+ }
129
134
  if (roles.length === 0) return { allowed: false };
130
- for (const role of roles) for (const rule of role.deny) if (rule._actionRegex.test(res.action)) return { allowed: false };
135
+ const matches = roles.map((role) => this.matchAction(role, res.action));
136
+ for (const match of matches) if (match.deny) return { allowed: false };
131
137
  const scopes = [];
132
138
  let allowed = false;
133
- for (const role of roles) for (const rule of role.allow) if (rule._actionRegex.test(res.action)) {
139
+ let attrs;
140
+ let haveAttrs = false;
141
+ for (const match of matches) for (const rule of match.allow) {
134
142
  allowed = true;
135
- if (rule.scope) scopes.push(rule.scope(await getAttrs(), String(id)));
136
- else scopes.push({});
143
+ if (rule.scope) {
144
+ if (!haveAttrs) {
145
+ attrs = await getAttrs();
146
+ haveAttrs = true;
147
+ }
148
+ scopes.push(rule.scope(attrs, String(id)));
149
+ } else scopes.push({});
137
150
  }
138
151
  return allowed ? {
139
152
  allowed,
140
153
  scopes
141
154
  } : { allowed };
142
155
  }
156
+ /**
157
+ * The rules of one compiled (role, resource) that match `action`: whether
158
+ * any deny rule does, and the matching allow rules in declaration order.
159
+ * Memoized per compiled object (a re-registered role or resource compiles
160
+ * a fresh one, so the memo resets with it), bounded per object.
161
+ */
162
+ matchAction(role, action) {
163
+ let perAction = this.actionMatches.get(role);
164
+ if (!perAction) {
165
+ perAction = /* @__PURE__ */ new Map();
166
+ this.actionMatches.set(role, perAction);
167
+ }
168
+ let match = perAction.get(action);
169
+ if (!match) {
170
+ match = {
171
+ deny: role.deny.some((rule) => rule._actionRegex.test(action)),
172
+ allow: role.allow.filter((rule) => rule._actionRegex.test(action))
173
+ };
174
+ if (perAction.size < MAX_MEMOIZED_ACTIONS) perAction.set(action, match);
175
+ }
176
+ return match;
177
+ }
143
178
  };
144
179
  //#endregion
145
180
  export { Arbac, arbacPatternToRegex };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aooth/arbac-core",
3
- "version": "0.1.76",
3
+ "version": "0.1.78",
4
4
  "description": "Advanced Role-Based Access Control (ARBAC) engine",
5
5
  "keywords": [
6
6
  "access-control",