@aooth/arbac-core 0.1.7 → 0.1.8
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 +38 -14
- package/dist/index.d.cts +45 -7
- package/dist/index.d.mts +45 -7
- package/dist/index.mjs +38 -14
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -81,37 +81,61 @@ var Arbac = class {
|
|
|
81
81
|
return this;
|
|
82
82
|
}
|
|
83
83
|
/**
|
|
84
|
-
* Evaluates whether a given action on a resource is allowed for a user with
|
|
84
|
+
* Evaluates whether a given action on a resource is allowed for a user with
|
|
85
|
+
* the specified roles, returning the allow decision plus any applicable scopes.
|
|
86
|
+
*
|
|
87
|
+
* When `user.attenuate` is supplied (the credential-claims bridge) the policy
|
|
88
|
+
* is evaluated TWICE and the OUTCOMES are intersected — see the `attenuate`
|
|
89
|
+
* field for the restrict-only safety guarantee.
|
|
85
90
|
*
|
|
86
|
-
* @param {{
|
|
87
|
-
* roleIds: string[];
|
|
88
|
-
* userId: string;
|
|
89
|
-
* resource: string;
|
|
90
|
-
* action: string;
|
|
91
|
-
* }} res The options for the evaluation.
|
|
92
91
|
* @returns {Promise<TArbacEvalResult<TScope>>} The result of the evaluation, including whether the action is allowed and any applicable scopes.
|
|
93
92
|
*/
|
|
94
93
|
async evaluate(res, user) {
|
|
95
94
|
this.registerResource(res.resource);
|
|
96
|
-
|
|
95
|
+
let resolvedAttrs;
|
|
96
|
+
const userAttrs = async () => {
|
|
97
|
+
if (resolvedAttrs === void 0) resolvedAttrs = typeof user.attrs === "function" ? await user.attrs(user.id) : user.attrs;
|
|
98
|
+
return resolvedAttrs;
|
|
99
|
+
};
|
|
100
|
+
const userEval = await this.evaluateForRoles(res, user.roles, userAttrs, user.id);
|
|
101
|
+
if (!user.attenuate) return userEval;
|
|
102
|
+
const claimRoles = user.attenuate.roles;
|
|
103
|
+
const claimAttrs = user.attenuate.attrs;
|
|
104
|
+
const credRoles = claimRoles ? user.roles.filter((r) => claimRoles.includes(r)) : user.roles;
|
|
105
|
+
const credAttrs = claimAttrs ? async () => ({
|
|
106
|
+
...await userAttrs(),
|
|
107
|
+
...claimAttrs
|
|
108
|
+
}) : userAttrs;
|
|
109
|
+
const credEval = await this.evaluateForRoles(res, credRoles, credAttrs, user.id);
|
|
110
|
+
if (!userEval.allowed || !credEval.allowed) return { allowed: false };
|
|
111
|
+
return {
|
|
112
|
+
allowed: true,
|
|
113
|
+
scopes: userEval.scopes,
|
|
114
|
+
credScopes: credEval.scopes
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* One evaluation pass over a concrete role-id set + attrs resolver. Carries
|
|
119
|
+
* the deny-wins / allow-union / `{}` universe-sentinel semantics; `evaluate`
|
|
120
|
+
* runs it once (no attenuation) or twice (ceiling + narrowed credential view).
|
|
121
|
+
*/
|
|
122
|
+
async evaluateForRoles(res, roleIds, getAttrs, id) {
|
|
123
|
+
const roles = roleIds.map((r) => {
|
|
97
124
|
const role = this.resources[res.resource][r];
|
|
98
125
|
if (!role && !this.roles[r] && !this.warnedRoles.has(r)) {
|
|
99
126
|
this.warnedRoles.add(r);
|
|
100
|
-
console.warn(`Role "${r}" assigned to user "${
|
|
127
|
+
console.warn(`Role "${r}" assigned to user "${id}" does not exist.`);
|
|
101
128
|
}
|
|
102
129
|
return role;
|
|
103
130
|
}).filter(Boolean);
|
|
104
131
|
if (roles.length === 0) return { allowed: false };
|
|
105
132
|
for (const role of roles) for (const rule of role.deny) if (rule._actionRegex.test(res.action)) return { allowed: false };
|
|
106
|
-
let userAttrs;
|
|
107
133
|
const scopes = [];
|
|
108
134
|
let allowed = false;
|
|
109
135
|
for (const role of roles) for (const rule of role.allow) if (rule._actionRegex.test(res.action)) {
|
|
110
136
|
allowed = true;
|
|
111
|
-
if (rule.scope)
|
|
112
|
-
|
|
113
|
-
scopes.push(rule.scope(userAttrs, String(user.id)));
|
|
114
|
-
} else scopes.push({});
|
|
137
|
+
if (rule.scope) scopes.push(rule.scope(await getAttrs(), String(id)));
|
|
138
|
+
else scopes.push({});
|
|
115
139
|
}
|
|
116
140
|
return allowed ? {
|
|
117
141
|
allowed,
|
package/dist/index.d.cts
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
interface TArbacEvalResult<TScope> {
|
|
3
3
|
allowed: boolean;
|
|
4
4
|
scopes?: TScope[];
|
|
5
|
+
/**
|
|
6
|
+
* Present ONLY on an attenuated evaluation (`user.attenuate` was supplied).
|
|
7
|
+
* The scopes from the credential's narrowed pass, to be CONJOINED with
|
|
8
|
+
* `scopes` (the full-authority pass) by the scope layer — a row/field is
|
|
9
|
+
* effective only if BOTH passes admit it. The conjunction is the scope
|
|
10
|
+
* shape's responsibility (the engine is scope-agnostic), so the two unions
|
|
11
|
+
* are surfaced separately here rather than pre-combined. Absent on a normal
|
|
12
|
+
* (non-attenuated) evaluation.
|
|
13
|
+
*/
|
|
14
|
+
credScopes?: TScope[];
|
|
5
15
|
}
|
|
6
16
|
interface TArbacRole<TUserAttrs, TScope> {
|
|
7
17
|
id: string;
|
|
@@ -62,14 +72,13 @@ declare class Arbac<TUserAttrs extends object, TScope extends object> {
|
|
|
62
72
|
*/
|
|
63
73
|
protected evalRoleForResource(roleId: string, resourceId: string): Arbac<TUserAttrs, TScope>;
|
|
64
74
|
/**
|
|
65
|
-
* Evaluates whether a given action on a resource is allowed for a user with
|
|
75
|
+
* Evaluates whether a given action on a resource is allowed for a user with
|
|
76
|
+
* the specified roles, returning the allow decision plus any applicable scopes.
|
|
77
|
+
*
|
|
78
|
+
* When `user.attenuate` is supplied (the credential-claims bridge) the policy
|
|
79
|
+
* is evaluated TWICE and the OUTCOMES are intersected — see the `attenuate`
|
|
80
|
+
* field for the restrict-only safety guarantee.
|
|
66
81
|
*
|
|
67
|
-
* @param {{
|
|
68
|
-
* roleIds: string[];
|
|
69
|
-
* userId: string;
|
|
70
|
-
* resource: string;
|
|
71
|
-
* action: string;
|
|
72
|
-
* }} res The options for the evaluation.
|
|
73
82
|
* @returns {Promise<TArbacEvalResult<TScope>>} The result of the evaluation, including whether the action is allowed and any applicable scopes.
|
|
74
83
|
*/
|
|
75
84
|
evaluate<T extends string | undefined>(res: {
|
|
@@ -79,7 +88,36 @@ declare class Arbac<TUserAttrs extends object, TScope extends object> {
|
|
|
79
88
|
id: T;
|
|
80
89
|
roles: string[];
|
|
81
90
|
attrs: TUserAttrs | ((userId: T) => TUserAttrs | Promise<TUserAttrs>);
|
|
91
|
+
/**
|
|
92
|
+
* Opt-in, restrict-only attenuation sourced from the authenticated
|
|
93
|
+
* credential's claims. When present the policy runs twice — at full
|
|
94
|
+
* authority (the ceiling) and at the credential's narrowed view — and
|
|
95
|
+
* the outcomes are intersected: `allowed` is ANDed (so dropping a role
|
|
96
|
+
* can never drop a matching `deny` and thus can never escalate a denied
|
|
97
|
+
* action), and the two passes' scope lists are surfaced separately as
|
|
98
|
+
* `scopes` (ceiling) and `credScopes` (narrowed) for the scope layer to
|
|
99
|
+
* conjoin. A credential can therefore only ever do/see/affect LESS than
|
|
100
|
+
* its owning user — escalation-proof even against an untrusted minter.
|
|
101
|
+
*
|
|
102
|
+
* `roles: []` → no roles (deny-all, fail-closed). An OMITTED `roles` key
|
|
103
|
+
* → keep all the user's roles (attrs-only narrowing). A claimed role the
|
|
104
|
+
* user lacks is dropped by the intersection (fail-closed). Absent
|
|
105
|
+
* `attenuate` → a single evaluation, byte-for-byte today's behavior.
|
|
106
|
+
*/
|
|
107
|
+
attenuate?: {
|
|
108
|
+
roles?: string[];
|
|
109
|
+
attrs?: Partial<TUserAttrs>;
|
|
110
|
+
};
|
|
82
111
|
}): Promise<TArbacEvalResult<TScope>>;
|
|
112
|
+
/**
|
|
113
|
+
* One evaluation pass over a concrete role-id set + attrs resolver. Carries
|
|
114
|
+
* the deny-wins / allow-union / `{}` universe-sentinel semantics; `evaluate`
|
|
115
|
+
* runs it once (no attenuation) or twice (ceiling + narrowed credential view).
|
|
116
|
+
*/
|
|
117
|
+
protected evaluateForRoles(res: {
|
|
118
|
+
resource: string;
|
|
119
|
+
action: string;
|
|
120
|
+
}, roleIds: string[], getAttrs: () => Promise<TUserAttrs>, id: string | undefined): Promise<TArbacEvalResult<TScope>>;
|
|
83
121
|
}
|
|
84
122
|
//#endregion
|
|
85
123
|
//#region src/utils.d.ts
|
package/dist/index.d.mts
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
interface TArbacEvalResult<TScope> {
|
|
3
3
|
allowed: boolean;
|
|
4
4
|
scopes?: TScope[];
|
|
5
|
+
/**
|
|
6
|
+
* Present ONLY on an attenuated evaluation (`user.attenuate` was supplied).
|
|
7
|
+
* The scopes from the credential's narrowed pass, to be CONJOINED with
|
|
8
|
+
* `scopes` (the full-authority pass) by the scope layer — a row/field is
|
|
9
|
+
* effective only if BOTH passes admit it. The conjunction is the scope
|
|
10
|
+
* shape's responsibility (the engine is scope-agnostic), so the two unions
|
|
11
|
+
* are surfaced separately here rather than pre-combined. Absent on a normal
|
|
12
|
+
* (non-attenuated) evaluation.
|
|
13
|
+
*/
|
|
14
|
+
credScopes?: TScope[];
|
|
5
15
|
}
|
|
6
16
|
interface TArbacRole<TUserAttrs, TScope> {
|
|
7
17
|
id: string;
|
|
@@ -62,14 +72,13 @@ declare class Arbac<TUserAttrs extends object, TScope extends object> {
|
|
|
62
72
|
*/
|
|
63
73
|
protected evalRoleForResource(roleId: string, resourceId: string): Arbac<TUserAttrs, TScope>;
|
|
64
74
|
/**
|
|
65
|
-
* Evaluates whether a given action on a resource is allowed for a user with
|
|
75
|
+
* Evaluates whether a given action on a resource is allowed for a user with
|
|
76
|
+
* the specified roles, returning the allow decision plus any applicable scopes.
|
|
77
|
+
*
|
|
78
|
+
* When `user.attenuate` is supplied (the credential-claims bridge) the policy
|
|
79
|
+
* is evaluated TWICE and the OUTCOMES are intersected — see the `attenuate`
|
|
80
|
+
* field for the restrict-only safety guarantee.
|
|
66
81
|
*
|
|
67
|
-
* @param {{
|
|
68
|
-
* roleIds: string[];
|
|
69
|
-
* userId: string;
|
|
70
|
-
* resource: string;
|
|
71
|
-
* action: string;
|
|
72
|
-
* }} res The options for the evaluation.
|
|
73
82
|
* @returns {Promise<TArbacEvalResult<TScope>>} The result of the evaluation, including whether the action is allowed and any applicable scopes.
|
|
74
83
|
*/
|
|
75
84
|
evaluate<T extends string | undefined>(res: {
|
|
@@ -79,7 +88,36 @@ declare class Arbac<TUserAttrs extends object, TScope extends object> {
|
|
|
79
88
|
id: T;
|
|
80
89
|
roles: string[];
|
|
81
90
|
attrs: TUserAttrs | ((userId: T) => TUserAttrs | Promise<TUserAttrs>);
|
|
91
|
+
/**
|
|
92
|
+
* Opt-in, restrict-only attenuation sourced from the authenticated
|
|
93
|
+
* credential's claims. When present the policy runs twice — at full
|
|
94
|
+
* authority (the ceiling) and at the credential's narrowed view — and
|
|
95
|
+
* the outcomes are intersected: `allowed` is ANDed (so dropping a role
|
|
96
|
+
* can never drop a matching `deny` and thus can never escalate a denied
|
|
97
|
+
* action), and the two passes' scope lists are surfaced separately as
|
|
98
|
+
* `scopes` (ceiling) and `credScopes` (narrowed) for the scope layer to
|
|
99
|
+
* conjoin. A credential can therefore only ever do/see/affect LESS than
|
|
100
|
+
* its owning user — escalation-proof even against an untrusted minter.
|
|
101
|
+
*
|
|
102
|
+
* `roles: []` → no roles (deny-all, fail-closed). An OMITTED `roles` key
|
|
103
|
+
* → keep all the user's roles (attrs-only narrowing). A claimed role the
|
|
104
|
+
* user lacks is dropped by the intersection (fail-closed). Absent
|
|
105
|
+
* `attenuate` → a single evaluation, byte-for-byte today's behavior.
|
|
106
|
+
*/
|
|
107
|
+
attenuate?: {
|
|
108
|
+
roles?: string[];
|
|
109
|
+
attrs?: Partial<TUserAttrs>;
|
|
110
|
+
};
|
|
82
111
|
}): Promise<TArbacEvalResult<TScope>>;
|
|
112
|
+
/**
|
|
113
|
+
* One evaluation pass over a concrete role-id set + attrs resolver. Carries
|
|
114
|
+
* the deny-wins / allow-union / `{}` universe-sentinel semantics; `evaluate`
|
|
115
|
+
* runs it once (no attenuation) or twice (ceiling + narrowed credential view).
|
|
116
|
+
*/
|
|
117
|
+
protected evaluateForRoles(res: {
|
|
118
|
+
resource: string;
|
|
119
|
+
action: string;
|
|
120
|
+
}, roleIds: string[], getAttrs: () => Promise<TUserAttrs>, id: string | undefined): Promise<TArbacEvalResult<TScope>>;
|
|
83
121
|
}
|
|
84
122
|
//#endregion
|
|
85
123
|
//#region src/utils.d.ts
|
package/dist/index.mjs
CHANGED
|
@@ -80,37 +80,61 @@ var Arbac = class {
|
|
|
80
80
|
return this;
|
|
81
81
|
}
|
|
82
82
|
/**
|
|
83
|
-
* Evaluates whether a given action on a resource is allowed for a user with
|
|
83
|
+
* Evaluates whether a given action on a resource is allowed for a user with
|
|
84
|
+
* the specified roles, returning the allow decision plus any applicable scopes.
|
|
85
|
+
*
|
|
86
|
+
* When `user.attenuate` is supplied (the credential-claims bridge) the policy
|
|
87
|
+
* is evaluated TWICE and the OUTCOMES are intersected — see the `attenuate`
|
|
88
|
+
* field for the restrict-only safety guarantee.
|
|
84
89
|
*
|
|
85
|
-
* @param {{
|
|
86
|
-
* roleIds: string[];
|
|
87
|
-
* userId: string;
|
|
88
|
-
* resource: string;
|
|
89
|
-
* action: string;
|
|
90
|
-
* }} res The options for the evaluation.
|
|
91
90
|
* @returns {Promise<TArbacEvalResult<TScope>>} The result of the evaluation, including whether the action is allowed and any applicable scopes.
|
|
92
91
|
*/
|
|
93
92
|
async evaluate(res, user) {
|
|
94
93
|
this.registerResource(res.resource);
|
|
95
|
-
|
|
94
|
+
let resolvedAttrs;
|
|
95
|
+
const userAttrs = async () => {
|
|
96
|
+
if (resolvedAttrs === void 0) resolvedAttrs = typeof user.attrs === "function" ? await user.attrs(user.id) : user.attrs;
|
|
97
|
+
return resolvedAttrs;
|
|
98
|
+
};
|
|
99
|
+
const userEval = await this.evaluateForRoles(res, user.roles, userAttrs, user.id);
|
|
100
|
+
if (!user.attenuate) return userEval;
|
|
101
|
+
const claimRoles = user.attenuate.roles;
|
|
102
|
+
const claimAttrs = user.attenuate.attrs;
|
|
103
|
+
const credRoles = claimRoles ? user.roles.filter((r) => claimRoles.includes(r)) : user.roles;
|
|
104
|
+
const credAttrs = claimAttrs ? async () => ({
|
|
105
|
+
...await userAttrs(),
|
|
106
|
+
...claimAttrs
|
|
107
|
+
}) : userAttrs;
|
|
108
|
+
const credEval = await this.evaluateForRoles(res, credRoles, credAttrs, user.id);
|
|
109
|
+
if (!userEval.allowed || !credEval.allowed) return { allowed: false };
|
|
110
|
+
return {
|
|
111
|
+
allowed: true,
|
|
112
|
+
scopes: userEval.scopes,
|
|
113
|
+
credScopes: credEval.scopes
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* One evaluation pass over a concrete role-id set + attrs resolver. Carries
|
|
118
|
+
* the deny-wins / allow-union / `{}` universe-sentinel semantics; `evaluate`
|
|
119
|
+
* runs it once (no attenuation) or twice (ceiling + narrowed credential view).
|
|
120
|
+
*/
|
|
121
|
+
async evaluateForRoles(res, roleIds, getAttrs, id) {
|
|
122
|
+
const roles = roleIds.map((r) => {
|
|
96
123
|
const role = this.resources[res.resource][r];
|
|
97
124
|
if (!role && !this.roles[r] && !this.warnedRoles.has(r)) {
|
|
98
125
|
this.warnedRoles.add(r);
|
|
99
|
-
console.warn(`Role "${r}" assigned to user "${
|
|
126
|
+
console.warn(`Role "${r}" assigned to user "${id}" does not exist.`);
|
|
100
127
|
}
|
|
101
128
|
return role;
|
|
102
129
|
}).filter(Boolean);
|
|
103
130
|
if (roles.length === 0) return { allowed: false };
|
|
104
131
|
for (const role of roles) for (const rule of role.deny) if (rule._actionRegex.test(res.action)) return { allowed: false };
|
|
105
|
-
let userAttrs;
|
|
106
132
|
const scopes = [];
|
|
107
133
|
let allowed = false;
|
|
108
134
|
for (const role of roles) for (const rule of role.allow) if (rule._actionRegex.test(res.action)) {
|
|
109
135
|
allowed = true;
|
|
110
|
-
if (rule.scope)
|
|
111
|
-
|
|
112
|
-
scopes.push(rule.scope(userAttrs, String(user.id)));
|
|
113
|
-
} else scopes.push({});
|
|
136
|
+
if (rule.scope) scopes.push(rule.scope(await getAttrs(), String(id)));
|
|
137
|
+
else scopes.push({});
|
|
114
138
|
}
|
|
115
139
|
return allowed ? {
|
|
116
140
|
allowed,
|