@auth-gate/rbac 0.8.0
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 +566 -0
- package/dist/cli.cjs +789 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.mjs +217 -0
- package/dist/index.cjs +623 -0
- package/dist/index.d.cts +315 -0
- package/dist/index.d.ts +315 -0
- package/dist/index.mjs +49 -0
- package/package.json +35 -0
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
3
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
4
|
+
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
5
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
6
|
+
var __spreadValues = (a, b) => {
|
|
7
|
+
for (var prop in b || (b = {}))
|
|
8
|
+
if (__hasOwnProp.call(b, prop))
|
|
9
|
+
__defNormalProp(a, prop, b[prop]);
|
|
10
|
+
if (__getOwnPropSymbols)
|
|
11
|
+
for (var prop of __getOwnPropSymbols(b)) {
|
|
12
|
+
if (__propIsEnum.call(b, prop))
|
|
13
|
+
__defNormalProp(a, prop, b[prop]);
|
|
14
|
+
}
|
|
15
|
+
return a;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// src/validator.ts
|
|
19
|
+
var KEY_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
20
|
+
function validateRbacConfig(config) {
|
|
21
|
+
if (!config || typeof config !== "object") {
|
|
22
|
+
throw new Error(
|
|
23
|
+
"RBAC config must be an object. Did you forget `export default defineRbac({ ... })`?"
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
const c = config;
|
|
27
|
+
if (!c.resources || typeof c.resources !== "object" || Array.isArray(c.resources)) {
|
|
28
|
+
throw new Error("RBAC config must have a `resources` object.");
|
|
29
|
+
}
|
|
30
|
+
if (!c.roles || typeof c.roles !== "object" || Array.isArray(c.roles)) {
|
|
31
|
+
throw new Error("RBAC config must have a `roles` object.");
|
|
32
|
+
}
|
|
33
|
+
const resources = c.resources;
|
|
34
|
+
const roles = c.roles;
|
|
35
|
+
const resourceKeys = Object.keys(resources);
|
|
36
|
+
const roleKeys = Object.keys(roles);
|
|
37
|
+
if (resourceKeys.length === 0) {
|
|
38
|
+
throw new Error("RBAC config must define at least one resource.");
|
|
39
|
+
}
|
|
40
|
+
if (roleKeys.length === 0) {
|
|
41
|
+
throw new Error("RBAC config must define at least one role.");
|
|
42
|
+
}
|
|
43
|
+
const resourceActions = /* @__PURE__ */ new Map();
|
|
44
|
+
for (const key of resourceKeys) {
|
|
45
|
+
if (!KEY_PATTERN.test(key)) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
`Resource key "${key}" is invalid. Use lowercase alphanumeric with underscores (e.g., "documents", "billing_plans").`
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
const resource = resources[key];
|
|
51
|
+
if (!resource || typeof resource !== "object") {
|
|
52
|
+
throw new Error(`Resource "${key}" must be an object.`);
|
|
53
|
+
}
|
|
54
|
+
if (!Array.isArray(resource.actions) || resource.actions.length === 0) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
`Resource "${key}" must have a non-empty "actions" array.`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
for (let i = 0; i < resource.actions.length; i++) {
|
|
60
|
+
if (typeof resource.actions[i] !== "string") {
|
|
61
|
+
throw new Error(
|
|
62
|
+
`Resource "${key}".actions[${i}] must be a string.`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
resourceActions.set(key, new Set(resource.actions));
|
|
67
|
+
if (resource.scopes !== void 0) {
|
|
68
|
+
if (!Array.isArray(resource.scopes)) {
|
|
69
|
+
throw new Error(`Resource "${key}".scopes must be an array of strings.`);
|
|
70
|
+
}
|
|
71
|
+
for (let i = 0; i < resource.scopes.length; i++) {
|
|
72
|
+
if (typeof resource.scopes[i] !== "string") {
|
|
73
|
+
throw new Error(`Resource "${key}".scopes[${i}] must be a string.`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
for (const key of roleKeys) {
|
|
79
|
+
if (!KEY_PATTERN.test(key)) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`Role key "${key}" is invalid. Use lowercase alphanumeric with underscores (e.g., "admin", "org_owner").`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
const role = roles[key];
|
|
85
|
+
if (!role || typeof role !== "object") {
|
|
86
|
+
throw new Error(`Role "${key}" must be an object.`);
|
|
87
|
+
}
|
|
88
|
+
if (!role.name || typeof role.name !== "string") {
|
|
89
|
+
throw new Error(`Role "${key}" must have a "name" string.`);
|
|
90
|
+
}
|
|
91
|
+
if (!role.grants || typeof role.grants !== "object" || Array.isArray(role.grants)) {
|
|
92
|
+
throw new Error(`Role "${key}" must have a "grants" object.`);
|
|
93
|
+
}
|
|
94
|
+
const grants = role.grants;
|
|
95
|
+
for (const resourceKey of Object.keys(grants)) {
|
|
96
|
+
if (!resourceActions.has(resourceKey)) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
`Role "${key}".grants references undeclared resource "${resourceKey}". Declared resources: ${[...resourceActions.keys()].join(", ")}.`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
const actionsGrant = grants[resourceKey];
|
|
102
|
+
if (!actionsGrant || typeof actionsGrant !== "object" || Array.isArray(actionsGrant)) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`Role "${key}".grants.${resourceKey} must be an object mapping actions to grant values.`
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
const declaredActions = resourceActions.get(resourceKey);
|
|
108
|
+
for (const actionKey of Object.keys(actionsGrant)) {
|
|
109
|
+
if (!declaredActions.has(actionKey)) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
`Role "${key}".grants.${resourceKey} references undeclared action "${actionKey}". Declared actions for "${resourceKey}": ${[...declaredActions].join(", ")}.`
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
const value = actionsGrant[actionKey];
|
|
115
|
+
if (value !== true && typeof value !== "string" && !(value && typeof value === "object" && "when" in value && typeof value.when === "string")) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
`Role "${key}".grants.${resourceKey}.${actionKey} has invalid value. Expected true, a scope string, or { when: string }.`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
for (const key of roleKeys) {
|
|
124
|
+
const role = roles[key];
|
|
125
|
+
if (role.inherits !== void 0) {
|
|
126
|
+
if (!Array.isArray(role.inherits)) {
|
|
127
|
+
throw new Error(`Role "${key}".inherits must be an array of role key strings.`);
|
|
128
|
+
}
|
|
129
|
+
for (const parent of role.inherits) {
|
|
130
|
+
if (typeof parent !== "string") {
|
|
131
|
+
throw new Error(`Role "${key}".inherits contains a non-string value.`);
|
|
132
|
+
}
|
|
133
|
+
if (!roleKeys.includes(parent)) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
`Role "${key}".inherits references unknown role "${parent}". Declared roles: ${roleKeys.join(", ")}.`
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function detectCycle(roleKey) {
|
|
142
|
+
const visited = /* @__PURE__ */ new Set();
|
|
143
|
+
function dfs(current, path) {
|
|
144
|
+
if (visited.has(current)) {
|
|
145
|
+
const cycleStart = path.indexOf(current);
|
|
146
|
+
const cycle = [...path.slice(cycleStart), current].join(" -> ");
|
|
147
|
+
throw new Error(
|
|
148
|
+
`Circular inheritance detected: ${cycle}. A role cannot directly or indirectly inherit from itself.`
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
visited.add(current);
|
|
152
|
+
const role = roles[current];
|
|
153
|
+
if ((role == null ? void 0 : role.inherits) && Array.isArray(role.inherits)) {
|
|
154
|
+
for (const parent of role.inherits) {
|
|
155
|
+
dfs(parent, [...path, current]);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
visited.delete(current);
|
|
159
|
+
}
|
|
160
|
+
dfs(roleKey, []);
|
|
161
|
+
}
|
|
162
|
+
for (const key of roleKeys) {
|
|
163
|
+
detectCycle(key);
|
|
164
|
+
}
|
|
165
|
+
const renameTargets = /* @__PURE__ */ new Map();
|
|
166
|
+
for (const key of roleKeys) {
|
|
167
|
+
const role = roles[key];
|
|
168
|
+
if (role.renamedFrom !== void 0) {
|
|
169
|
+
if (typeof role.renamedFrom !== "string") {
|
|
170
|
+
throw new Error(`Role "${key}".renamedFrom must be a string.`);
|
|
171
|
+
}
|
|
172
|
+
if (role.renamedFrom === key) {
|
|
173
|
+
throw new Error(`Role "${key}".renamedFrom cannot equal its own key.`);
|
|
174
|
+
}
|
|
175
|
+
if (roleKeys.includes(role.renamedFrom)) {
|
|
176
|
+
throw new Error(
|
|
177
|
+
`Role "${key}".renamedFrom "${role.renamedFrom}" cannot reference an existing role key.`
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
if (renameTargets.has(role.renamedFrom)) {
|
|
181
|
+
throw new Error(
|
|
182
|
+
`renamedFrom "${role.renamedFrom}" is claimed by multiple roles: "${renameTargets.get(role.renamedFrom)}" and "${key}".`
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
renameTargets.set(role.renamedFrom, key);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (c.conditions !== void 0) {
|
|
189
|
+
if (typeof c.conditions !== "object" || Array.isArray(c.conditions) || c.conditions === null) {
|
|
190
|
+
throw new Error("RBAC config `conditions` must be an object mapping names to functions.");
|
|
191
|
+
}
|
|
192
|
+
const conditions = c.conditions;
|
|
193
|
+
for (const [condKey, condValue] of Object.entries(conditions)) {
|
|
194
|
+
if (typeof condValue !== "function") {
|
|
195
|
+
throw new Error(
|
|
196
|
+
`Condition "${condKey}" must be a function. Got ${typeof condValue}.`
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return config;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// src/config-loader.ts
|
|
205
|
+
import { resolve } from "path";
|
|
206
|
+
import { existsSync } from "fs";
|
|
207
|
+
var CONFIG_FILENAMES = [
|
|
208
|
+
"authgate.rbac.ts",
|
|
209
|
+
"authgate.rbac.js",
|
|
210
|
+
"authgate.rbac.mjs"
|
|
211
|
+
];
|
|
212
|
+
async function loadRbacConfig(cwd) {
|
|
213
|
+
var _a, _b;
|
|
214
|
+
let configPath = null;
|
|
215
|
+
for (const filename of CONFIG_FILENAMES) {
|
|
216
|
+
const candidate = resolve(cwd, filename);
|
|
217
|
+
if (existsSync(candidate)) {
|
|
218
|
+
configPath = candidate;
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (!configPath) {
|
|
223
|
+
throw new Error(
|
|
224
|
+
`No RBAC config found. Expected one of: ${CONFIG_FILENAMES.join(", ")}
|
|
225
|
+
Run \`npx @auth-gate/rbac init\` to create one.`
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
const { createJiti } = await import("jiti");
|
|
229
|
+
const jiti = createJiti(cwd, { interopDefault: true });
|
|
230
|
+
const mod = await jiti.import(configPath);
|
|
231
|
+
const raw = (_b = (_a = mod.default) != null ? _a : mod.rbac) != null ? _b : mod;
|
|
232
|
+
const config = raw && typeof raw === "object" && "_config" in raw ? raw._config : raw;
|
|
233
|
+
return validateRbacConfig(config);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// src/diff.ts
|
|
237
|
+
function hashConditionSource(fn) {
|
|
238
|
+
const src = fn.toString();
|
|
239
|
+
let hash = 0;
|
|
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);
|
|
245
|
+
}
|
|
246
|
+
function computeRbacDiff(config, server, memberCounts) {
|
|
247
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
|
|
248
|
+
const resourceOps = [];
|
|
249
|
+
const roleOps = [];
|
|
250
|
+
const conditionOps = [];
|
|
251
|
+
let hasDestructive = false;
|
|
252
|
+
const serverResourceByKey = /* @__PURE__ */ new Map();
|
|
253
|
+
for (const res of server.resources) {
|
|
254
|
+
if (res.managedBy === "config") {
|
|
255
|
+
serverResourceByKey.set(res.key, res);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
for (const [key, resource] of Object.entries(config.resources)) {
|
|
259
|
+
const existing = serverResourceByKey.get(key);
|
|
260
|
+
if (!existing) {
|
|
261
|
+
resourceOps.push({ type: "create", key, resource });
|
|
262
|
+
} else {
|
|
263
|
+
const changes = [];
|
|
264
|
+
const existingActions = [...existing.actions].sort().join(",");
|
|
265
|
+
const configActions = [...resource.actions].sort().join(",");
|
|
266
|
+
if (existingActions !== configActions) {
|
|
267
|
+
changes.push(`actions: [${existing.actions.join(", ")}] -> [${[...resource.actions].join(", ")}]`);
|
|
268
|
+
}
|
|
269
|
+
const existingScopes = [...(_a = existing.scopes) != null ? _a : []].sort().join(",");
|
|
270
|
+
const configScopes = [...(_b = resource.scopes) != null ? _b : []].sort().join(",");
|
|
271
|
+
if (existingScopes !== configScopes) {
|
|
272
|
+
changes.push("scopes changed");
|
|
273
|
+
}
|
|
274
|
+
if (changes.length > 0) {
|
|
275
|
+
resourceOps.push({ type: "update", key, resource, existing, changes });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
serverResourceByKey.delete(key);
|
|
279
|
+
}
|
|
280
|
+
for (const [key, resource] of serverResourceByKey) {
|
|
281
|
+
if (resource.isActive) {
|
|
282
|
+
resourceOps.push({ type: "archive", key, existing: resource });
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
const serverRoleByKey = /* @__PURE__ */ new Map();
|
|
286
|
+
const serverRoleByPreviousKey = /* @__PURE__ */ new Map();
|
|
287
|
+
for (const role of server.roles) {
|
|
288
|
+
if (role.managedBy === "config") {
|
|
289
|
+
serverRoleByKey.set(role.configKey, role);
|
|
290
|
+
}
|
|
291
|
+
for (const prevKey of (_c = role.previousConfigKeys) != null ? _c : []) {
|
|
292
|
+
serverRoleByPreviousKey.set(prevKey, role);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
const renameMap = /* @__PURE__ */ new Map();
|
|
296
|
+
for (const [key, role] of Object.entries(config.roles)) {
|
|
297
|
+
if (role.renamedFrom) {
|
|
298
|
+
renameMap.set(role.renamedFrom, key);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
for (const [key, role] of Object.entries(config.roles)) {
|
|
302
|
+
let existing = serverRoleByKey.get(key);
|
|
303
|
+
if (!existing && role.renamedFrom) {
|
|
304
|
+
existing = (_d = serverRoleByKey.get(role.renamedFrom)) != null ? _d : serverRoleByPreviousKey.get(role.renamedFrom);
|
|
305
|
+
if (existing) {
|
|
306
|
+
const members = (_e = memberCounts[existing.id]) != null ? _e : 0;
|
|
307
|
+
roleOps.push({
|
|
308
|
+
type: "rename",
|
|
309
|
+
key,
|
|
310
|
+
oldKey: role.renamedFrom,
|
|
311
|
+
role,
|
|
312
|
+
existing,
|
|
313
|
+
assignedMembers: members
|
|
314
|
+
});
|
|
315
|
+
serverRoleByKey.delete(existing.configKey);
|
|
316
|
+
serverRoleByKey.delete(key);
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
if (!existing) {
|
|
321
|
+
roleOps.push({ type: "create", key, role });
|
|
322
|
+
} else {
|
|
323
|
+
const changes = [];
|
|
324
|
+
if (existing.name !== role.name) {
|
|
325
|
+
changes.push(`name: "${existing.name}" -> "${role.name}"`);
|
|
326
|
+
}
|
|
327
|
+
if (((_f = existing.description) != null ? _f : void 0) !== ((_g = role.description) != null ? _g : void 0)) {
|
|
328
|
+
changes.push("description changed");
|
|
329
|
+
}
|
|
330
|
+
const existingGrants = JSON.stringify((_h = existing.grants) != null ? _h : null);
|
|
331
|
+
const configGrants = JSON.stringify(role.grants);
|
|
332
|
+
if (existingGrants !== configGrants) {
|
|
333
|
+
changes.push("grants changed");
|
|
334
|
+
}
|
|
335
|
+
const existingInherits = [...(_i = existing.inherits) != null ? _i : []].sort().join(",");
|
|
336
|
+
const configInherits = [...(_j = role.inherits) != null ? _j : []].sort().join(",");
|
|
337
|
+
if (existingInherits !== configInherits) {
|
|
338
|
+
changes.push("inherits changed");
|
|
339
|
+
}
|
|
340
|
+
if (changes.length > 0) {
|
|
341
|
+
roleOps.push({ type: "update", key, role, existing, changes });
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
serverRoleByKey.delete(key);
|
|
345
|
+
}
|
|
346
|
+
for (const [key, role] of serverRoleByKey) {
|
|
347
|
+
if (renameMap.has(key)) continue;
|
|
348
|
+
if (role.isActive) {
|
|
349
|
+
const members = (_k = memberCounts[role.id]) != null ? _k : 0;
|
|
350
|
+
if (members > 0) hasDestructive = true;
|
|
351
|
+
roleOps.push({ type: "archive", key, existing: role, assignedMembers: members });
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
const serverConditionByKey = /* @__PURE__ */ new Map();
|
|
355
|
+
for (const cond of server.conditions) {
|
|
356
|
+
serverConditionByKey.set(cond.key, cond);
|
|
357
|
+
}
|
|
358
|
+
if (config.conditions) {
|
|
359
|
+
for (const [key, condFn] of Object.entries(config.conditions)) {
|
|
360
|
+
const existing = serverConditionByKey.get(key);
|
|
361
|
+
const newHash = hashConditionSource(condFn);
|
|
362
|
+
if (!existing) {
|
|
363
|
+
conditionOps.push({ type: "create", key });
|
|
364
|
+
} else {
|
|
365
|
+
if (existing.sourceHash !== newHash) {
|
|
366
|
+
conditionOps.push({ type: "update", key, changes: ["source changed"] });
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
serverConditionByKey.delete(key);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
for (const [key, cond] of serverConditionByKey) {
|
|
373
|
+
if (cond.isActive) {
|
|
374
|
+
conditionOps.push({ type: "archive", key, existing: cond });
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return { resourceOps, roleOps, conditionOps, hasDestructive };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// src/formatter.ts
|
|
381
|
+
import chalk from "chalk";
|
|
382
|
+
function formatRbacDiff(diff, dryRun) {
|
|
383
|
+
const lines = [];
|
|
384
|
+
if (dryRun) {
|
|
385
|
+
lines.push(
|
|
386
|
+
chalk.bold("AuthGate RBAC Sync \u2014 DRY RUN") + chalk.dim(" (use --apply to execute)")
|
|
387
|
+
);
|
|
388
|
+
} else {
|
|
389
|
+
lines.push(chalk.bold("AuthGate RBAC Sync \u2014 APPLYING CHANGES"));
|
|
390
|
+
}
|
|
391
|
+
lines.push("");
|
|
392
|
+
const totalOps = diff.resourceOps.length + diff.roleOps.length + diff.conditionOps.length;
|
|
393
|
+
if (totalOps === 0) {
|
|
394
|
+
lines.push(chalk.green(" Everything is in sync. No changes needed."));
|
|
395
|
+
return lines.join("\n");
|
|
396
|
+
}
|
|
397
|
+
if (diff.resourceOps.length > 0) {
|
|
398
|
+
lines.push(chalk.bold.underline(" Resources"));
|
|
399
|
+
lines.push("");
|
|
400
|
+
for (const op of diff.resourceOps) {
|
|
401
|
+
if (op.type === "create") {
|
|
402
|
+
lines.push(chalk.green(` + CREATE resource "${op.key}"`));
|
|
403
|
+
lines.push(
|
|
404
|
+
chalk.dim(` actions: [${[...op.resource.actions].join(", ")}]`)
|
|
405
|
+
);
|
|
406
|
+
if (op.resource.scopes && op.resource.scopes.length > 0) {
|
|
407
|
+
lines.push(
|
|
408
|
+
chalk.dim(` scopes: [${[...op.resource.scopes].join(", ")}]`)
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
} else if (op.type === "update") {
|
|
412
|
+
lines.push(chalk.yellow(` ~ UPDATE resource "${op.key}"`));
|
|
413
|
+
for (const change of op.changes) {
|
|
414
|
+
lines.push(chalk.yellow(` ${change}`));
|
|
415
|
+
}
|
|
416
|
+
} else if (op.type === "archive") {
|
|
417
|
+
lines.push(chalk.red(` - ARCHIVE resource "${op.key}"`));
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
lines.push("");
|
|
421
|
+
}
|
|
422
|
+
if (diff.roleOps.length > 0) {
|
|
423
|
+
lines.push(chalk.bold.underline(" Roles"));
|
|
424
|
+
lines.push("");
|
|
425
|
+
for (const op of diff.roleOps) {
|
|
426
|
+
if (op.type === "create") {
|
|
427
|
+
lines.push(chalk.green(` + CREATE role "${op.key}"`));
|
|
428
|
+
if (op.role.description) {
|
|
429
|
+
lines.push(chalk.dim(` ${op.role.description}`));
|
|
430
|
+
}
|
|
431
|
+
if (op.role.inherits && op.role.inherits.length > 0) {
|
|
432
|
+
lines.push(
|
|
433
|
+
chalk.dim(` inherits: [${[...op.role.inherits].join(", ")}]`)
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
} else if (op.type === "update") {
|
|
437
|
+
lines.push(chalk.yellow(` ~ UPDATE role "${op.key}"`));
|
|
438
|
+
for (const change of op.changes) {
|
|
439
|
+
lines.push(chalk.yellow(` ${change}`));
|
|
440
|
+
}
|
|
441
|
+
} else if (op.type === "archive") {
|
|
442
|
+
lines.push(chalk.red(` - ARCHIVE role "${op.key}"`));
|
|
443
|
+
if (op.assignedMembers > 0) {
|
|
444
|
+
lines.push(
|
|
445
|
+
chalk.red.bold(
|
|
446
|
+
` WARNING: ${op.assignedMembers} assigned member${op.assignedMembers > 1 ? "s" : ""} will lose this role`
|
|
447
|
+
)
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
} else if (op.type === "rename") {
|
|
451
|
+
lines.push(
|
|
452
|
+
chalk.cyan(` ~ RENAME role "${op.oldKey}" -> "${op.key}"`)
|
|
453
|
+
);
|
|
454
|
+
if (op.assignedMembers > 0) {
|
|
455
|
+
lines.push(
|
|
456
|
+
chalk.dim(
|
|
457
|
+
` ${op.assignedMembers} member${op.assignedMembers > 1 ? "s" : ""} preserved`
|
|
458
|
+
)
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
lines.push("");
|
|
464
|
+
}
|
|
465
|
+
if (diff.conditionOps.length > 0) {
|
|
466
|
+
lines.push(chalk.bold.underline(" Conditions"));
|
|
467
|
+
lines.push("");
|
|
468
|
+
for (const op of diff.conditionOps) {
|
|
469
|
+
if (op.type === "create") {
|
|
470
|
+
lines.push(chalk.green(` + CREATE condition "${op.key}"`));
|
|
471
|
+
} else if (op.type === "update") {
|
|
472
|
+
lines.push(chalk.yellow(` ~ UPDATE condition "${op.key}"`));
|
|
473
|
+
for (const change of op.changes) {
|
|
474
|
+
lines.push(chalk.yellow(` ${change}`));
|
|
475
|
+
}
|
|
476
|
+
} else if (op.type === "archive") {
|
|
477
|
+
lines.push(chalk.red(` - ARCHIVE condition "${op.key}"`));
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
lines.push("");
|
|
481
|
+
}
|
|
482
|
+
const allOps = [
|
|
483
|
+
...diff.resourceOps.map((o) => o.type),
|
|
484
|
+
...diff.roleOps.map((o) => o.type),
|
|
485
|
+
...diff.conditionOps.map((o) => o.type)
|
|
486
|
+
];
|
|
487
|
+
const creates = allOps.filter((t) => t === "create").length;
|
|
488
|
+
const updates = allOps.filter((t) => t === "update").length;
|
|
489
|
+
const renames = allOps.filter((t) => t === "rename").length;
|
|
490
|
+
const archives = allOps.filter((t) => t === "archive").length;
|
|
491
|
+
lines.push(
|
|
492
|
+
chalk.dim(
|
|
493
|
+
` Summary: ${creates} create, ${updates} update, ${renames} rename, ${archives} archive.`
|
|
494
|
+
) + (dryRun ? chalk.dim(" Run with --apply to execute.") : "")
|
|
495
|
+
);
|
|
496
|
+
if (diff.hasDestructive && dryRun) {
|
|
497
|
+
lines.push(
|
|
498
|
+
chalk.red.bold(
|
|
499
|
+
"\n WARNING: Destructive changes detected (roles with assigned members)."
|
|
500
|
+
) + chalk.red(" Use --apply --force to proceed.")
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
return lines.join("\n");
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// src/sync.ts
|
|
507
|
+
var RbacSyncClient = class {
|
|
508
|
+
constructor(config) {
|
|
509
|
+
this.baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
510
|
+
this.apiKey = config.apiKey;
|
|
511
|
+
}
|
|
512
|
+
async request(method, path, body) {
|
|
513
|
+
var _a, _b;
|
|
514
|
+
const url = `${this.baseUrl}${path}`;
|
|
515
|
+
const headers = {
|
|
516
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
517
|
+
"Content-Type": "application/json"
|
|
518
|
+
};
|
|
519
|
+
const res = await fetch(url, {
|
|
520
|
+
method,
|
|
521
|
+
headers,
|
|
522
|
+
body: body ? JSON.stringify(body) : void 0
|
|
523
|
+
});
|
|
524
|
+
if (!res.ok) {
|
|
525
|
+
const text = await res.text();
|
|
526
|
+
let message;
|
|
527
|
+
try {
|
|
528
|
+
const json = JSON.parse(text);
|
|
529
|
+
message = (_b = (_a = json.error) != null ? _a : json.message) != null ? _b : text;
|
|
530
|
+
} catch (e) {
|
|
531
|
+
message = text;
|
|
532
|
+
}
|
|
533
|
+
throw new Error(`API error (${res.status}): ${message}`);
|
|
534
|
+
}
|
|
535
|
+
return res.json();
|
|
536
|
+
}
|
|
537
|
+
async getState() {
|
|
538
|
+
return this.request("GET", "/api/v1/rbac/sync/state");
|
|
539
|
+
}
|
|
540
|
+
async getMemberCounts() {
|
|
541
|
+
return this.request(
|
|
542
|
+
"GET",
|
|
543
|
+
"/api/v1/rbac/sync/member-counts"
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
async apply(config, force) {
|
|
547
|
+
return this.request("POST", "/api/v1/rbac/sync/apply", {
|
|
548
|
+
resources: config.resources,
|
|
549
|
+
roles: config.roles,
|
|
550
|
+
conditions: config.conditions ? Object.fromEntries(
|
|
551
|
+
Object.entries(config.conditions).map(([k]) => [k, { key: k }])
|
|
552
|
+
) : void 0,
|
|
553
|
+
force
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
export {
|
|
559
|
+
__spreadValues,
|
|
560
|
+
validateRbacConfig,
|
|
561
|
+
loadRbacConfig,
|
|
562
|
+
hashConditionSource,
|
|
563
|
+
computeRbacDiff,
|
|
564
|
+
formatRbacDiff,
|
|
565
|
+
RbacSyncClient
|
|
566
|
+
};
|