@auth-gate/rbac 0.8.0 → 0.8.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/dist/{chunk-HE57TIQI.mjs → chunk-5YEMXO7B.mjs} +88 -21
- package/dist/cli.cjs +82 -21
- package/dist/cli.mjs +1 -1
- package/dist/index.cjs +86 -22
- package/dist/index.d.cts +4 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.mjs +5 -2
- package/package.json +1 -1
|
@@ -14,6 +14,12 @@ var __spreadValues = (a, b) => {
|
|
|
14
14
|
}
|
|
15
15
|
return a;
|
|
16
16
|
};
|
|
17
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
18
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
19
|
+
}) : x)(function(x) {
|
|
20
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
21
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
22
|
+
});
|
|
17
23
|
|
|
18
24
|
// src/validator.ts
|
|
19
25
|
var KEY_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
@@ -63,15 +69,28 @@ function validateRbacConfig(config) {
|
|
|
63
69
|
);
|
|
64
70
|
}
|
|
65
71
|
}
|
|
66
|
-
|
|
72
|
+
const actionSet = /* @__PURE__ */ new Set();
|
|
73
|
+
for (const action of resource.actions) {
|
|
74
|
+
if (actionSet.has(action)) {
|
|
75
|
+
throw new Error(`Resource "${key}" has duplicate action: "${action}".`);
|
|
76
|
+
}
|
|
77
|
+
actionSet.add(action);
|
|
78
|
+
}
|
|
79
|
+
resourceActions.set(key, actionSet);
|
|
67
80
|
if (resource.scopes !== void 0) {
|
|
68
81
|
if (!Array.isArray(resource.scopes)) {
|
|
69
82
|
throw new Error(`Resource "${key}".scopes must be an array of strings.`);
|
|
70
83
|
}
|
|
84
|
+
const scopeSet = /* @__PURE__ */ new Set();
|
|
71
85
|
for (let i = 0; i < resource.scopes.length; i++) {
|
|
72
86
|
if (typeof resource.scopes[i] !== "string") {
|
|
73
87
|
throw new Error(`Resource "${key}".scopes[${i}] must be a string.`);
|
|
74
88
|
}
|
|
89
|
+
const scope = resource.scopes[i];
|
|
90
|
+
if (scopeSet.has(scope)) {
|
|
91
|
+
throw new Error(`Resource "${key}" has duplicate scope: "${scope}".`);
|
|
92
|
+
}
|
|
93
|
+
scopeSet.add(scope);
|
|
75
94
|
}
|
|
76
95
|
}
|
|
77
96
|
}
|
|
@@ -112,11 +131,22 @@ function validateRbacConfig(config) {
|
|
|
112
131
|
);
|
|
113
132
|
}
|
|
114
133
|
const value = actionsGrant[actionKey];
|
|
115
|
-
if (value !== true && typeof value !== "string" && !(value && typeof value === "object" && "when" in value && typeof value.when === "string")) {
|
|
134
|
+
if (value !== true && (typeof value !== "string" || value === "") && !(value && typeof value === "object" && "when" in value && typeof value.when === "string" && value.when !== "")) {
|
|
116
135
|
throw new Error(
|
|
117
|
-
`Role "${key}".grants.${resourceKey}.${actionKey} has invalid value. Expected true, a scope string, or { when: string }.`
|
|
136
|
+
`Role "${key}".grants.${resourceKey}.${actionKey} has invalid value. Expected true, a non-empty scope string, or { when: non-empty string }.`
|
|
118
137
|
);
|
|
119
138
|
}
|
|
139
|
+
if (typeof value === "string" && value !== "") {
|
|
140
|
+
const resource = resources[resourceKey];
|
|
141
|
+
if (resource.scopes && Array.isArray(resource.scopes)) {
|
|
142
|
+
const declaredScopes = new Set(resource.scopes);
|
|
143
|
+
if (!declaredScopes.has(value)) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
`Role "${key}".grants.${resourceKey}.${actionKey} scope "${value}" is not in declared scopes for "${resourceKey}": ${[...declaredScopes].join(", ")}.`
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
120
150
|
}
|
|
121
151
|
}
|
|
122
152
|
}
|
|
@@ -185,6 +215,7 @@ function validateRbacConfig(config) {
|
|
|
185
215
|
renameTargets.set(role.renamedFrom, key);
|
|
186
216
|
}
|
|
187
217
|
}
|
|
218
|
+
const conditionKeys = /* @__PURE__ */ new Set();
|
|
188
219
|
if (c.conditions !== void 0) {
|
|
189
220
|
if (typeof c.conditions !== "object" || Array.isArray(c.conditions) || c.conditions === null) {
|
|
190
221
|
throw new Error("RBAC config `conditions` must be an object mapping names to functions.");
|
|
@@ -196,6 +227,23 @@ function validateRbacConfig(config) {
|
|
|
196
227
|
`Condition "${condKey}" must be a function. Got ${typeof condValue}.`
|
|
197
228
|
);
|
|
198
229
|
}
|
|
230
|
+
conditionKeys.add(condKey);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
for (const key of roleKeys) {
|
|
234
|
+
const role = roles[key];
|
|
235
|
+
const grants = role.grants;
|
|
236
|
+
for (const [resourceKey, actionGrants] of Object.entries(grants)) {
|
|
237
|
+
for (const [actionKey, value] of Object.entries(actionGrants)) {
|
|
238
|
+
if (value && typeof value === "object" && "when" in value) {
|
|
239
|
+
const whenKey = value.when;
|
|
240
|
+
if (!conditionKeys.has(whenKey)) {
|
|
241
|
+
throw new Error(
|
|
242
|
+
`Role "${key}".grants.${resourceKey}.${actionKey} references condition "${whenKey}" but it is not declared in conditions.` + (conditionKeys.size > 0 ? ` Declared conditions: ${[...conditionKeys].join(", ")}.` : ` No conditions are declared.`)
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
199
247
|
}
|
|
200
248
|
}
|
|
201
249
|
return config;
|
|
@@ -234,17 +282,27 @@ Run \`npx @auth-gate/rbac init\` to create one.`
|
|
|
234
282
|
}
|
|
235
283
|
|
|
236
284
|
// src/diff.ts
|
|
285
|
+
function deepEqual(a, b) {
|
|
286
|
+
if (a === b) return true;
|
|
287
|
+
if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
|
|
288
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
289
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
290
|
+
if (a.length !== b.length) return false;
|
|
291
|
+
return a.every((val, i) => deepEqual(val, b[i]));
|
|
292
|
+
}
|
|
293
|
+
const keysA = Object.keys(a).sort();
|
|
294
|
+
const keysB = Object.keys(b).sort();
|
|
295
|
+
if (keysA.length !== keysB.length) return false;
|
|
296
|
+
return keysA.every(
|
|
297
|
+
(key, i) => key === keysB[i] && deepEqual(a[key], b[key])
|
|
298
|
+
);
|
|
299
|
+
}
|
|
237
300
|
function hashConditionSource(fn) {
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
for (let i = 0; i < src.length; i++) {
|
|
241
|
-
const ch = src.charCodeAt(i);
|
|
242
|
-
hash = (hash << 5) - hash + ch | 0;
|
|
243
|
-
}
|
|
244
|
-
return hash.toString(36);
|
|
301
|
+
const { createHash } = __require("crypto");
|
|
302
|
+
return createHash("sha256").update(fn.toString()).digest("hex").slice(0, 16);
|
|
245
303
|
}
|
|
246
304
|
function computeRbacDiff(config, server, memberCounts) {
|
|
247
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
|
|
305
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
|
|
248
306
|
const resourceOps = [];
|
|
249
307
|
const roleOps = [];
|
|
250
308
|
const conditionOps = [];
|
|
@@ -303,7 +361,7 @@ function computeRbacDiff(config, server, memberCounts) {
|
|
|
303
361
|
if (!existing && role.renamedFrom) {
|
|
304
362
|
existing = (_d = serverRoleByKey.get(role.renamedFrom)) != null ? _d : serverRoleByPreviousKey.get(role.renamedFrom);
|
|
305
363
|
if (existing) {
|
|
306
|
-
const members = (_e = memberCounts[existing.
|
|
364
|
+
const members = (_e = memberCounts[existing.configKey]) != null ? _e : 0;
|
|
307
365
|
roleOps.push({
|
|
308
366
|
type: "rename",
|
|
309
367
|
key,
|
|
@@ -327,13 +385,11 @@ function computeRbacDiff(config, server, memberCounts) {
|
|
|
327
385
|
if (((_f = existing.description) != null ? _f : void 0) !== ((_g = role.description) != null ? _g : void 0)) {
|
|
328
386
|
changes.push("description changed");
|
|
329
387
|
}
|
|
330
|
-
|
|
331
|
-
const configGrants = JSON.stringify(role.grants);
|
|
332
|
-
if (existingGrants !== configGrants) {
|
|
388
|
+
if (!deepEqual((_h = existing.grants) != null ? _h : null, (_i = role.grants) != null ? _i : null)) {
|
|
333
389
|
changes.push("grants changed");
|
|
334
390
|
}
|
|
335
|
-
const existingInherits = [...(
|
|
336
|
-
const configInherits = [...(
|
|
391
|
+
const existingInherits = [...(_j = existing.inherits) != null ? _j : []].sort().join(",");
|
|
392
|
+
const configInherits = [...(_k = role.inherits) != null ? _k : []].sort().join(",");
|
|
337
393
|
if (existingInherits !== configInherits) {
|
|
338
394
|
changes.push("inherits changed");
|
|
339
395
|
}
|
|
@@ -346,7 +402,7 @@ function computeRbacDiff(config, server, memberCounts) {
|
|
|
346
402
|
for (const [key, role] of serverRoleByKey) {
|
|
347
403
|
if (renameMap.has(key)) continue;
|
|
348
404
|
if (role.isActive) {
|
|
349
|
-
const members = (
|
|
405
|
+
const members = (_l = memberCounts[role.configKey]) != null ? _l : 0;
|
|
350
406
|
if (members > 0) hasDestructive = true;
|
|
351
407
|
roleOps.push({ type: "archive", key, existing: role, assignedMembers: members });
|
|
352
408
|
}
|
|
@@ -508,6 +564,11 @@ var RbacSyncClient = class {
|
|
|
508
564
|
constructor(config) {
|
|
509
565
|
this.baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
510
566
|
this.apiKey = config.apiKey;
|
|
567
|
+
if (!this.baseUrl.startsWith("https://") && !this.baseUrl.startsWith("http://localhost") && !this.baseUrl.startsWith("http://127.0.0.1")) {
|
|
568
|
+
console.warn(
|
|
569
|
+
"WARNING: AUTHGATE_BASE_URL does not use HTTPS. API key may be transmitted insecurely."
|
|
570
|
+
);
|
|
571
|
+
}
|
|
511
572
|
}
|
|
512
573
|
async request(method, path, body) {
|
|
513
574
|
var _a, _b;
|
|
@@ -526,9 +587,12 @@ var RbacSyncClient = class {
|
|
|
526
587
|
let message;
|
|
527
588
|
try {
|
|
528
589
|
const json = JSON.parse(text);
|
|
529
|
-
message = (_b = (_a = json.error) != null ? _a : json.message) != null ? _b :
|
|
590
|
+
message = (_b = (_a = json.error) != null ? _a : json.message) != null ? _b : "Unknown error";
|
|
530
591
|
} catch (e) {
|
|
531
|
-
message = text;
|
|
592
|
+
message = text.length > 500 ? text.slice(0, 500) + "..." : text;
|
|
593
|
+
}
|
|
594
|
+
if (this.apiKey) {
|
|
595
|
+
message = message.replaceAll(this.apiKey, "[REDACTED]");
|
|
532
596
|
}
|
|
533
597
|
throw new Error(`API error (${res.status}): ${message}`);
|
|
534
598
|
}
|
|
@@ -548,7 +612,10 @@ var RbacSyncClient = class {
|
|
|
548
612
|
resources: config.resources,
|
|
549
613
|
roles: config.roles,
|
|
550
614
|
conditions: config.conditions ? Object.fromEntries(
|
|
551
|
-
Object.entries(config.conditions).map(([k]) => [
|
|
615
|
+
Object.entries(config.conditions).map(([k, fn]) => [
|
|
616
|
+
k,
|
|
617
|
+
{ key: k, sourceHash: hashConditionSource(fn) }
|
|
618
|
+
])
|
|
552
619
|
) : void 0,
|
|
553
620
|
force
|
|
554
621
|
});
|
package/dist/cli.cjs
CHANGED
|
@@ -89,15 +89,28 @@ function validateRbacConfig(config) {
|
|
|
89
89
|
);
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
|
-
|
|
92
|
+
const actionSet = /* @__PURE__ */ new Set();
|
|
93
|
+
for (const action of resource.actions) {
|
|
94
|
+
if (actionSet.has(action)) {
|
|
95
|
+
throw new Error(`Resource "${key}" has duplicate action: "${action}".`);
|
|
96
|
+
}
|
|
97
|
+
actionSet.add(action);
|
|
98
|
+
}
|
|
99
|
+
resourceActions.set(key, actionSet);
|
|
93
100
|
if (resource.scopes !== void 0) {
|
|
94
101
|
if (!Array.isArray(resource.scopes)) {
|
|
95
102
|
throw new Error(`Resource "${key}".scopes must be an array of strings.`);
|
|
96
103
|
}
|
|
104
|
+
const scopeSet = /* @__PURE__ */ new Set();
|
|
97
105
|
for (let i = 0; i < resource.scopes.length; i++) {
|
|
98
106
|
if (typeof resource.scopes[i] !== "string") {
|
|
99
107
|
throw new Error(`Resource "${key}".scopes[${i}] must be a string.`);
|
|
100
108
|
}
|
|
109
|
+
const scope = resource.scopes[i];
|
|
110
|
+
if (scopeSet.has(scope)) {
|
|
111
|
+
throw new Error(`Resource "${key}" has duplicate scope: "${scope}".`);
|
|
112
|
+
}
|
|
113
|
+
scopeSet.add(scope);
|
|
101
114
|
}
|
|
102
115
|
}
|
|
103
116
|
}
|
|
@@ -138,11 +151,22 @@ function validateRbacConfig(config) {
|
|
|
138
151
|
);
|
|
139
152
|
}
|
|
140
153
|
const value = actionsGrant[actionKey];
|
|
141
|
-
if (value !== true && typeof value !== "string" && !(value && typeof value === "object" && "when" in value && typeof value.when === "string")) {
|
|
154
|
+
if (value !== true && (typeof value !== "string" || value === "") && !(value && typeof value === "object" && "when" in value && typeof value.when === "string" && value.when !== "")) {
|
|
142
155
|
throw new Error(
|
|
143
|
-
`Role "${key}".grants.${resourceKey}.${actionKey} has invalid value. Expected true, a scope string, or { when: string }.`
|
|
156
|
+
`Role "${key}".grants.${resourceKey}.${actionKey} has invalid value. Expected true, a non-empty scope string, or { when: non-empty string }.`
|
|
144
157
|
);
|
|
145
158
|
}
|
|
159
|
+
if (typeof value === "string" && value !== "") {
|
|
160
|
+
const resource = resources[resourceKey];
|
|
161
|
+
if (resource.scopes && Array.isArray(resource.scopes)) {
|
|
162
|
+
const declaredScopes = new Set(resource.scopes);
|
|
163
|
+
if (!declaredScopes.has(value)) {
|
|
164
|
+
throw new Error(
|
|
165
|
+
`Role "${key}".grants.${resourceKey}.${actionKey} scope "${value}" is not in declared scopes for "${resourceKey}": ${[...declaredScopes].join(", ")}.`
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
146
170
|
}
|
|
147
171
|
}
|
|
148
172
|
}
|
|
@@ -211,6 +235,7 @@ function validateRbacConfig(config) {
|
|
|
211
235
|
renameTargets.set(role.renamedFrom, key);
|
|
212
236
|
}
|
|
213
237
|
}
|
|
238
|
+
const conditionKeys = /* @__PURE__ */ new Set();
|
|
214
239
|
if (c.conditions !== void 0) {
|
|
215
240
|
if (typeof c.conditions !== "object" || Array.isArray(c.conditions) || c.conditions === null) {
|
|
216
241
|
throw new Error("RBAC config `conditions` must be an object mapping names to functions.");
|
|
@@ -222,6 +247,23 @@ function validateRbacConfig(config) {
|
|
|
222
247
|
`Condition "${condKey}" must be a function. Got ${typeof condValue}.`
|
|
223
248
|
);
|
|
224
249
|
}
|
|
250
|
+
conditionKeys.add(condKey);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
for (const key of roleKeys) {
|
|
254
|
+
const role = roles[key];
|
|
255
|
+
const grants = role.grants;
|
|
256
|
+
for (const [resourceKey, actionGrants] of Object.entries(grants)) {
|
|
257
|
+
for (const [actionKey, value] of Object.entries(actionGrants)) {
|
|
258
|
+
if (value && typeof value === "object" && "when" in value) {
|
|
259
|
+
const whenKey = value.when;
|
|
260
|
+
if (!conditionKeys.has(whenKey)) {
|
|
261
|
+
throw new Error(
|
|
262
|
+
`Role "${key}".grants.${resourceKey}.${actionKey} references condition "${whenKey}" but it is not declared in conditions.` + (conditionKeys.size > 0 ? ` Declared conditions: ${[...conditionKeys].join(", ")}.` : ` No conditions are declared.`)
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
225
267
|
}
|
|
226
268
|
}
|
|
227
269
|
return config;
|
|
@@ -258,17 +300,27 @@ Run \`npx @auth-gate/rbac init\` to create one.`
|
|
|
258
300
|
}
|
|
259
301
|
|
|
260
302
|
// src/diff.ts
|
|
303
|
+
function deepEqual(a, b) {
|
|
304
|
+
if (a === b) return true;
|
|
305
|
+
if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
|
|
306
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
307
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
308
|
+
if (a.length !== b.length) return false;
|
|
309
|
+
return a.every((val, i) => deepEqual(val, b[i]));
|
|
310
|
+
}
|
|
311
|
+
const keysA = Object.keys(a).sort();
|
|
312
|
+
const keysB = Object.keys(b).sort();
|
|
313
|
+
if (keysA.length !== keysB.length) return false;
|
|
314
|
+
return keysA.every(
|
|
315
|
+
(key, i) => key === keysB[i] && deepEqual(a[key], b[key])
|
|
316
|
+
);
|
|
317
|
+
}
|
|
261
318
|
function hashConditionSource(fn) {
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
for (let i = 0; i < src.length; i++) {
|
|
265
|
-
const ch = src.charCodeAt(i);
|
|
266
|
-
hash = (hash << 5) - hash + ch | 0;
|
|
267
|
-
}
|
|
268
|
-
return hash.toString(36);
|
|
319
|
+
const { createHash } = require("crypto");
|
|
320
|
+
return createHash("sha256").update(fn.toString()).digest("hex").slice(0, 16);
|
|
269
321
|
}
|
|
270
322
|
function computeRbacDiff(config, server, memberCounts) {
|
|
271
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
|
|
323
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
|
|
272
324
|
const resourceOps = [];
|
|
273
325
|
const roleOps = [];
|
|
274
326
|
const conditionOps = [];
|
|
@@ -327,7 +379,7 @@ function computeRbacDiff(config, server, memberCounts) {
|
|
|
327
379
|
if (!existing && role.renamedFrom) {
|
|
328
380
|
existing = (_d = serverRoleByKey.get(role.renamedFrom)) != null ? _d : serverRoleByPreviousKey.get(role.renamedFrom);
|
|
329
381
|
if (existing) {
|
|
330
|
-
const members = (_e = memberCounts[existing.
|
|
382
|
+
const members = (_e = memberCounts[existing.configKey]) != null ? _e : 0;
|
|
331
383
|
roleOps.push({
|
|
332
384
|
type: "rename",
|
|
333
385
|
key,
|
|
@@ -351,13 +403,11 @@ function computeRbacDiff(config, server, memberCounts) {
|
|
|
351
403
|
if (((_f = existing.description) != null ? _f : void 0) !== ((_g = role.description) != null ? _g : void 0)) {
|
|
352
404
|
changes.push("description changed");
|
|
353
405
|
}
|
|
354
|
-
|
|
355
|
-
const configGrants = JSON.stringify(role.grants);
|
|
356
|
-
if (existingGrants !== configGrants) {
|
|
406
|
+
if (!deepEqual((_h = existing.grants) != null ? _h : null, (_i = role.grants) != null ? _i : null)) {
|
|
357
407
|
changes.push("grants changed");
|
|
358
408
|
}
|
|
359
|
-
const existingInherits = [...(
|
|
360
|
-
const configInherits = [...(
|
|
409
|
+
const existingInherits = [...(_j = existing.inherits) != null ? _j : []].sort().join(",");
|
|
410
|
+
const configInherits = [...(_k = role.inherits) != null ? _k : []].sort().join(",");
|
|
361
411
|
if (existingInherits !== configInherits) {
|
|
362
412
|
changes.push("inherits changed");
|
|
363
413
|
}
|
|
@@ -370,7 +420,7 @@ function computeRbacDiff(config, server, memberCounts) {
|
|
|
370
420
|
for (const [key, role] of serverRoleByKey) {
|
|
371
421
|
if (renameMap.has(key)) continue;
|
|
372
422
|
if (role.isActive) {
|
|
373
|
-
const members = (
|
|
423
|
+
const members = (_l = memberCounts[role.configKey]) != null ? _l : 0;
|
|
374
424
|
if (members > 0) hasDestructive = true;
|
|
375
425
|
roleOps.push({ type: "archive", key, existing: role, assignedMembers: members });
|
|
376
426
|
}
|
|
@@ -532,6 +582,11 @@ var RbacSyncClient = class {
|
|
|
532
582
|
constructor(config) {
|
|
533
583
|
this.baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
534
584
|
this.apiKey = config.apiKey;
|
|
585
|
+
if (!this.baseUrl.startsWith("https://") && !this.baseUrl.startsWith("http://localhost") && !this.baseUrl.startsWith("http://127.0.0.1")) {
|
|
586
|
+
console.warn(
|
|
587
|
+
"WARNING: AUTHGATE_BASE_URL does not use HTTPS. API key may be transmitted insecurely."
|
|
588
|
+
);
|
|
589
|
+
}
|
|
535
590
|
}
|
|
536
591
|
async request(method, path, body) {
|
|
537
592
|
var _a, _b;
|
|
@@ -550,9 +605,12 @@ var RbacSyncClient = class {
|
|
|
550
605
|
let message;
|
|
551
606
|
try {
|
|
552
607
|
const json = JSON.parse(text);
|
|
553
|
-
message = (_b = (_a = json.error) != null ? _a : json.message) != null ? _b :
|
|
608
|
+
message = (_b = (_a = json.error) != null ? _a : json.message) != null ? _b : "Unknown error";
|
|
554
609
|
} catch (e) {
|
|
555
|
-
message = text;
|
|
610
|
+
message = text.length > 500 ? text.slice(0, 500) + "..." : text;
|
|
611
|
+
}
|
|
612
|
+
if (this.apiKey) {
|
|
613
|
+
message = message.replaceAll(this.apiKey, "[REDACTED]");
|
|
556
614
|
}
|
|
557
615
|
throw new Error(`API error (${res.status}): ${message}`);
|
|
558
616
|
}
|
|
@@ -572,7 +630,10 @@ var RbacSyncClient = class {
|
|
|
572
630
|
resources: config.resources,
|
|
573
631
|
roles: config.roles,
|
|
574
632
|
conditions: config.conditions ? Object.fromEntries(
|
|
575
|
-
Object.entries(config.conditions).map(([k]) => [
|
|
633
|
+
Object.entries(config.conditions).map(([k, fn]) => [
|
|
634
|
+
k,
|
|
635
|
+
{ key: k, sourceHash: hashConditionSource(fn) }
|
|
636
|
+
])
|
|
576
637
|
) : void 0,
|
|
577
638
|
force
|
|
578
639
|
});
|
package/dist/cli.mjs
CHANGED
package/dist/index.cjs
CHANGED
|
@@ -88,15 +88,28 @@ function validateRbacConfig(config) {
|
|
|
88
88
|
);
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
|
-
|
|
91
|
+
const actionSet = /* @__PURE__ */ new Set();
|
|
92
|
+
for (const action of resource.actions) {
|
|
93
|
+
if (actionSet.has(action)) {
|
|
94
|
+
throw new Error(`Resource "${key}" has duplicate action: "${action}".`);
|
|
95
|
+
}
|
|
96
|
+
actionSet.add(action);
|
|
97
|
+
}
|
|
98
|
+
resourceActions.set(key, actionSet);
|
|
92
99
|
if (resource.scopes !== void 0) {
|
|
93
100
|
if (!Array.isArray(resource.scopes)) {
|
|
94
101
|
throw new Error(`Resource "${key}".scopes must be an array of strings.`);
|
|
95
102
|
}
|
|
103
|
+
const scopeSet = /* @__PURE__ */ new Set();
|
|
96
104
|
for (let i = 0; i < resource.scopes.length; i++) {
|
|
97
105
|
if (typeof resource.scopes[i] !== "string") {
|
|
98
106
|
throw new Error(`Resource "${key}".scopes[${i}] must be a string.`);
|
|
99
107
|
}
|
|
108
|
+
const scope = resource.scopes[i];
|
|
109
|
+
if (scopeSet.has(scope)) {
|
|
110
|
+
throw new Error(`Resource "${key}" has duplicate scope: "${scope}".`);
|
|
111
|
+
}
|
|
112
|
+
scopeSet.add(scope);
|
|
100
113
|
}
|
|
101
114
|
}
|
|
102
115
|
}
|
|
@@ -137,11 +150,22 @@ function validateRbacConfig(config) {
|
|
|
137
150
|
);
|
|
138
151
|
}
|
|
139
152
|
const value = actionsGrant[actionKey];
|
|
140
|
-
if (value !== true && typeof value !== "string" && !(value && typeof value === "object" && "when" in value && typeof value.when === "string")) {
|
|
153
|
+
if (value !== true && (typeof value !== "string" || value === "") && !(value && typeof value === "object" && "when" in value && typeof value.when === "string" && value.when !== "")) {
|
|
141
154
|
throw new Error(
|
|
142
|
-
`Role "${key}".grants.${resourceKey}.${actionKey} has invalid value. Expected true, a scope string, or { when: string }.`
|
|
155
|
+
`Role "${key}".grants.${resourceKey}.${actionKey} has invalid value. Expected true, a non-empty scope string, or { when: non-empty string }.`
|
|
143
156
|
);
|
|
144
157
|
}
|
|
158
|
+
if (typeof value === "string" && value !== "") {
|
|
159
|
+
const resource = resources[resourceKey];
|
|
160
|
+
if (resource.scopes && Array.isArray(resource.scopes)) {
|
|
161
|
+
const declaredScopes = new Set(resource.scopes);
|
|
162
|
+
if (!declaredScopes.has(value)) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
`Role "${key}".grants.${resourceKey}.${actionKey} scope "${value}" is not in declared scopes for "${resourceKey}": ${[...declaredScopes].join(", ")}.`
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
145
169
|
}
|
|
146
170
|
}
|
|
147
171
|
}
|
|
@@ -210,6 +234,7 @@ function validateRbacConfig(config) {
|
|
|
210
234
|
renameTargets.set(role.renamedFrom, key);
|
|
211
235
|
}
|
|
212
236
|
}
|
|
237
|
+
const conditionKeys = /* @__PURE__ */ new Set();
|
|
213
238
|
if (c.conditions !== void 0) {
|
|
214
239
|
if (typeof c.conditions !== "object" || Array.isArray(c.conditions) || c.conditions === null) {
|
|
215
240
|
throw new Error("RBAC config `conditions` must be an object mapping names to functions.");
|
|
@@ -221,6 +246,23 @@ function validateRbacConfig(config) {
|
|
|
221
246
|
`Condition "${condKey}" must be a function. Got ${typeof condValue}.`
|
|
222
247
|
);
|
|
223
248
|
}
|
|
249
|
+
conditionKeys.add(condKey);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
for (const key of roleKeys) {
|
|
253
|
+
const role = roles[key];
|
|
254
|
+
const grants = role.grants;
|
|
255
|
+
for (const [resourceKey, actionGrants] of Object.entries(grants)) {
|
|
256
|
+
for (const [actionKey, value] of Object.entries(actionGrants)) {
|
|
257
|
+
if (value && typeof value === "object" && "when" in value) {
|
|
258
|
+
const whenKey = value.when;
|
|
259
|
+
if (!conditionKeys.has(whenKey)) {
|
|
260
|
+
throw new Error(
|
|
261
|
+
`Role "${key}".grants.${resourceKey}.${actionKey} references condition "${whenKey}" but it is not declared in conditions.` + (conditionKeys.size > 0 ? ` Declared conditions: ${[...conditionKeys].join(", ")}.` : ` No conditions are declared.`)
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
224
266
|
}
|
|
225
267
|
}
|
|
226
268
|
return config;
|
|
@@ -259,17 +301,27 @@ Run \`npx @auth-gate/rbac init\` to create one.`
|
|
|
259
301
|
}
|
|
260
302
|
|
|
261
303
|
// src/diff.ts
|
|
304
|
+
function deepEqual(a, b) {
|
|
305
|
+
if (a === b) return true;
|
|
306
|
+
if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
|
|
307
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
308
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
309
|
+
if (a.length !== b.length) return false;
|
|
310
|
+
return a.every((val, i) => deepEqual(val, b[i]));
|
|
311
|
+
}
|
|
312
|
+
const keysA = Object.keys(a).sort();
|
|
313
|
+
const keysB = Object.keys(b).sort();
|
|
314
|
+
if (keysA.length !== keysB.length) return false;
|
|
315
|
+
return keysA.every(
|
|
316
|
+
(key, i) => key === keysB[i] && deepEqual(a[key], b[key])
|
|
317
|
+
);
|
|
318
|
+
}
|
|
262
319
|
function hashConditionSource(fn) {
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
for (let i = 0; i < src.length; i++) {
|
|
266
|
-
const ch = src.charCodeAt(i);
|
|
267
|
-
hash = (hash << 5) - hash + ch | 0;
|
|
268
|
-
}
|
|
269
|
-
return hash.toString(36);
|
|
320
|
+
const { createHash } = require("crypto");
|
|
321
|
+
return createHash("sha256").update(fn.toString()).digest("hex").slice(0, 16);
|
|
270
322
|
}
|
|
271
323
|
function computeRbacDiff(config, server, memberCounts) {
|
|
272
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
|
|
324
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
|
|
273
325
|
const resourceOps = [];
|
|
274
326
|
const roleOps = [];
|
|
275
327
|
const conditionOps = [];
|
|
@@ -328,7 +380,7 @@ function computeRbacDiff(config, server, memberCounts) {
|
|
|
328
380
|
if (!existing && role.renamedFrom) {
|
|
329
381
|
existing = (_d = serverRoleByKey.get(role.renamedFrom)) != null ? _d : serverRoleByPreviousKey.get(role.renamedFrom);
|
|
330
382
|
if (existing) {
|
|
331
|
-
const members = (_e = memberCounts[existing.
|
|
383
|
+
const members = (_e = memberCounts[existing.configKey]) != null ? _e : 0;
|
|
332
384
|
roleOps.push({
|
|
333
385
|
type: "rename",
|
|
334
386
|
key,
|
|
@@ -352,13 +404,11 @@ function computeRbacDiff(config, server, memberCounts) {
|
|
|
352
404
|
if (((_f = existing.description) != null ? _f : void 0) !== ((_g = role.description) != null ? _g : void 0)) {
|
|
353
405
|
changes.push("description changed");
|
|
354
406
|
}
|
|
355
|
-
|
|
356
|
-
const configGrants = JSON.stringify(role.grants);
|
|
357
|
-
if (existingGrants !== configGrants) {
|
|
407
|
+
if (!deepEqual((_h = existing.grants) != null ? _h : null, (_i = role.grants) != null ? _i : null)) {
|
|
358
408
|
changes.push("grants changed");
|
|
359
409
|
}
|
|
360
|
-
const existingInherits = [...(
|
|
361
|
-
const configInherits = [...(
|
|
410
|
+
const existingInherits = [...(_j = existing.inherits) != null ? _j : []].sort().join(",");
|
|
411
|
+
const configInherits = [...(_k = role.inherits) != null ? _k : []].sort().join(",");
|
|
362
412
|
if (existingInherits !== configInherits) {
|
|
363
413
|
changes.push("inherits changed");
|
|
364
414
|
}
|
|
@@ -371,7 +421,7 @@ function computeRbacDiff(config, server, memberCounts) {
|
|
|
371
421
|
for (const [key, role] of serverRoleByKey) {
|
|
372
422
|
if (renameMap.has(key)) continue;
|
|
373
423
|
if (role.isActive) {
|
|
374
|
-
const members = (
|
|
424
|
+
const members = (_l = memberCounts[role.configKey]) != null ? _l : 0;
|
|
375
425
|
if (members > 0) hasDestructive = true;
|
|
376
426
|
roleOps.push({ type: "archive", key, existing: role, assignedMembers: members });
|
|
377
427
|
}
|
|
@@ -407,6 +457,11 @@ var RbacSyncClient = class {
|
|
|
407
457
|
constructor(config) {
|
|
408
458
|
this.baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
409
459
|
this.apiKey = config.apiKey;
|
|
460
|
+
if (!this.baseUrl.startsWith("https://") && !this.baseUrl.startsWith("http://localhost") && !this.baseUrl.startsWith("http://127.0.0.1")) {
|
|
461
|
+
console.warn(
|
|
462
|
+
"WARNING: AUTHGATE_BASE_URL does not use HTTPS. API key may be transmitted insecurely."
|
|
463
|
+
);
|
|
464
|
+
}
|
|
410
465
|
}
|
|
411
466
|
async request(method, path, body) {
|
|
412
467
|
var _a, _b;
|
|
@@ -425,9 +480,12 @@ var RbacSyncClient = class {
|
|
|
425
480
|
let message;
|
|
426
481
|
try {
|
|
427
482
|
const json = JSON.parse(text);
|
|
428
|
-
message = (_b = (_a = json.error) != null ? _a : json.message) != null ? _b :
|
|
483
|
+
message = (_b = (_a = json.error) != null ? _a : json.message) != null ? _b : "Unknown error";
|
|
429
484
|
} catch (e) {
|
|
430
|
-
message = text;
|
|
485
|
+
message = text.length > 500 ? text.slice(0, 500) + "..." : text;
|
|
486
|
+
}
|
|
487
|
+
if (this.apiKey) {
|
|
488
|
+
message = message.replaceAll(this.apiKey, "[REDACTED]");
|
|
431
489
|
}
|
|
432
490
|
throw new Error(`API error (${res.status}): ${message}`);
|
|
433
491
|
}
|
|
@@ -447,7 +505,10 @@ var RbacSyncClient = class {
|
|
|
447
505
|
resources: config.resources,
|
|
448
506
|
roles: config.roles,
|
|
449
507
|
conditions: config.conditions ? Object.fromEntries(
|
|
450
|
-
Object.entries(config.conditions).map(([k]) => [
|
|
508
|
+
Object.entries(config.conditions).map(([k, fn]) => [
|
|
509
|
+
k,
|
|
510
|
+
{ key: k, sourceHash: hashConditionSource(fn) }
|
|
511
|
+
])
|
|
451
512
|
) : void 0,
|
|
452
513
|
force
|
|
453
514
|
});
|
|
@@ -581,7 +642,10 @@ function formatRbacDiff(diff, dryRun) {
|
|
|
581
642
|
}
|
|
582
643
|
|
|
583
644
|
// src/index.ts
|
|
584
|
-
function defineRbac(config) {
|
|
645
|
+
function defineRbac(config, opts) {
|
|
646
|
+
if ((opts == null ? void 0 : opts.validate) !== false) {
|
|
647
|
+
validateRbacConfig(config);
|
|
648
|
+
}
|
|
585
649
|
const resources = {};
|
|
586
650
|
for (const [key, resource] of Object.entries(config.resources)) {
|
|
587
651
|
const actions = {};
|
package/dist/index.d.cts
CHANGED
|
@@ -245,7 +245,7 @@ interface DiffResult {
|
|
|
245
245
|
conditionOps: ConditionOp[];
|
|
246
246
|
hasDestructive: boolean;
|
|
247
247
|
}
|
|
248
|
-
/**
|
|
248
|
+
/** Hash of a condition function's source code for change detection. */
|
|
249
249
|
declare function hashConditionSource(fn: Function): string;
|
|
250
250
|
declare function computeRbacDiff(config: RbacConfig, server: ServerState, memberCounts: Record<string, number>): DiffResult;
|
|
251
251
|
|
|
@@ -310,6 +310,8 @@ declare function formatRbacDiff(diff: DiffResult, dryRun: boolean): string;
|
|
|
310
310
|
* rbac.permissions.documents.write // "documents:write"
|
|
311
311
|
* ```
|
|
312
312
|
*/
|
|
313
|
-
declare function defineRbac<const T extends RbacConfig>(config: T
|
|
313
|
+
declare function defineRbac<const T extends RbacConfig>(config: T, opts?: {
|
|
314
|
+
validate?: boolean;
|
|
315
|
+
}): TypedRbac<T>;
|
|
314
316
|
|
|
315
317
|
export { type ApplyResult, type ConditionContext, type ConditionFn, type ConditionOp, type DiffResult, type GrantValue, type InferActions, type InferConditionKeys, type InferPermissions, type InferResourceKeys, type InferRoleKeys, type InferScopes, type Permission, type RbacConfig, RbacSyncClient, type RbacSyncClientConfig, type ResourceConfig, type ResourceKey, type ResourceOp, type RoleConfig, type RoleKey, type RoleOp, type ServerCondition, type ServerResource, type ServerRole, type ServerState, type TypedRbac, computeRbacDiff, defineRbac, formatRbacDiff, hashConditionSource, loadRbacConfig, validateRbacConfig };
|
package/dist/index.d.ts
CHANGED
|
@@ -245,7 +245,7 @@ interface DiffResult {
|
|
|
245
245
|
conditionOps: ConditionOp[];
|
|
246
246
|
hasDestructive: boolean;
|
|
247
247
|
}
|
|
248
|
-
/**
|
|
248
|
+
/** Hash of a condition function's source code for change detection. */
|
|
249
249
|
declare function hashConditionSource(fn: Function): string;
|
|
250
250
|
declare function computeRbacDiff(config: RbacConfig, server: ServerState, memberCounts: Record<string, number>): DiffResult;
|
|
251
251
|
|
|
@@ -310,6 +310,8 @@ declare function formatRbacDiff(diff: DiffResult, dryRun: boolean): string;
|
|
|
310
310
|
* rbac.permissions.documents.write // "documents:write"
|
|
311
311
|
* ```
|
|
312
312
|
*/
|
|
313
|
-
declare function defineRbac<const T extends RbacConfig>(config: T
|
|
313
|
+
declare function defineRbac<const T extends RbacConfig>(config: T, opts?: {
|
|
314
|
+
validate?: boolean;
|
|
315
|
+
}): TypedRbac<T>;
|
|
314
316
|
|
|
315
317
|
export { type ApplyResult, type ConditionContext, type ConditionFn, type ConditionOp, type DiffResult, type GrantValue, type InferActions, type InferConditionKeys, type InferPermissions, type InferResourceKeys, type InferRoleKeys, type InferScopes, type Permission, type RbacConfig, RbacSyncClient, type RbacSyncClientConfig, type ResourceConfig, type ResourceKey, type ResourceOp, type RoleConfig, type RoleKey, type RoleOp, type ServerCondition, type ServerResource, type ServerRole, type ServerState, type TypedRbac, computeRbacDiff, defineRbac, formatRbacDiff, hashConditionSource, loadRbacConfig, validateRbacConfig };
|
package/dist/index.mjs
CHANGED
|
@@ -5,10 +5,13 @@ import {
|
|
|
5
5
|
hashConditionSource,
|
|
6
6
|
loadRbacConfig,
|
|
7
7
|
validateRbacConfig
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-5YEMXO7B.mjs";
|
|
9
9
|
|
|
10
10
|
// src/index.ts
|
|
11
|
-
function defineRbac(config) {
|
|
11
|
+
function defineRbac(config, opts) {
|
|
12
|
+
if ((opts == null ? void 0 : opts.validate) !== false) {
|
|
13
|
+
validateRbacConfig(config);
|
|
14
|
+
}
|
|
12
15
|
const resources = {};
|
|
13
16
|
for (const [key, resource] of Object.entries(config.resources)) {
|
|
14
17
|
const actions = {};
|