snipe-auth-rbac 0.4.1 → 0.6.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/admin/index.cjs +162 -78
- package/dist/admin/index.cjs.map +1 -1
- package/dist/admin/index.d.cts +106 -18
- package/dist/admin/index.d.ts +106 -18
- package/dist/admin/index.js +161 -78
- package/dist/admin/index.js.map +1 -1
- package/package.json +1 -1
- package/sql/0001_initial.sql +354 -57
package/dist/admin/index.cjs
CHANGED
|
@@ -37,6 +37,7 @@ __export(admin_exports, {
|
|
|
37
37
|
useDeleteRole: () => useDeleteRole,
|
|
38
38
|
useInviteCompanyMember: () => useInviteCompanyMember,
|
|
39
39
|
useRolePermissionGrid: () => useRolePermissionGrid,
|
|
40
|
+
useRolePermissionOverrides: () => useRolePermissionOverrides,
|
|
40
41
|
useSetRolePermissionCell: () => useSetRolePermissionCell,
|
|
41
42
|
useUpdateRole: () => useUpdateRole
|
|
42
43
|
});
|
|
@@ -225,6 +226,32 @@ function createSupabaseAdminClient(opts) {
|
|
|
225
226
|
}
|
|
226
227
|
return data ?? [];
|
|
227
228
|
},
|
|
229
|
+
async listRolePermissionOverrides(roleId) {
|
|
230
|
+
const { data, error } = await rbac.from("role_permission_overrides").select("role_id, resource, action").eq("role_id", roleId);
|
|
231
|
+
if (error) {
|
|
232
|
+
if (/role_permission_overrides/i.test(error.message) && /does not exist/i.test(error.message)) {
|
|
233
|
+
return [];
|
|
234
|
+
}
|
|
235
|
+
throw new Error(`listRolePermissionOverrides: ${error.message}`);
|
|
236
|
+
}
|
|
237
|
+
return data ?? [];
|
|
238
|
+
},
|
|
239
|
+
async setRolePermissionOverride({ role_id, resource, action, suppress }) {
|
|
240
|
+
if (suppress) {
|
|
241
|
+
const { error: error2 } = await rbac.from("role_permission_overrides").upsert(
|
|
242
|
+
{ role_id, resource, action },
|
|
243
|
+
{ onConflict: "role_id,resource,action" }
|
|
244
|
+
);
|
|
245
|
+
if (error2) {
|
|
246
|
+
throw new Error(`setRolePermissionOverride(insert): ${error2.message}`);
|
|
247
|
+
}
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const { error } = await rbac.from("role_permission_overrides").delete().eq("role_id", role_id).eq("resource", resource).eq("action", action);
|
|
251
|
+
if (error) {
|
|
252
|
+
throw new Error(`setRolePermissionOverride(delete): ${error.message}`);
|
|
253
|
+
}
|
|
254
|
+
},
|
|
228
255
|
async applyTemplateDefaults({ role_id, only_missing = true }) {
|
|
229
256
|
const { data, error } = await rbac.rpc("apply_template_defaults", {
|
|
230
257
|
p_role_id: role_id,
|
|
@@ -408,6 +435,57 @@ function useAdminResourceDependencies() {
|
|
|
408
435
|
[transport]
|
|
409
436
|
);
|
|
410
437
|
}
|
|
438
|
+
function useRolePermissionOverrides(roleId) {
|
|
439
|
+
const transport = useAdminTransport();
|
|
440
|
+
const [overrides, setOverrides] = (0, import_react.useState)(() => /* @__PURE__ */ new Set());
|
|
441
|
+
const [error, setError] = (0, import_react.useState)(null);
|
|
442
|
+
const fetchOverrides = (0, import_react.useCallback)(async () => {
|
|
443
|
+
if (!roleId) {
|
|
444
|
+
setOverrides(/* @__PURE__ */ new Set());
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
try {
|
|
448
|
+
const rows = await transport.listRolePermissionOverrides(roleId);
|
|
449
|
+
setOverrides(new Set(rows.map((r) => `${r.resource}:${r.action}`)));
|
|
450
|
+
setError(null);
|
|
451
|
+
} catch (e) {
|
|
452
|
+
setError(e instanceof Error ? e : new Error(String(e)));
|
|
453
|
+
}
|
|
454
|
+
}, [transport, roleId]);
|
|
455
|
+
(0, import_react.useEffect)(() => {
|
|
456
|
+
void fetchOverrides();
|
|
457
|
+
}, [fetchOverrides]);
|
|
458
|
+
const setOverride = (0, import_react.useCallback)(
|
|
459
|
+
async (resource, action, suppress) => {
|
|
460
|
+
if (!roleId) {
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
const key = `${resource}:${action}`;
|
|
464
|
+
setOverrides((prev) => {
|
|
465
|
+
const next = new Set(prev);
|
|
466
|
+
if (suppress) {
|
|
467
|
+
next.add(key);
|
|
468
|
+
} else {
|
|
469
|
+
next.delete(key);
|
|
470
|
+
}
|
|
471
|
+
return next;
|
|
472
|
+
});
|
|
473
|
+
try {
|
|
474
|
+
await transport.setRolePermissionOverride({
|
|
475
|
+
role_id: roleId,
|
|
476
|
+
resource,
|
|
477
|
+
action,
|
|
478
|
+
suppress
|
|
479
|
+
});
|
|
480
|
+
} catch (e) {
|
|
481
|
+
setError(e instanceof Error ? e : new Error(String(e)));
|
|
482
|
+
}
|
|
483
|
+
void fetchOverrides();
|
|
484
|
+
},
|
|
485
|
+
[transport, roleId, fetchOverrides]
|
|
486
|
+
);
|
|
487
|
+
return { overrides, setOverride, error, refresh: fetchOverrides };
|
|
488
|
+
}
|
|
411
489
|
function useCreateCompany() {
|
|
412
490
|
const transport = useAdminTransport();
|
|
413
491
|
return useMutation(transport.createCompany);
|
|
@@ -419,10 +497,9 @@ function useInviteCompanyMember() {
|
|
|
419
497
|
function useRolePermissionGrid(roleId) {
|
|
420
498
|
const { data, isLoading, error, refresh } = useAdminRolePermissions(roleId);
|
|
421
499
|
const dependencies = useAdminResourceDependencies();
|
|
500
|
+
const overridesHook = useRolePermissionOverrides(roleId);
|
|
422
501
|
const setCell = useSetRolePermissionCell();
|
|
423
502
|
const transport = useAdminTransport();
|
|
424
|
-
const [isCascading, setCascading] = (0, import_react.useState)(false);
|
|
425
|
-
const [cascadeError, setCascadeError] = (0, import_react.useState)(null);
|
|
426
503
|
const grid = (0, import_react.useMemo)(() => {
|
|
427
504
|
const out = {};
|
|
428
505
|
for (const row of data ?? []) {
|
|
@@ -435,100 +512,90 @@ function useRolePermissionGrid(roleId) {
|
|
|
435
512
|
}
|
|
436
513
|
return out;
|
|
437
514
|
}, [data]);
|
|
438
|
-
const
|
|
439
|
-
const out = {};
|
|
440
|
-
for (const row of data ?? []) {
|
|
441
|
-
out[row.resource] = {
|
|
442
|
-
read: row.read_granted_via ?? null,
|
|
443
|
-
write: row.write_granted_via ?? null,
|
|
444
|
-
update: row.update_granted_via ?? null,
|
|
445
|
-
delete: row.delete_granted_via ?? null
|
|
446
|
-
};
|
|
447
|
-
}
|
|
448
|
-
return out;
|
|
449
|
-
}, [data]);
|
|
450
|
-
const edgesByParent = (0, import_react.useMemo)(() => {
|
|
515
|
+
const parentsByChild = (0, import_react.useMemo)(() => {
|
|
451
516
|
const map = /* @__PURE__ */ new Map();
|
|
452
517
|
for (const edge of dependencies.data ?? []) {
|
|
453
|
-
const list = map.get(edge.
|
|
454
|
-
map.set(edge.
|
|
518
|
+
const list = map.get(edge.child_resource) ?? [];
|
|
519
|
+
map.set(edge.child_resource, [
|
|
455
520
|
...list,
|
|
456
|
-
{
|
|
521
|
+
{ parent: edge.parent_resource, action: edge.action }
|
|
457
522
|
]);
|
|
458
523
|
}
|
|
459
524
|
return map;
|
|
460
525
|
}, [dependencies.data]);
|
|
526
|
+
const originGrid = (0, import_react.useMemo)(() => {
|
|
527
|
+
const out = {};
|
|
528
|
+
const resources = new Set(Object.keys(grid));
|
|
529
|
+
for (const child of parentsByChild.keys()) {
|
|
530
|
+
resources.add(child);
|
|
531
|
+
}
|
|
532
|
+
const overrides = overridesHook.overrides;
|
|
533
|
+
for (const resource of resources) {
|
|
534
|
+
const directs = grid[resource];
|
|
535
|
+
const cellOrigins = {
|
|
536
|
+
read: null,
|
|
537
|
+
write: null,
|
|
538
|
+
update: null,
|
|
539
|
+
delete: null
|
|
540
|
+
};
|
|
541
|
+
for (const action of ACTIONS) {
|
|
542
|
+
if (overrides.has(`${resource}:${action}`)) {
|
|
543
|
+
cellOrigins[action] = "override";
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
if (directs?.[action]) {
|
|
547
|
+
cellOrigins[action] = "direct";
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
const parents = parentsByChild.get(resource) ?? [];
|
|
551
|
+
const impliedFrom = parents.find(
|
|
552
|
+
(p) => p.action === action && grid[p.parent]?.[action] === true
|
|
553
|
+
);
|
|
554
|
+
cellOrigins[action] = impliedFrom ? impliedFrom.parent : null;
|
|
555
|
+
}
|
|
556
|
+
out[resource] = cellOrigins;
|
|
557
|
+
}
|
|
558
|
+
return out;
|
|
559
|
+
}, [grid, parentsByChild, overridesHook.overrides]);
|
|
461
560
|
const updateCell = (0, import_react.useCallback)(
|
|
462
561
|
async (resource, action, value) => {
|
|
463
562
|
if (!roleId) {
|
|
464
563
|
return;
|
|
465
564
|
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
}
|
|
475
|
-
const childRow = (data ?? []).find((r) => r.resource === edge.child);
|
|
476
|
-
const childValue = childRow?.[ACTION_FIELD[action]] === true;
|
|
477
|
-
const childOrigin = childRow?.[ORIGIN_FIELD[action]] ?? null;
|
|
478
|
-
if (childValue && childOrigin == null) {
|
|
479
|
-
continue;
|
|
480
|
-
}
|
|
481
|
-
writes.push({
|
|
482
|
-
role_id: roleId,
|
|
483
|
-
resource: edge.child,
|
|
484
|
-
action,
|
|
485
|
-
value: true,
|
|
486
|
-
grantedVia: resource
|
|
487
|
-
});
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
setCascading(true);
|
|
491
|
-
setCascadeError(null);
|
|
492
|
-
try {
|
|
493
|
-
const [first, ...rest] = writes;
|
|
494
|
-
if (first && rest.length === 0) {
|
|
495
|
-
await setCell.mutate(first);
|
|
496
|
-
} else {
|
|
497
|
-
await transport.batchSetRolePermissionCells(writes);
|
|
498
|
-
}
|
|
499
|
-
void refresh();
|
|
500
|
-
} catch (e) {
|
|
501
|
-
setCascadeError(e instanceof Error ? e : new Error(String(e)));
|
|
502
|
-
throw e;
|
|
503
|
-
} finally {
|
|
504
|
-
setCascading(false);
|
|
505
|
-
}
|
|
565
|
+
await setCell.mutate({
|
|
566
|
+
role_id: roleId,
|
|
567
|
+
resource,
|
|
568
|
+
action,
|
|
569
|
+
value,
|
|
570
|
+
grantedVia: null
|
|
571
|
+
});
|
|
572
|
+
void refresh();
|
|
506
573
|
},
|
|
507
|
-
[roleId, setCell, refresh
|
|
574
|
+
[roleId, setCell, refresh]
|
|
508
575
|
);
|
|
576
|
+
void transport;
|
|
509
577
|
return {
|
|
510
578
|
grid,
|
|
511
579
|
originGrid,
|
|
580
|
+
parentsByChild,
|
|
581
|
+
/** 0.6.0+. Set of `"<resource>:<action>"` for this role's overrides. */
|
|
582
|
+
overrides: overridesHook.overrides,
|
|
583
|
+
/**
|
|
584
|
+
* 0.6.0+. Suppress (`suppress=true`) or restore (`suppress=false`)
|
|
585
|
+
* an implied permission for this role. The grid + originGrid
|
|
586
|
+
* re-render with `'override'` state on the cell as soon as the
|
|
587
|
+
* optimistic flip lands.
|
|
588
|
+
*/
|
|
589
|
+
setOverride: overridesHook.setOverride,
|
|
512
590
|
isLoading: isLoading || dependencies.isLoading,
|
|
513
|
-
error: error ?? dependencies.error,
|
|
591
|
+
error: error ?? dependencies.error ?? overridesHook.error,
|
|
514
592
|
refresh,
|
|
515
593
|
updateCell,
|
|
516
|
-
isUpdating: setCell.isPending
|
|
517
|
-
updateError: setCell.error
|
|
594
|
+
isUpdating: setCell.isPending,
|
|
595
|
+
updateError: setCell.error
|
|
518
596
|
};
|
|
519
597
|
}
|
|
520
|
-
var
|
|
521
|
-
read: "can_read",
|
|
522
|
-
write: "can_write",
|
|
523
|
-
update: "can_update",
|
|
524
|
-
delete: "can_delete"
|
|
525
|
-
};
|
|
526
|
-
var ORIGIN_FIELD = {
|
|
527
|
-
read: "read_granted_via",
|
|
528
|
-
write: "write_granted_via",
|
|
529
|
-
update: "update_granted_via",
|
|
530
|
-
delete: "delete_granted_via"
|
|
531
|
-
};
|
|
598
|
+
var ACTIONS = ["read", "write", "update", "delete"];
|
|
532
599
|
|
|
533
600
|
// src/admin/PermissionsMatrix.tsx
|
|
534
601
|
var import_react2 = require("react");
|
|
@@ -550,15 +617,27 @@ function groupResources(registry) {
|
|
|
550
617
|
|
|
551
618
|
// src/admin/PermissionsMatrix.tsx
|
|
552
619
|
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
553
|
-
var
|
|
620
|
+
var ACTIONS2 = ["read", "write", "update", "delete"];
|
|
554
621
|
function PermissionsMatrix(props) {
|
|
555
|
-
const {
|
|
622
|
+
const {
|
|
623
|
+
grid,
|
|
624
|
+
originGrid,
|
|
625
|
+
isLoading,
|
|
626
|
+
error,
|
|
627
|
+
updateCell,
|
|
628
|
+
isUpdating,
|
|
629
|
+
setOverride: gridSetOverride
|
|
630
|
+
} = useRolePermissionGrid(props.roleId);
|
|
556
631
|
const groups = (0, import_react2.useMemo)(
|
|
557
632
|
() => groupResources(props.resources),
|
|
558
633
|
[props.resources]
|
|
559
634
|
);
|
|
560
635
|
const isCellEnabled = (resource, action) => {
|
|
561
|
-
|
|
636
|
+
const origin = originGrid[resource]?.[action];
|
|
637
|
+
if (origin == null || origin === "override") {
|
|
638
|
+
return false;
|
|
639
|
+
}
|
|
640
|
+
return true;
|
|
562
641
|
};
|
|
563
642
|
const cellOrigin = (resource, action) => {
|
|
564
643
|
const origin = originGrid[resource]?.[action];
|
|
@@ -567,15 +646,19 @@ function PermissionsMatrix(props) {
|
|
|
567
646
|
const setCell = async (resource, action, value) => {
|
|
568
647
|
await updateCell(resource, action, value);
|
|
569
648
|
};
|
|
649
|
+
const setOverride = async (resource, action, suppress) => {
|
|
650
|
+
await gridSetOverride(resource, action, suppress);
|
|
651
|
+
};
|
|
570
652
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children: props.children({
|
|
571
653
|
groups,
|
|
572
654
|
isCellEnabled,
|
|
573
655
|
cellOrigin,
|
|
574
656
|
setCell,
|
|
657
|
+
setOverride,
|
|
575
658
|
isLoading,
|
|
576
659
|
isUpdating,
|
|
577
660
|
error,
|
|
578
|
-
actions:
|
|
661
|
+
actions: ACTIONS2
|
|
579
662
|
}) });
|
|
580
663
|
}
|
|
581
664
|
|
|
@@ -717,6 +800,7 @@ function InviteMemberForm(props) {
|
|
|
717
800
|
useDeleteRole,
|
|
718
801
|
useInviteCompanyMember,
|
|
719
802
|
useRolePermissionGrid,
|
|
803
|
+
useRolePermissionOverrides,
|
|
720
804
|
useSetRolePermissionCell,
|
|
721
805
|
useUpdateRole
|
|
722
806
|
});
|