@jini-ai/user-management 0.2.0 → 0.3.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/CHANGELOG.md +39 -0
- package/dist/admin/people.rules.js +2 -1
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +1 -0
- package/dist/core/principal-kind-policy.d.ts +30 -0
- package/dist/core/principal-kind-policy.js +28 -0
- package/dist/core/types.d.ts +7 -2
- package/dist/server/admin-crud-service.js +4 -1
- package/dist/server/authorize.d.ts +9 -6
- package/dist/server/authorize.js +11 -3
- package/dist/server/permission-migrations.d.ts +35 -15
- package/dist/server/permission-migrations.js +16 -16
- package/dist/server/seed.js +6 -1
- package/package.json +7 -7
- package/dist/react/roles/Roles.d.ts +0 -100
- package/dist/react/roles/Roles.hooks.d.ts +0 -17
- package/dist/react/roles/Roles.hooks.js +0 -40
- package/dist/react/roles/Roles.js +0 -150
- package/dist/react/roles/hooks/use-roles.hooks.d.ts +0 -80
- package/dist/react/roles/hooks/use-roles.hooks.js +0 -303
- package/dist/react/roles/rules.d.ts +0 -35
- package/dist/react/roles/rules.js +0 -31
- package/dist/react/roles/text.d.ts +0 -27
- package/dist/react/roles/text.js +0 -13
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0 — 2026-10-05
|
|
4
|
+
|
|
5
|
+
### BREAKING
|
|
6
|
+
|
|
7
|
+
- `PrincipalKind` adds `"member"` (a host's public-site sign-up). New `principalKindMayExercisePermission({ kind, permission })`: `authorize()` checks it right after the disabled-principal check, so a member is denied every operator permission (reason `principal_kind_denied`) even when a `*` grant reaches it, and the last-owner guard never counts a member as the remaining owner. Exhaustive switches over `PrincipalKind` need the new case.
|
|
8
|
+
- Permission migrations are registered explicitly at composition: `registerPermissionMigration`/`listPermissionMigrations` (an import-time module singleton) are replaced by `createPermissionMigrationRegistry({}, { migrations })`, seeded with the built-in pairs, and `migrateDeprecatedPermissionGrants` takes the explicit `deps.migrations` list.
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- `seedIdentity` grants `role.manage` to the built-in admin policy, so an admin runs Roles & Permissions. `user.manage` stays owner-only and the grant-exceeds-issuer clamp is unchanged. Already-seeded workspaces are reconciled by the host.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- `describePeopleError` ignores inherited `Object.prototype` keys (an error code of `"constructor"` or `"toString"` resolved to that member instead of the message or fallback).
|
|
17
|
+
- Internal `@jini-ai/core` and `@jini-ai/ui-kit` dependencies are caret ranges (`workspace:^`) instead of exact pins.
|
|
18
|
+
|
|
3
19
|
## 0.2.0 — 2026-10-02
|
|
4
20
|
|
|
5
21
|
### BREAKING
|
|
@@ -9,6 +25,29 @@
|
|
|
9
25
|
|
|
10
26
|
## Unreleased
|
|
11
27
|
|
|
28
|
+
### The built-in admin role holds role.manage
|
|
29
|
+
|
|
30
|
+
- `seedIdentity` grants `role.manage` to the built-in admin policy (owner decision, 2026-10-05), so a
|
|
31
|
+
fresh workspace's admin runs Roles & Permissions. `user.manage` stays owner-only. The issuer clamp
|
|
32
|
+
(`GRANT_EXCEEDS_ISSUER`) is unchanged. Already-seeded workspaces are not revisited by the seed; a
|
|
33
|
+
host reconciles them (e.g. a built-in-role grant applied at boot).
|
|
34
|
+
|
|
35
|
+
### Site members are barred from operator permissions
|
|
36
|
+
|
|
37
|
+
- Add `"member"` to `PrincipalKind` for a host's public-site sign-ups. `authorize()` now denies a
|
|
38
|
+
`member` principal every permission with reason `principal_kind_denied`, ahead of any grant
|
|
39
|
+
(including the owner wildcard). The rule is one exported function,
|
|
40
|
+
`principalKindMayExercisePermission({ kind, permission })`, so a later decision can relax it in one
|
|
41
|
+
place. Owner counting for the last-owner guard skips barred kinds. Existing kinds are unaffected.
|
|
42
|
+
|
|
43
|
+
### BREAKING: permission migrations are an explicit registry
|
|
44
|
+
|
|
45
|
+
- Remove the module-singleton `registerPermissionMigration`/`listPermissionMigrations`. Create a
|
|
46
|
+
registry with `createPermissionMigrationRegistry({}, { migrations })` at the composition root
|
|
47
|
+
(seeded with the built-in pairs; `register` keeps the overwrite-by-`from` semantics) and pass
|
|
48
|
+
`migrations: registry.list({})` to `migrateDeprecatedPermissionGrants`, which now requires it.
|
|
49
|
+
Which pairs exist no longer depends on module import order.
|
|
50
|
+
|
|
12
51
|
### Compilation fixes
|
|
13
52
|
|
|
14
53
|
- Import the consolidated tab-strip barrel (requires the UI peer to export that
|
|
@@ -39,7 +39,8 @@ export function describePeopleError({ error, fallback }, _optional = {}) {
|
|
|
39
39
|
SELF_DELETE: 'You cannot delete your own account.', USER_IN_TRASH: 'This user is in the Trash; restore them first.',
|
|
40
40
|
USERNAME_IN_TRASH: 'A user with this username is in the Trash; restore or delete them permanently first.',
|
|
41
41
|
};
|
|
42
|
-
|
|
42
|
+
// Own keys only: a server code such as "constructor" must not resolve to an Object.prototype member.
|
|
43
|
+
if (error && typeof error === 'object' && 'code' in error && typeof error.code === 'string' && Object.hasOwn(copy, error.code))
|
|
43
44
|
return copy[error.code];
|
|
44
45
|
return error instanceof Error && error.message ? error.message : fallback;
|
|
45
46
|
}
|
package/dist/core/index.d.ts
CHANGED
package/dist/core/index.js
CHANGED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { PrincipalKind } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* @file Which principal kinds may exercise operator permissions at all.
|
|
4
|
+
*
|
|
5
|
+
* Purpose:
|
|
6
|
+
* The one place that decides whether a principal's KIND lets it use the operator RBAC axis
|
|
7
|
+
* (`authorize()`'s permissions). Today the answer is: every kind except `member`. A `member` is a
|
|
8
|
+
* public-site sign-up — a host's front-end account, governed by that host's own entitlement axis
|
|
9
|
+
* (tiers, gated content), never by operator RBAC. OWNER DECISION 2026-10-04 (Tovu F3144): site
|
|
10
|
+
* members must not be able to use admin features, at least for now.
|
|
11
|
+
*
|
|
12
|
+
* Why a function and not a check at each call site:
|
|
13
|
+
* `authorize()` calls this before it reads any grant, so a member principal is denied every
|
|
14
|
+
* permission even if a grant row reaches it (a hand-edited database, a future grant path that forgets
|
|
15
|
+
* the `kind='user'` target check in `grant-service.ts`). Owner-counting guards call it too, so a
|
|
16
|
+
* member can never count as the workspace's remaining owner. Allowing members some admin capability
|
|
17
|
+
* later is a change to this one function — `permission` is passed so such a decision can open a
|
|
18
|
+
* specific capability rather than all of them.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Whether a principal of `kind` may exercise `permission` through operator RBAC at all. Returning
|
|
22
|
+
* `true` only means the kind is not barred; the principal still needs a matching grant.
|
|
23
|
+
*
|
|
24
|
+
* @complexity O(1).
|
|
25
|
+
*/
|
|
26
|
+
export declare function principalKindMayExercisePermission(required: {
|
|
27
|
+
kind: PrincipalKind;
|
|
28
|
+
permission: string;
|
|
29
|
+
}, _optional?: Record<string, never>): boolean;
|
|
30
|
+
//# sourceMappingURL=principal-kind-policy.d.ts.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Which principal kinds may exercise operator permissions at all.
|
|
3
|
+
*
|
|
4
|
+
* Purpose:
|
|
5
|
+
* The one place that decides whether a principal's KIND lets it use the operator RBAC axis
|
|
6
|
+
* (`authorize()`'s permissions). Today the answer is: every kind except `member`. A `member` is a
|
|
7
|
+
* public-site sign-up — a host's front-end account, governed by that host's own entitlement axis
|
|
8
|
+
* (tiers, gated content), never by operator RBAC. OWNER DECISION 2026-10-04 (Tovu F3144): site
|
|
9
|
+
* members must not be able to use admin features, at least for now.
|
|
10
|
+
*
|
|
11
|
+
* Why a function and not a check at each call site:
|
|
12
|
+
* `authorize()` calls this before it reads any grant, so a member principal is denied every
|
|
13
|
+
* permission even if a grant row reaches it (a hand-edited database, a future grant path that forgets
|
|
14
|
+
* the `kind='user'` target check in `grant-service.ts`). Owner-counting guards call it too, so a
|
|
15
|
+
* member can never count as the workspace's remaining owner. Allowing members some admin capability
|
|
16
|
+
* later is a change to this one function — `permission` is passed so such a decision can open a
|
|
17
|
+
* specific capability rather than all of them.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Whether a principal of `kind` may exercise `permission` through operator RBAC at all. Returning
|
|
21
|
+
* `true` only means the kind is not barred; the principal still needs a matching grant.
|
|
22
|
+
*
|
|
23
|
+
* @complexity O(1).
|
|
24
|
+
*/
|
|
25
|
+
export function principalKindMayExercisePermission(required, _optional = {}) {
|
|
26
|
+
return required.kind !== "member";
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=principal-kind-policy.js.map
|
package/dist/core/types.d.ts
CHANGED
|
@@ -18,8 +18,13 @@ import type { ISODateTime, UUID } from "@jini-ai/core/primitives";
|
|
|
18
18
|
* the full target schema so those transitions are additive later,
|
|
19
19
|
* not a repaint.
|
|
20
20
|
*/
|
|
21
|
-
/**
|
|
22
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Every actor is one of these. `member` is a host's public-site sign-up (a front-end account, not an
|
|
23
|
+
* operator): it may hold a principal row for identity and audit, but `principalKindMayExercisePermission`
|
|
24
|
+
* (`principal-kind-policy.ts`) bars it from every operator permission, so `authorize()` denies it
|
|
25
|
+
* whatever grants reach it.
|
|
26
|
+
*/
|
|
27
|
+
export type PrincipalKind = "user" | "agent" | "api_key" | "system" | "member";
|
|
23
28
|
/** Disable-only lifecycle — there is no hard-delete state. See docs/decisions/DR-001-identity-and-session-boundary.md. */
|
|
24
29
|
export type PrincipalStatus = "active" | "disabled";
|
|
25
30
|
/**
|
|
@@ -3,6 +3,7 @@ import { inIdentityTransaction } from "./identity-transaction.js";
|
|
|
3
3
|
import { assertCallerHasAnyPermission, assertGrantClamp, assertOwnerTargetMayBeModified, principalHoldsOwnerWildcard } from "./grant-service.js";
|
|
4
4
|
import { validatePasswordPolicy } from "./password-policy.js";
|
|
5
5
|
import { isKnownPermission } from "../core/permissions.js";
|
|
6
|
+
import { principalKindMayExercisePermission } from "../core/principal-kind-policy.js";
|
|
6
7
|
import { IdentityConflictError, IdentityNotFoundError, IdentityValidationError, OwnerRequiredError, PermissionUnknownError, } from "../core/types.js";
|
|
7
8
|
/**
|
|
8
9
|
* @file An amendment that completes the users/roles/policies admin CRUD surface
|
|
@@ -554,7 +555,9 @@ export async function removePolicyPermission(required) {
|
|
|
554
555
|
async function countActiveOwnerWildcardPrincipals(required) {
|
|
555
556
|
const { deps, workspaceId } = required;
|
|
556
557
|
const principals = await deps.repos.principals.list({ workspaceId });
|
|
557
|
-
|
|
558
|
+
// A kind barred from operator permissions (a site `member`) can never act as an owner, so it is
|
|
559
|
+
// not counted even if a `*` row reaches it — and members never cost a grant read here.
|
|
560
|
+
const activePrincipals = principals.filter((principal) => principal.status === "active" && principalKindMayExercisePermission({ kind: principal.kind, permission: "*" }));
|
|
558
561
|
const flags = await Promise.all(activePrincipals.map((principal) => principalHoldsOwnerWildcard({ deps, workspaceId, principalId: principal.id })));
|
|
559
562
|
return flags.filter(Boolean).length;
|
|
560
563
|
}
|
|
@@ -19,8 +19,8 @@ import type { PolicyPermissionRecord } from "../core/types.js";
|
|
|
19
19
|
*
|
|
20
20
|
* Architectural role:
|
|
21
21
|
* Implements the matcher precedence in behavior.spec §1.1 exactly:
|
|
22
|
-
* disabled-principal short-circuit >
|
|
23
|
-
* fail-closed default. Fail-closed extensibility: a non-null
|
|
22
|
+
* disabled-principal short-circuit > principal-kind bar > owner wildcard >
|
|
23
|
+
* exact matching row > fail-closed default. Fail-closed extensibility: a non-null
|
|
24
24
|
* `constraintJson` is never treated as unconstrained, and a `resourceType`
|
|
25
25
|
* mismatch/absence is never treated as a global grant.
|
|
26
26
|
* See docs/decisions/DR-001-identity-and-session-boundary.md.
|
|
@@ -34,7 +34,7 @@ export interface AuthorizeContext {
|
|
|
34
34
|
/** `authorize`'s result — always fail-closed on `allowed: false`. See docs/decisions/DR-001-identity-and-session-boundary.md. */
|
|
35
35
|
export interface AuthorizeResult {
|
|
36
36
|
allowed: boolean;
|
|
37
|
-
/** Machine-readable reason: `principal_disabled | no_grant | resource_scope_mismatch | unconstrained_deny | owner_wildcard | matched`. */
|
|
37
|
+
/** Machine-readable reason: `principal_disabled | principal_kind_denied | no_grant | resource_scope_mismatch | unconstrained_deny | owner_wildcard | matched`. */
|
|
38
38
|
reason: string;
|
|
39
39
|
}
|
|
40
40
|
/** Repos `authorize()`/`resolveEffectivePermissions()` read. Not a port bag with alternate adapters — see file header. */
|
|
@@ -68,10 +68,13 @@ export declare function resolveEffectivePermissions(required: {
|
|
|
68
68
|
*
|
|
69
69
|
* Precedence (behavior.spec §1.1, highest to lowest):
|
|
70
70
|
* 1. Disabled principal -> `false`, `principal_disabled` (overrides all grants).
|
|
71
|
-
* 2.
|
|
72
|
-
*
|
|
71
|
+
* 2. A principal whose kind may not exercise `permission` (today: `member`, see
|
|
72
|
+
* `principal-kind-policy.ts`) -> `false`, `principal_kind_denied` (overrides all grants,
|
|
73
|
+
* including the owner wildcard).
|
|
74
|
+
* 3. An unconstrained owner `"*"` row -> `true`, `owner_wildcard`.
|
|
75
|
+
* 4. An exact matching row (permission + workspace + resource-scope + no
|
|
73
76
|
* constraint) -> `true`, `matched`.
|
|
74
|
-
*
|
|
77
|
+
* 5. Fail-closed default -> `false`, with the most specific diagnostic reason
|
|
75
78
|
* available among same-permission candidates (`unconstrained_deny` over
|
|
76
79
|
* `resource_scope_mismatch` over `no_grant` — an inferred, undocumented-but-
|
|
77
80
|
* reasonable tie-break; the spec only pins each reason in isolation, see
|
package/dist/server/authorize.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { principalKindMayExercisePermission } from "../core/principal-kind-policy.js";
|
|
1
2
|
/**
|
|
2
3
|
* Resolve a principal's effective permission rows: the union of (a) its
|
|
3
4
|
* roles' policies' permissions and (b) policies attached directly via
|
|
@@ -46,10 +47,13 @@ function matchesRow(row, permission, context) {
|
|
|
46
47
|
*
|
|
47
48
|
* Precedence (behavior.spec §1.1, highest to lowest):
|
|
48
49
|
* 1. Disabled principal -> `false`, `principal_disabled` (overrides all grants).
|
|
49
|
-
* 2.
|
|
50
|
-
*
|
|
50
|
+
* 2. A principal whose kind may not exercise `permission` (today: `member`, see
|
|
51
|
+
* `principal-kind-policy.ts`) -> `false`, `principal_kind_denied` (overrides all grants,
|
|
52
|
+
* including the owner wildcard).
|
|
53
|
+
* 3. An unconstrained owner `"*"` row -> `true`, `owner_wildcard`.
|
|
54
|
+
* 4. An exact matching row (permission + workspace + resource-scope + no
|
|
51
55
|
* constraint) -> `true`, `matched`.
|
|
52
|
-
*
|
|
56
|
+
* 5. Fail-closed default -> `false`, with the most specific diagnostic reason
|
|
53
57
|
* available among same-permission candidates (`unconstrained_deny` over
|
|
54
58
|
* `resource_scope_mismatch` over `no_grant` — an inferred, undocumented-but-
|
|
55
59
|
* reasonable tie-break; the spec only pins each reason in isolation, see
|
|
@@ -68,6 +72,10 @@ export async function authorize(required, optional = {}) {
|
|
|
68
72
|
if (!principal || principal.status === "disabled") {
|
|
69
73
|
return { allowed: false, reason: "principal_disabled" };
|
|
70
74
|
}
|
|
75
|
+
// Ahead of every grant read: a barred kind is denied even when a `*` row reaches it.
|
|
76
|
+
if (!principalKindMayExercisePermission({ kind: principal.kind, permission })) {
|
|
77
|
+
return { allowed: false, reason: "principal_kind_denied" };
|
|
78
|
+
}
|
|
71
79
|
const effectiveRows = await resolveEffectivePermissions({
|
|
72
80
|
deps,
|
|
73
81
|
principalId,
|
|
@@ -16,11 +16,17 @@ import type { IdentityTransactionPort, PolicyPermissionRepoPort, PolicyRepoPort
|
|
|
16
16
|
* hand-rolling a fourth divergent copy of the same fix.
|
|
17
17
|
*
|
|
18
18
|
* How it relates to the project:
|
|
19
|
-
* - `
|
|
20
|
-
*
|
|
21
|
-
* `
|
|
19
|
+
* - `createPermissionMigrationRegistry` builds a small, in-process registry
|
|
20
|
+
* object — pure metadata, no I/O, mirrors `permissions.ts`'s
|
|
21
|
+
* `PermissionCatalog.register` overwrite semantics. A host creates ONE at its
|
|
22
|
+
* composition root and registers its own pairs on it explicitly; there is no
|
|
23
|
+
* module-scope registry, so which migrations exist never depends on which
|
|
24
|
+
* modules a process happened to import (a module-singleton filled by import
|
|
25
|
+
* side effects once ran a standalone script's boot reconciliation against an
|
|
26
|
+
* empty registry because that script never imported the registering module).
|
|
22
27
|
* - `migrateDeprecatedPermissionGrants` is the actual grant fan-out: for
|
|
23
|
-
* every
|
|
28
|
+
* every pair the caller passes in `deps.migrations` (normally its registry's
|
|
29
|
+
* `list({})`), every policy holding `from` gets every string in
|
|
24
30
|
* `to` it doesn't already hold. It is deliberately, structurally
|
|
25
31
|
* additive-only — there is no delete/update call against
|
|
26
32
|
* `policy_permissions` anywhere in this file.
|
|
@@ -42,29 +48,43 @@ export type { PermissionMigration } from '../core/builtin-permissions.js';
|
|
|
42
48
|
export interface MigrateDeprecatedPermissionGrantsDeps {
|
|
43
49
|
/** Bound to the same storage as both repository ports; required before any migration work. */
|
|
44
50
|
transactions: IdentityTransactionPort;
|
|
51
|
+
/** Every `{from, to}` pair to fan out — normally `createPermissionMigrationRegistry(...).list({})`. */
|
|
52
|
+
migrations: readonly PermissionMigration[];
|
|
45
53
|
policyPermissions: PolicyPermissionRepoPort;
|
|
46
54
|
policies: PolicyRepoPort;
|
|
47
55
|
idGen: IdGenerator;
|
|
48
56
|
workspaceId: UUID;
|
|
49
57
|
}
|
|
50
58
|
export interface MigrateDeprecatedPermissionGrantsResult {
|
|
51
|
-
/** Total new `policy_permissions` rows written across every policy and every
|
|
59
|
+
/** Total new `policy_permissions` rows written across every policy and every passed pair. */
|
|
52
60
|
migratedGrantCount: number;
|
|
53
61
|
}
|
|
62
|
+
/** A host-owned set of `{from, to, reason}` pairs; see {@link createPermissionMigrationRegistry}. */
|
|
63
|
+
export interface PermissionMigrationRegistry {
|
|
64
|
+
/**
|
|
65
|
+
* Register a `{from, to, reason}` rename/split pair for later fan-out.
|
|
66
|
+
* Idempotent: re-registering the same `from` overwrites the prior entry
|
|
67
|
+
* rather than duplicating it (matches `PermissionCatalog.register`'s
|
|
68
|
+
* existing overwrite semantics, `identity/permissions.ts`).
|
|
69
|
+
*
|
|
70
|
+
* @complexity O(1).
|
|
71
|
+
*/
|
|
72
|
+
register(migration: PermissionMigration): void;
|
|
73
|
+
/** Enumerate every registered migration pair, built-ins first, in first-registration order. */
|
|
74
|
+
list(_required: Record<string, never>): PermissionMigration[];
|
|
75
|
+
}
|
|
54
76
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* rather than duplicating it (matches `PermissionCatalog.register`'s
|
|
58
|
-
* existing overwrite semantics, `identity/permissions.ts`).
|
|
77
|
+
* Create a migration registry seeded with the library's built-in pairs, then `optional.migrations`.
|
|
78
|
+
* Keyed by `from`, so a host pair with a built-in's `from` overwrites it rather than duplicating.
|
|
59
79
|
*
|
|
60
|
-
* @complexity O(
|
|
80
|
+
* @complexity O(b + m) where b = built-in pairs and m = `optional.migrations`.
|
|
61
81
|
* @overallScore 100
|
|
62
82
|
*/
|
|
63
|
-
export declare function
|
|
64
|
-
|
|
65
|
-
|
|
83
|
+
export declare function createPermissionMigrationRegistry(_required?: Record<string, never>, optional?: {
|
|
84
|
+
migrations?: readonly PermissionMigration[];
|
|
85
|
+
}): PermissionMigrationRegistry;
|
|
66
86
|
/**
|
|
67
|
-
* For every
|
|
87
|
+
* For every `{from, to}` pair in `deps.migrations`, for every policy in the workspace
|
|
68
88
|
* holding `from`, ensure every string in `to` is also granted — adding only
|
|
69
89
|
* the rows that are missing. Never removes or mutates `from` (or any other
|
|
70
90
|
* existing row) — additive-only by construction. The entire fan-out runs in
|
|
@@ -75,7 +95,7 @@ export declare function listPermissionMigrations(_required: Record<string, never
|
|
|
75
95
|
* returns `migratedGrantCount: 0` (mirrors `migrateLegacyPresentationSettings`'s
|
|
76
96
|
* boot-safety precedent).
|
|
77
97
|
*
|
|
78
|
-
* @complexity O(p * m) where p = policies in the workspace, m =
|
|
98
|
+
* @complexity O(p * m) where p = policies in the workspace, m = passed
|
|
79
99
|
* migration pairs; both are small, bounded collections in this app's shape.
|
|
80
100
|
* @overallScore 100
|
|
81
101
|
* See docs/decisions/DR-003-additive-permission-migration.md.
|
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
import { inIdentityTransaction } from "./identity-transaction.js";
|
|
2
2
|
import { BUILTIN_PERMISSION_MIGRATIONS } from '../core/builtin-permissions.js';
|
|
3
|
-
/** Module-singleton registry — keyed by `from` so re-registration overwrites (idempotent, matches `PermissionCatalog.register`). */
|
|
4
|
-
const registry = new Map(BUILTIN_PERMISSION_MIGRATIONS.map((migration) => [migration.from, migration]));
|
|
5
3
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* rather than duplicating it (matches `PermissionCatalog.register`'s
|
|
9
|
-
* existing overwrite semantics, `identity/permissions.ts`).
|
|
4
|
+
* Create a migration registry seeded with the library's built-in pairs, then `optional.migrations`.
|
|
5
|
+
* Keyed by `from`, so a host pair with a built-in's `from` overwrites it rather than duplicating.
|
|
10
6
|
*
|
|
11
|
-
* @complexity O(
|
|
7
|
+
* @complexity O(b + m) where b = built-in pairs and m = `optional.migrations`.
|
|
12
8
|
* @overallScore 100
|
|
13
9
|
*/
|
|
14
|
-
export function
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
10
|
+
export function createPermissionMigrationRegistry(_required = {}, optional = {}) {
|
|
11
|
+
const byFrom = new Map();
|
|
12
|
+
const register = (migration) => {
|
|
13
|
+
byFrom.set(migration.from, migration);
|
|
14
|
+
};
|
|
15
|
+
for (const migration of BUILTIN_PERMISSION_MIGRATIONS)
|
|
16
|
+
register(migration);
|
|
17
|
+
for (const migration of optional.migrations ?? [])
|
|
18
|
+
register(migration);
|
|
19
|
+
return { register, list: () => [...byFrom.values()] };
|
|
20
20
|
}
|
|
21
21
|
/**
|
|
22
|
-
* For every
|
|
22
|
+
* For every `{from, to}` pair in `deps.migrations`, for every policy in the workspace
|
|
23
23
|
* holding `from`, ensure every string in `to` is also granted — adding only
|
|
24
24
|
* the rows that are missing. Never removes or mutates `from` (or any other
|
|
25
25
|
* existing row) — additive-only by construction. The entire fan-out runs in
|
|
@@ -30,13 +30,13 @@ export function listPermissionMigrations(_required) {
|
|
|
30
30
|
* returns `migratedGrantCount: 0` (mirrors `migrateLegacyPresentationSettings`'s
|
|
31
31
|
* boot-safety precedent).
|
|
32
32
|
*
|
|
33
|
-
* @complexity O(p * m) where p = policies in the workspace, m =
|
|
33
|
+
* @complexity O(p * m) where p = policies in the workspace, m = passed
|
|
34
34
|
* migration pairs; both are small, bounded collections in this app's shape.
|
|
35
35
|
* @overallScore 100
|
|
36
36
|
* See docs/decisions/DR-003-additive-permission-migration.md.
|
|
37
37
|
*/
|
|
38
38
|
export async function migrateDeprecatedPermissionGrants(deps) {
|
|
39
|
-
const migrations =
|
|
39
|
+
const { migrations } = deps;
|
|
40
40
|
if (migrations.length === 0)
|
|
41
41
|
return { migratedGrantCount: 0 };
|
|
42
42
|
return inIdentityTransaction({
|
package/dist/server/seed.js
CHANGED
|
@@ -126,7 +126,12 @@ const BUILTIN_ADMIN_PERMISSIONS = [
|
|
|
126
126
|
// directly at seed — a distinct grant from settings.write, not owner-only (unlike user.manage/
|
|
127
127
|
// role.manage below).
|
|
128
128
|
"workspace.manage",
|
|
129
|
-
//
|
|
129
|
+
// Roles & Permissions: admin holds role.manage (owner decision 2026-10-05, reversing DR-001's
|
|
130
|
+
// owner-only placement for this one string). The grant service's issuer clamp still refuses any
|
|
131
|
+
// grant beyond what the caller holds (GRANT_EXCEEDS_ISSUER), so an admin cannot hand out owner.
|
|
132
|
+
// Hosts reconcile already-seeded workspaces themselves; seedIdentity never revisits them.
|
|
133
|
+
"role.manage",
|
|
134
|
+
// Owner-only per DR-001: "user.manage" is deliberately absent. See docs/decisions/DR-001-identity-and-session-boundary.md.
|
|
130
135
|
];
|
|
131
136
|
const BUILTIN_EDITOR_PERMISSIONS = [
|
|
132
137
|
"content.read",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jini-ai/user-management",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Runtime-separated user management contracts and Node authentication, authorization, and administration services.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -127,14 +127,14 @@
|
|
|
127
127
|
"@testing-library/user-event": "^14.5.0",
|
|
128
128
|
"@testing-library/jest-dom": "^6.6.3",
|
|
129
129
|
"jsdom": "^25.0.1",
|
|
130
|
-
"@jini-ai/
|
|
131
|
-
"@jini-ai/admin": "0.4.
|
|
132
|
-
"@jini-ai/
|
|
133
|
-
"@jini-ai/
|
|
130
|
+
"@jini-ai/ui": "0.4.3",
|
|
131
|
+
"@jini-ai/admin": "0.4.2",
|
|
132
|
+
"@jini-ai/agentic": "0.4.1",
|
|
133
|
+
"@jini-ai/cms": "0.5.0"
|
|
134
134
|
},
|
|
135
135
|
"dependencies": {
|
|
136
|
-
"@jini-ai/
|
|
137
|
-
"@jini-ai/
|
|
136
|
+
"@jini-ai/ui-kit": "^0.1.1",
|
|
137
|
+
"@jini-ai/core": "^0.4.1"
|
|
138
138
|
},
|
|
139
139
|
"scripts": {
|
|
140
140
|
"build": "tsc -p tsconfig.json",
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import type { Translate } from "@jini-ai/ui/panel-kit";
|
|
2
|
-
import { type FormEvent } from "react";
|
|
3
|
-
import type { AdminPolicy, AdminPolicyPermission, AdminRole } from "../models.js";
|
|
4
|
-
import { useRoles, type RolesDependencies, type PendingPermissionRemove } from "./hooks/use-roles.hooks.js";
|
|
5
|
-
import { type RolesTabId } from "./Roles.hooks.js";
|
|
6
|
-
export interface RolesProps extends RolesDependencies {
|
|
7
|
-
onTabChange: (required: {
|
|
8
|
-
tabId: RolesTabId;
|
|
9
|
-
}) => void;
|
|
10
|
-
usersHref: string;
|
|
11
|
-
tabId?: string | null | undefined;
|
|
12
|
-
useRolesHook?: typeof useRoles | undefined;
|
|
13
|
-
}
|
|
14
|
-
export interface RoleCreateFormController {
|
|
15
|
-
name: string;
|
|
16
|
-
setName: (name: string) => void;
|
|
17
|
-
saving: boolean;
|
|
18
|
-
error: string | null;
|
|
19
|
-
submit: (e: FormEvent) => Promise<void>;
|
|
20
|
-
}
|
|
21
|
-
export interface RoleRowController {
|
|
22
|
-
editingId: string | null;
|
|
23
|
-
setEditingId: (roleId: string | null) => void;
|
|
24
|
-
draftName: string;
|
|
25
|
-
setDraftName: (name: string) => void;
|
|
26
|
-
startRename: (role: AdminRole) => void;
|
|
27
|
-
saveRename: (required: {
|
|
28
|
-
roleId: string;
|
|
29
|
-
}) => Promise<void>;
|
|
30
|
-
savingId: string | null;
|
|
31
|
-
requestDelete: (role: AdminRole | null) => void;
|
|
32
|
-
}
|
|
33
|
-
export interface RolesSectionProps {
|
|
34
|
-
roles: AdminRole[];
|
|
35
|
-
create: RoleCreateFormController;
|
|
36
|
-
row: RoleRowController;
|
|
37
|
-
t: (key: string) => string;
|
|
38
|
-
translate: Translate;
|
|
39
|
-
}
|
|
40
|
-
export declare function RolesSection({ roles, create, row, t, translate }: RolesSectionProps): import("react").JSX.Element;
|
|
41
|
-
export interface PolicyRowController {
|
|
42
|
-
editingId: string | null;
|
|
43
|
-
setEditingId: (policyId: string | null) => void;
|
|
44
|
-
draftName: string;
|
|
45
|
-
setDraftName: (name: string) => void;
|
|
46
|
-
draftDescription: string;
|
|
47
|
-
setDraftDescription: (description: string) => void;
|
|
48
|
-
startRename: (policy: AdminPolicy) => void;
|
|
49
|
-
saveRename: (required: {
|
|
50
|
-
policyId: string;
|
|
51
|
-
}) => Promise<void>;
|
|
52
|
-
savingId: string | null;
|
|
53
|
-
requestDelete: (policy: AdminPolicy | null) => void;
|
|
54
|
-
}
|
|
55
|
-
export interface PolicyPermissionController {
|
|
56
|
-
openForPolicyId: string | null;
|
|
57
|
-
permission: string;
|
|
58
|
-
setPermission: (permission: string) => void;
|
|
59
|
-
resourceType: string;
|
|
60
|
-
setResourceType: (resourceType: string) => void;
|
|
61
|
-
toggleForm: (required: {
|
|
62
|
-
policyId: string;
|
|
63
|
-
}) => void;
|
|
64
|
-
write: (required: {
|
|
65
|
-
policyId: string;
|
|
66
|
-
}) => Promise<void>;
|
|
67
|
-
rows: AdminPolicyPermission[];
|
|
68
|
-
loading: boolean;
|
|
69
|
-
removingId: string | null;
|
|
70
|
-
requestRemove: (target: PendingPermissionRemove) => void;
|
|
71
|
-
}
|
|
72
|
-
export interface PolicyRowProps {
|
|
73
|
-
policy: AdminPolicy;
|
|
74
|
-
row: PolicyRowController;
|
|
75
|
-
permission: PolicyPermissionController;
|
|
76
|
-
agentBase: string;
|
|
77
|
-
t: (key: string) => string;
|
|
78
|
-
translate: Translate;
|
|
79
|
-
}
|
|
80
|
-
export declare function PolicyRow({ policy, row, permission, agentBase, t, translate }: PolicyRowProps): import("react").JSX.Element;
|
|
81
|
-
export interface PolicyCreateFormController {
|
|
82
|
-
name: string;
|
|
83
|
-
setName: (name: string) => void;
|
|
84
|
-
description: string;
|
|
85
|
-
setDescription: (description: string) => void;
|
|
86
|
-
saving: boolean;
|
|
87
|
-
error: string | null;
|
|
88
|
-
submit: (e: FormEvent) => Promise<void>;
|
|
89
|
-
}
|
|
90
|
-
export interface PoliciesSectionProps {
|
|
91
|
-
policies: AdminPolicy[];
|
|
92
|
-
create: PolicyCreateFormController;
|
|
93
|
-
row: PolicyRowController;
|
|
94
|
-
permission: PolicyPermissionController;
|
|
95
|
-
t: (key: string) => string;
|
|
96
|
-
translate: Translate;
|
|
97
|
-
}
|
|
98
|
-
export declare function PoliciesSection({ policies, create, row, permission, t, translate }: PoliciesSectionProps): import("react").JSX.Element;
|
|
99
|
-
export declare function Roles({ tabId, useRolesHook, port, translate: injectedTranslate, queryScope, onTabChange, usersHref }: RolesProps): import("react").JSX.Element;
|
|
100
|
-
//# sourceMappingURL=Roles.d.ts.map
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import type { TabBarTab } from "@jini-ai/ui/tab-strip";
|
|
2
|
-
import type { Translate } from "@jini-ai/ui/panel-kit";
|
|
3
|
-
export declare function RolesIcon({ size }: {
|
|
4
|
-
size?: number;
|
|
5
|
-
}): import("react").JSX.Element;
|
|
6
|
-
export declare function PoliciesIcon({ size }: {
|
|
7
|
-
size?: number;
|
|
8
|
-
}): import("react").JSX.Element;
|
|
9
|
-
export declare const ROLES_TAB_IDS: readonly ["roles", "policies"];
|
|
10
|
-
export type RolesTabId = (typeof ROLES_TAB_IDS)[number];
|
|
11
|
-
export declare function resolveRolesTabId({ tabId }: {
|
|
12
|
-
tabId: string | null | undefined;
|
|
13
|
-
}): RolesTabId;
|
|
14
|
-
export declare function resolveRolesTabs({ translate: t }: {
|
|
15
|
-
translate: Translate;
|
|
16
|
-
}): TabBarTab[];
|
|
17
|
-
//# sourceMappingURL=Roles.hooks.d.ts.map
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { resolveActiveTabId } from "@jini-ai/ui/panel-kit";
|
|
3
|
-
const LINE_ICON = {
|
|
4
|
-
viewBox: "0 0 24 24",
|
|
5
|
-
fill: "none",
|
|
6
|
-
stroke: "currentColor",
|
|
7
|
-
strokeWidth: 1.5,
|
|
8
|
-
strokeLinecap: "round",
|
|
9
|
-
strokeLinejoin: "round",
|
|
10
|
-
"aria-hidden": true,
|
|
11
|
-
};
|
|
12
|
-
export function RolesIcon({ size = 16 }) {
|
|
13
|
-
return (_jsxs("svg", { ...LINE_ICON, width: size, height: size, children: [_jsx("circle", { cx: "12", cy: "8", r: "3.5" }), _jsx("path", { d: "M5 20c0-3.3 3.1-6 7-6s7 2.7 7 6" })] }));
|
|
14
|
-
}
|
|
15
|
-
export function PoliciesIcon({ size = 16 }) {
|
|
16
|
-
return (_jsxs("svg", { ...LINE_ICON, width: size, height: size, children: [_jsx("path", { d: "M12 3l7 2.5v6c0 4-3 7.4-7 8.5-4-1.1-7-4.5-7-8.5v-6z" }), _jsx("path", { d: "M9.5 11.5h5M9.5 14.5h3" })] }));
|
|
17
|
-
}
|
|
18
|
-
export const ROLES_TAB_IDS = ["roles", "policies"];
|
|
19
|
-
export function resolveRolesTabId({ tabId }) {
|
|
20
|
-
return resolveActiveTabId({ tabId, validIds: ROLES_TAB_IDS, defaultId: "roles" });
|
|
21
|
-
}
|
|
22
|
-
export function resolveRolesTabs({ translate: t }) {
|
|
23
|
-
return [
|
|
24
|
-
{
|
|
25
|
-
id: "roles",
|
|
26
|
-
label: t("Roles"),
|
|
27
|
-
icon: _jsx(RolesIcon, {}),
|
|
28
|
-
handle: "roles-tab-roles",
|
|
29
|
-
handleLabel: t("Switch to the Roles tab — list, create, rename and delete the roles a person can be assigned"),
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
id: "policies",
|
|
33
|
-
label: t("Policies"),
|
|
34
|
-
icon: _jsx(PoliciesIcon, {}),
|
|
35
|
-
handle: "roles-tab-policies",
|
|
36
|
-
handleLabel: t("Switch to the Policies tab — list and create policies, and add or remove the individual permissions on each one"),
|
|
37
|
-
},
|
|
38
|
-
];
|
|
39
|
-
}
|
|
40
|
-
//# sourceMappingURL=Roles.hooks.js.map
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { rolesDescriptionParts, roleDeleteBodyParts, policyDeleteBodyParts, permissionRemoveBodyParts } from "./text.js";
|
|
3
|
-
import { Fragment } from "react";
|
|
4
|
-
import { DataTable, RowMenu, ConfirmDialog } from "@jini-ai/admin/react";
|
|
5
|
-
import { agentHandle } from "@jini-ai/agentic";
|
|
6
|
-
import { buildAgentListHandles } from "@jini-ai/ui/panel-kit";
|
|
7
|
-
import { TabBar } from "@jini-ai/ui/tab-strip";
|
|
8
|
-
import { roleMenuItems, policyMenuItems } from "./rules.js";
|
|
9
|
-
import { useRoles } from "./hooks/use-roles.hooks.js";
|
|
10
|
-
import { resolveRolesTabId, resolveRolesTabs } from "./Roles.hooks.js";
|
|
11
|
-
export function RolesSection({ roles, create, row, t, translate }) {
|
|
12
|
-
const roleMenuHandles = buildAgentListHandles({ prefix: "roles-row", ids: roles.map((role) => role.id) });
|
|
13
|
-
return (_jsxs(_Fragment, { children: [_jsx("h2", { className: "visually-hidden", children: t("Roles") }), _jsxs("form", { onSubmit: create.submit, className: "notice integrations-form form-measure", children: [create.error ? _jsx("span", { className: "save-error", children: create.error }) : null, _jsxs("label", { children: [t("Role name"), _jsx("input", { value: create.name, onChange: (e) => create.setName(e.target.value), required: true, ...agentHandle({ handle: "roles-create-name" }, { role: "field", label: t("New role's name") }) })] }), _jsx("button", { type: "submit", disabled: create.saving || !create.name, ...agentHandle({ handle: "roles-create-submit" }, { role: "button", label: t("Create this new role") }), children: create.saving ? t("Creating…") : t("Create role") })] }), _jsx(DataTable, { rows: roles, rowKey: (role) => role.id, empty: _jsx("div", { className: "card", children: _jsx("div", { className: "empty-state", children: _jsx("p", { children: t("No roles yet.") }) }) }), columns: [
|
|
14
|
-
{
|
|
15
|
-
key: "name",
|
|
16
|
-
header: t("Name"),
|
|
17
|
-
cell: (role, index) => row.editingId === role.id ? (_jsx("input", { value: row.draftName, onChange: (e) => row.setDraftName(e.target.value), ...agentHandle({ handle: `${roleMenuHandles[index]}-rename-name` }, { role: "field", label: t("This role's new name") }) })) : (role.name),
|
|
18
|
-
},
|
|
19
|
-
{ key: "type", header: t("Type"), cell: (role) => (role.isBuiltin ? t("Built-in") : t("Custom")) },
|
|
20
|
-
{
|
|
21
|
-
key: "actions",
|
|
22
|
-
header: t("More"),
|
|
23
|
-
cell: (role, index) => role.isBuiltin ? (_jsx("span", { className: "muted-cell", children: "\u2014" })) : row.editingId === role.id ? (_jsxs("span", { className: "editor-actions", children: [_jsx("button", { type: "button", disabled: row.savingId === role.id, onClick: () => row.saveRename({ roleId: role.id }), ...agentHandle({ handle: `${roleMenuHandles[index]}-save` }, { role: "button", label: t(`Save this role's new name`) }), children: row.savingId === role.id ? t("Saving…") : t("Save") }), _jsx("button", { type: "button", onClick: () => row.setEditingId(null), ...agentHandle({ handle: `${roleMenuHandles[index]}-cancel` }, { role: "button", label: t("Cancel renaming this role") }), children: t("Cancel") })] })) : (_jsx(RowMenu, { triggerLabel: `${t("Actions for role")} "${role.name}"`, agentHandle: `${roleMenuHandles[index]}-menu`, items: roleMenuItems({ role, handlers: { onRename: row.startRename, onDelete: row.requestDelete }, translate }) })),
|
|
24
|
-
},
|
|
25
|
-
] })] }));
|
|
26
|
-
}
|
|
27
|
-
function PolicyRowActions({ policy, row, permission, agentBase, t, translate }) {
|
|
28
|
-
if (policy.isBuiltin || policy.isFrozen)
|
|
29
|
-
return _jsx("span", { className: "muted-cell", children: "\u2014" });
|
|
30
|
-
if (row.editingId === policy.id) {
|
|
31
|
-
return (_jsxs("span", { className: "editor-actions", children: [_jsx("button", { type: "button", disabled: row.savingId === policy.id, onClick: () => row.saveRename({ policyId: policy.id }), ...agentHandle({ handle: `${agentBase}-save` }, { role: "button", label: t("Save this policy's new name and description") }), children: row.savingId === policy.id ? t("Saving…") : t("Save") }), _jsx("button", { type: "button", onClick: () => row.setEditingId(null), ...agentHandle({ handle: `${agentBase}-cancel` }, { role: "button", label: t("Cancel renaming this policy") }), children: t("Cancel") })] }));
|
|
32
|
-
}
|
|
33
|
-
return (_jsx(RowMenu, { triggerLabel: `${t("Actions for policy")} "${policy.name}"`, agentHandle: `${agentBase}-menu`, items: policyMenuItems({
|
|
34
|
-
policy,
|
|
35
|
-
permissionPolicyId: permission.openForPolicyId,
|
|
36
|
-
handlers: {
|
|
37
|
-
onRename: row.startRename,
|
|
38
|
-
onTogglePermissionForm: permission.toggleForm,
|
|
39
|
-
onDelete: row.requestDelete,
|
|
40
|
-
},
|
|
41
|
-
translate,
|
|
42
|
-
}) }));
|
|
43
|
-
}
|
|
44
|
-
function PolicyPermissionList({ policyId, permission, t }) {
|
|
45
|
-
if (permission.loading)
|
|
46
|
-
return _jsx("p", { className: "muted-cell", children: t("Loading permissions…") });
|
|
47
|
-
if (permission.rows.length === 0)
|
|
48
|
-
return _jsx("p", { className: "muted-cell", children: t("No permissions yet.") });
|
|
49
|
-
const removeHandles = buildAgentListHandles({
|
|
50
|
-
prefix: `policy-permission-remove-${policyId}`,
|
|
51
|
-
ids: permission.rows.map((row) => row.id),
|
|
52
|
-
});
|
|
53
|
-
return (_jsx("ul", { className: "permission-list", children: permission.rows.map((row, index) => (_jsxs("li", { children: [_jsx("code", { children: row.permission }), row.resourceType ? _jsxs("span", { className: "muted-cell", children: [" (", row.resourceType, ")"] }) : null, _jsx("button", { type: "button", disabled: permission.removingId === row.id, "aria-label": `${t("Remove permission")} ${row.permission}`, onClick: () => permission.requestRemove({ policyId, row }), ...agentHandle({ handle: removeHandles[index] }, { role: "button", label: t(`Remove the "${row.permission}" permission from this policy`) }), children: permission.removingId === row.id ? t("Removing…") : t("Remove") })] }, row.id))) }));
|
|
54
|
-
}
|
|
55
|
-
function PolicyPermissionForm({ policyId, savingId, permission, t }) {
|
|
56
|
-
const formHandleBase = buildAgentListHandles({ prefix: "policy-permission-form", ids: [policyId] })[0];
|
|
57
|
-
return (_jsx("tr", { children: _jsx("td", { colSpan: 4, children: _jsxs("div", { className: "notice integrations-form", children: [_jsx("p", { children: t("Current permissions") }), _jsx(PolicyPermissionList, { policyId: policyId, permission: permission, t: t }), _jsxs("label", { children: [t("Permission"), _jsxs("span", { className: "editor-actions permission-form-fields", children: [_jsx("input", { value: permission.permission, onChange: (e) => permission.setPermission(e.target.value), placeholder: t("e.g. content.write"), ...agentHandle({ handle: `${formHandleBase}-permission` }, { role: "field", label: t("Permission string to grant, e.g. content.write") }) }), _jsx("input", { value: permission.resourceType, onChange: (e) => permission.setResourceType(e.target.value), placeholder: t("resource type (optional)"), ...agentHandle({ handle: `${formHandleBase}-resource-type` }, { role: "field", label: t("Optional resource type this permission is scoped to") }) }), _jsx("button", { type: "button", disabled: !permission.permission || savingId === policyId, onClick: () => permission.write({ policyId }), ...agentHandle({ handle: `${formHandleBase}-add` }, { role: "button", label: t("Add this permission to the policy") }), children: savingId === policyId ? t("Saving…") : t("Add") })] })] })] }) }) }));
|
|
58
|
-
}
|
|
59
|
-
export function PolicyRow({ policy, row, permission, agentBase, t, translate }) {
|
|
60
|
-
const renaming = row.editingId === policy.id;
|
|
61
|
-
return (_jsxs(_Fragment, { children: [_jsxs("tr", { children: [_jsx("td", { children: renaming ? (_jsx("input", { value: row.draftName, onChange: (e) => row.setDraftName(e.target.value), ...agentHandle({ handle: `${agentBase}-rename-name` }, { role: "field", label: t("This policy's new name") }) })) : (policy.name) }), _jsx("td", { children: renaming ? (_jsx("input", { value: row.draftDescription, onChange: (e) => row.setDraftDescription(e.target.value), ...agentHandle({ handle: `${agentBase}-rename-description` }, { role: "field", label: t("This policy's new description") }) })) : (policy.description ?? _jsx("span", { className: "muted-cell", children: "\u2014" })) }), _jsxs("td", { children: [policy.isBuiltin ? t("Built-in") : t("Custom"), policy.isFrozen ? ` ${t("(frozen)")}` : ""] }), _jsx("td", { children: _jsx(PolicyRowActions, { policy: policy, row: row, permission: permission, agentBase: agentBase, t: t, translate: translate }) })] }), permission.openForPolicyId === policy.id ? (_jsx(PolicyPermissionForm, { policyId: policy.id, savingId: row.savingId, permission: permission, t: t })) : null] }));
|
|
62
|
-
}
|
|
63
|
-
export function PoliciesSection({ policies, create, row, permission, t, translate }) {
|
|
64
|
-
const policyMenuBases = buildAgentListHandles({ prefix: "policies-row", ids: policies.map((policy) => policy.id) });
|
|
65
|
-
return (_jsxs(_Fragment, { children: [_jsx("h2", { className: "visually-hidden", children: t("Policies") }), _jsxs("form", { onSubmit: create.submit, className: "notice integrations-form form-measure", children: [create.error ? _jsx("span", { className: "save-error", children: create.error }) : null, _jsxs("label", { children: [t("Policy name"), _jsx("input", { value: create.name, onChange: (e) => create.setName(e.target.value), required: true, ...agentHandle({ handle: "policies-create-name" }, { role: "field", label: t("New policy's name") }) })] }), _jsxs("label", { children: [t("Description (optional)"), _jsx("input", { value: create.description, onChange: (e) => create.setDescription(e.target.value), ...agentHandle({ handle: "policies-create-description" }, { role: "field", label: t("New policy's optional description") }) })] }), _jsx("button", { type: "submit", disabled: create.saving || !create.name, ...agentHandle({ handle: "policies-create-submit" }, { role: "button", label: t("Create this new policy") }), children: create.saving ? t("Creating…") : t("Create policy") })] }), policies.length === 0 ? (_jsx("div", { className: "card", children: _jsx("div", { className: "empty-state", children: _jsx("p", { children: t("No policies yet.") }) }) })) : (_jsx("div", { className: "table-scroll", children: _jsxs("table", { className: "list-table", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: t("Name") }), _jsx("th", { children: t("Description") }), _jsx("th", { children: t("Type") }), _jsx("th", { children: t("More") })] }) }), _jsx("tbody", { children: policies.map((policy, index) => (_jsx(Fragment, { children: _jsx(PolicyRow, { policy: policy, row: row, permission: permission, agentBase: policyMenuBases[index], t: t, translate: translate }) }, policy.id))) })] }) }))] }));
|
|
66
|
-
}
|
|
67
|
-
function RoleDeleteDialog({ pendingRoleDelete, setPendingRoleDelete, rowSavingId, onDeleteRole, t, translate }) {
|
|
68
|
-
const { prefix, suffix } = roleDeleteBodyParts({ translate });
|
|
69
|
-
return (_jsx(ConfirmDialog, { cancelLabel: t("Cancel"), open: pendingRoleDelete !== null, agentHandle: "roles-delete-role", title: t("Delete role?"), body: pendingRoleDelete ? (_jsxs("p", { children: [prefix, pendingRoleDelete.name, suffix] })) : null, confirmLabel: t("Delete"), destructive: true, pending: pendingRoleDelete !== null && rowSavingId === pendingRoleDelete.id, onConfirm: onDeleteRole, onCancel: () => setPendingRoleDelete(null) }));
|
|
70
|
-
}
|
|
71
|
-
function PolicyDeleteDialog({ pendingPolicyDelete, setPendingPolicyDelete, rowSavingId, onDeletePolicy, t, translate }) {
|
|
72
|
-
const { prefix, suffix } = policyDeleteBodyParts({ translate });
|
|
73
|
-
return (_jsx(ConfirmDialog, { cancelLabel: t("Cancel"), open: pendingPolicyDelete !== null, agentHandle: "roles-delete-policy", title: t("Delete policy?"), body: pendingPolicyDelete ? (_jsxs("p", { children: [prefix, pendingPolicyDelete.name, suffix] })) : null, confirmLabel: t("Delete"), destructive: true, pending: pendingPolicyDelete !== null && rowSavingId === pendingPolicyDelete.id, onConfirm: onDeletePolicy, onCancel: () => setPendingPolicyDelete(null) }));
|
|
74
|
-
}
|
|
75
|
-
function PermissionRemoveDialog({ pendingPermissionRemove, setPendingPermissionRemove, removingPermissionId, onConfirmRemovePermission, t, translate, }) {
|
|
76
|
-
const { prefix, suffix } = permissionRemoveBodyParts({ translate });
|
|
77
|
-
const pending = pendingPermissionRemove;
|
|
78
|
-
return (_jsx(ConfirmDialog, { cancelLabel: t("Cancel"), open: pending !== null, agentHandle: "roles-remove-permission", title: t("Remove permission?"), body: pending ? (_jsxs("p", { children: [prefix, _jsx("code", { children: pending.row.permission }), pending.row.resourceType ? ` (${pending.row.resourceType})` : "", suffix] })) : null, confirmLabel: t("Remove"), tone: "warning", pending: pending !== null && removingPermissionId === pending.row.id, onConfirm: onConfirmRemovePermission, onCancel: () => setPendingPermissionRemove(null) }));
|
|
79
|
-
}
|
|
80
|
-
function RolesTab({ controller }) {
|
|
81
|
-
const c = controller;
|
|
82
|
-
return (_jsx(RolesSection, { roles: c.roles ?? [], create: {
|
|
83
|
-
name: c.roleName,
|
|
84
|
-
setName: c.setRoleName,
|
|
85
|
-
saving: c.roleSaving,
|
|
86
|
-
error: c.roleError,
|
|
87
|
-
submit: c.onCreateRole,
|
|
88
|
-
}, row: {
|
|
89
|
-
editingId: c.editingRoleId,
|
|
90
|
-
setEditingId: c.setEditingRoleId,
|
|
91
|
-
draftName: c.editingRoleName,
|
|
92
|
-
setDraftName: c.setEditingRoleName,
|
|
93
|
-
startRename: c.startEditRole,
|
|
94
|
-
saveRename: c.onSaveRole,
|
|
95
|
-
savingId: c.rowSavingId,
|
|
96
|
-
requestDelete: c.setPendingRoleDelete,
|
|
97
|
-
}, t: c.t, translate: c.translate }));
|
|
98
|
-
}
|
|
99
|
-
function PoliciesTab({ controller }) {
|
|
100
|
-
const c = controller;
|
|
101
|
-
return (_jsx(PoliciesSection, { policies: c.policies ?? [], create: {
|
|
102
|
-
name: c.policyName,
|
|
103
|
-
setName: c.setPolicyName,
|
|
104
|
-
description: c.policyDescription,
|
|
105
|
-
setDescription: c.setPolicyDescription,
|
|
106
|
-
saving: c.policySaving,
|
|
107
|
-
error: c.policyError,
|
|
108
|
-
submit: c.onCreatePolicy,
|
|
109
|
-
}, row: {
|
|
110
|
-
editingId: c.editingPolicyId,
|
|
111
|
-
setEditingId: c.setEditingPolicyId,
|
|
112
|
-
draftName: c.editingPolicyName,
|
|
113
|
-
setDraftName: c.setEditingPolicyName,
|
|
114
|
-
draftDescription: c.editingPolicyDescription,
|
|
115
|
-
setDraftDescription: c.setEditingPolicyDescription,
|
|
116
|
-
startRename: c.startEditPolicy,
|
|
117
|
-
saveRename: c.onSavePolicy,
|
|
118
|
-
savingId: c.rowSavingId,
|
|
119
|
-
requestDelete: c.setPendingPolicyDelete,
|
|
120
|
-
}, permission: {
|
|
121
|
-
openForPolicyId: c.permissionPolicyId,
|
|
122
|
-
permission: c.permissionInput,
|
|
123
|
-
setPermission: c.setPermissionInput,
|
|
124
|
-
resourceType: c.resourceTypeInput,
|
|
125
|
-
setResourceType: c.setResourceTypeInput,
|
|
126
|
-
toggleForm: c.togglePermissionForm,
|
|
127
|
-
write: c.onWritePermission,
|
|
128
|
-
rows: c.permissionRows,
|
|
129
|
-
loading: c.permissionsLoading,
|
|
130
|
-
removingId: c.removingPermissionId,
|
|
131
|
-
requestRemove: c.setPendingPermissionRemove,
|
|
132
|
-
}, t: c.t, translate: c.translate }));
|
|
133
|
-
}
|
|
134
|
-
function rolesTabPanel(activeTabId, controller) {
|
|
135
|
-
if (activeTabId === "policies")
|
|
136
|
-
return _jsx(PoliciesTab, { controller: controller });
|
|
137
|
-
return _jsx(RolesTab, { controller: controller });
|
|
138
|
-
}
|
|
139
|
-
export function Roles({ tabId, useRolesHook = useRoles, port, translate: injectedTranslate, queryScope, onTabChange, usersHref }) {
|
|
140
|
-
const controller = useRolesHook({ port, translate: injectedTranslate, queryScope });
|
|
141
|
-
const { roles, policies, error, rowError, rowSavingId, pendingRoleDelete, setPendingRoleDelete, onDeleteRole, pendingPolicyDelete, setPendingPolicyDelete, onDeletePolicy, pendingPermissionRemove, setPendingPermissionRemove, removingPermissionId, onConfirmRemovePermission, t, translate, } = controller;
|
|
142
|
-
const { prefix: descriptionPrefix, linkLabel: descriptionLinkLabel, suffix: descriptionSuffix } = rolesDescriptionParts({ translate });
|
|
143
|
-
if (error)
|
|
144
|
-
return _jsx("div", { className: "notice error", children: error });
|
|
145
|
-
if (!roles || !policies)
|
|
146
|
-
return _jsx("div", { className: "notice", children: t("Loading roles & permissions…") });
|
|
147
|
-
const activeTabId = resolveRolesTabId({ tabId });
|
|
148
|
-
return (_jsxs("div", { className: "page", children: [_jsx("div", { className: "page-header", children: _jsxs("div", { className: "page-header-text", children: [_jsx("p", { className: "page-kicker", children: t("People") }), _jsx("h1", { className: "page-title", children: t("Roles & Permissions") }), _jsxs("p", { className: "page-description", children: [descriptionPrefix, _jsx("a", { href: usersHref, children: descriptionLinkLabel }), descriptionSuffix] })] }) }), rowError ? _jsx("div", { className: "notice error", children: rowError }) : null, _jsx(TabBar, { ariaLabel: t("Roles & Permissions"), tabs: resolveRolesTabs({ translate: t }), activeId: activeTabId, onChange: (next) => onTabChange({ tabId: resolveRolesTabId({ tabId: next }) }), containerHandle: "roles-tab-bar" }), rolesTabPanel(activeTabId, controller), _jsx(RoleDeleteDialog, { pendingRoleDelete: pendingRoleDelete, setPendingRoleDelete: setPendingRoleDelete, rowSavingId: rowSavingId, t: t, translate: translate, onDeleteRole: onDeleteRole }), _jsx(PolicyDeleteDialog, { pendingPolicyDelete: pendingPolicyDelete, setPendingPolicyDelete: setPendingPolicyDelete, rowSavingId: rowSavingId, t: t, translate: translate, onDeletePolicy: onDeletePolicy }), _jsx(PermissionRemoveDialog, { pendingPermissionRemove: pendingPermissionRemove, setPendingPermissionRemove: setPendingPermissionRemove, removingPermissionId: removingPermissionId, onConfirmRemovePermission: onConfirmRemovePermission, t: t, translate: translate })] }));
|
|
149
|
-
}
|
|
150
|
-
//# sourceMappingURL=Roles.js.map
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import type { Translate } from "@jini-ai/ui/panel-kit";
|
|
2
|
-
import { type AdminPolicy, type AdminPolicyPermission, type AdminRole } from "../../models.js";
|
|
3
|
-
import type { RolesPort } from "../../ports.js";
|
|
4
|
-
export interface PendingPermissionRemove {
|
|
5
|
-
policyId: string;
|
|
6
|
-
row: AdminPolicyPermission;
|
|
7
|
-
}
|
|
8
|
-
export interface RolesController {
|
|
9
|
-
roles: AdminRole[] | null;
|
|
10
|
-
policies: AdminPolicy[] | null;
|
|
11
|
-
error: string | null;
|
|
12
|
-
rowError: string | null;
|
|
13
|
-
roleName: string;
|
|
14
|
-
setRoleName: (name: string) => void;
|
|
15
|
-
roleSaving: boolean;
|
|
16
|
-
roleError: string | null;
|
|
17
|
-
onCreateRole: (e: React.FormEvent) => Promise<void>;
|
|
18
|
-
policyName: string;
|
|
19
|
-
setPolicyName: (name: string) => void;
|
|
20
|
-
policyDescription: string;
|
|
21
|
-
setPolicyDescription: (description: string) => void;
|
|
22
|
-
policySaving: boolean;
|
|
23
|
-
policyError: string | null;
|
|
24
|
-
onCreatePolicy: (e: React.FormEvent) => Promise<void>;
|
|
25
|
-
editingRoleId: string | null;
|
|
26
|
-
setEditingRoleId: (roleId: string | null) => void;
|
|
27
|
-
editingRoleName: string;
|
|
28
|
-
setEditingRoleName: (name: string) => void;
|
|
29
|
-
startEditRole: (role: AdminRole) => void;
|
|
30
|
-
onSaveRole: (required: {
|
|
31
|
-
roleId: string;
|
|
32
|
-
}) => Promise<void>;
|
|
33
|
-
editingPolicyId: string | null;
|
|
34
|
-
setEditingPolicyId: (policyId: string | null) => void;
|
|
35
|
-
editingPolicyName: string;
|
|
36
|
-
setEditingPolicyName: (name: string) => void;
|
|
37
|
-
editingPolicyDescription: string;
|
|
38
|
-
setEditingPolicyDescription: (description: string) => void;
|
|
39
|
-
startEditPolicy: (policy: AdminPolicy) => void;
|
|
40
|
-
onSavePolicy: (required: {
|
|
41
|
-
policyId: string;
|
|
42
|
-
}) => Promise<void>;
|
|
43
|
-
rowSavingId: string | null;
|
|
44
|
-
permissionPolicyId: string | null;
|
|
45
|
-
permissionInput: string;
|
|
46
|
-
setPermissionInput: (permission: string) => void;
|
|
47
|
-
resourceTypeInput: string;
|
|
48
|
-
setResourceTypeInput: (resourceType: string) => void;
|
|
49
|
-
togglePermissionForm: (required: {
|
|
50
|
-
policyId: string;
|
|
51
|
-
}) => void;
|
|
52
|
-
onWritePermission: (required: {
|
|
53
|
-
policyId: string;
|
|
54
|
-
}) => Promise<void>;
|
|
55
|
-
permissionRows: AdminPolicyPermission[];
|
|
56
|
-
permissionsLoading: boolean;
|
|
57
|
-
removingPermissionId: string | null;
|
|
58
|
-
onRemovePermission: (required: {
|
|
59
|
-
policyId: string;
|
|
60
|
-
policyPermissionId: string;
|
|
61
|
-
}) => Promise<void>;
|
|
62
|
-
pendingPermissionRemove: PendingPermissionRemove | null;
|
|
63
|
-
setPendingPermissionRemove: (target: PendingPermissionRemove | null) => void;
|
|
64
|
-
onConfirmRemovePermission: () => Promise<void>;
|
|
65
|
-
pendingRoleDelete: AdminRole | null;
|
|
66
|
-
setPendingRoleDelete: (role: AdminRole | null) => void;
|
|
67
|
-
onDeleteRole: () => Promise<void>;
|
|
68
|
-
pendingPolicyDelete: AdminPolicy | null;
|
|
69
|
-
setPendingPolicyDelete: (policy: AdminPolicy | null) => void;
|
|
70
|
-
onDeletePolicy: () => Promise<void>;
|
|
71
|
-
t: (key: string) => string;
|
|
72
|
-
translate: Translate;
|
|
73
|
-
}
|
|
74
|
-
export interface RolesDependencies {
|
|
75
|
-
port: RolesPort;
|
|
76
|
-
translate: Translate;
|
|
77
|
-
queryScope: string;
|
|
78
|
-
}
|
|
79
|
-
export declare function useRoles(deps: RolesDependencies): RolesController;
|
|
80
|
-
//# sourceMappingURL=use-roles.hooks.d.ts.map
|
|
@@ -1,303 +0,0 @@
|
|
|
1
|
-
import { useRef, useState } from "react";
|
|
2
|
-
import {} from "../../models.js";
|
|
3
|
-
import { useFetchMutation, useFetchQuery } from "@jini-ai/ui/panel-kit";
|
|
4
|
-
import { describeApiError as describeFeatureError, KEYS } from "../rules.js";
|
|
5
|
-
async function runRowDelete(id, mutate, setRowSavingId, setRowError, clearPending, describeError) {
|
|
6
|
-
setRowSavingId(id);
|
|
7
|
-
setRowError(null);
|
|
8
|
-
try {
|
|
9
|
-
await mutate(id);
|
|
10
|
-
}
|
|
11
|
-
catch (e) {
|
|
12
|
-
setRowError(describeError(e));
|
|
13
|
-
}
|
|
14
|
-
finally {
|
|
15
|
-
setRowSavingId((current) => (current === id ? null : current));
|
|
16
|
-
clearPending();
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
export function useRoles(deps) {
|
|
20
|
-
const { port, translate, queryScope } = deps;
|
|
21
|
-
const describeApiError = (error, fallback) => describeFeatureError({ error, fallback, translate });
|
|
22
|
-
const t = translate;
|
|
23
|
-
const boundT = translate;
|
|
24
|
-
const list = useFetchQuery({
|
|
25
|
-
key: [queryScope, ...KEYS.list],
|
|
26
|
-
fetch: async () => {
|
|
27
|
-
const [r, p] = await Promise.all([port.listRoles({}), port.listPolicies({})]);
|
|
28
|
-
return { roles: r.roles, policies: p.policies };
|
|
29
|
-
},
|
|
30
|
-
});
|
|
31
|
-
const roles = list.data?.roles ?? null;
|
|
32
|
-
const policies = list.data?.policies ?? null;
|
|
33
|
-
const error = list.error ? describeApiError(list.error, t("failed to load roles/policies")) : null;
|
|
34
|
-
const [rowError, setRowError] = useState(null);
|
|
35
|
-
const [roleName, setRoleName] = useState("");
|
|
36
|
-
const [policyName, setPolicyName] = useState("");
|
|
37
|
-
const [policyDescription, setPolicyDescription] = useState("");
|
|
38
|
-
const [editingRoleId, setEditingRoleId] = useState(null);
|
|
39
|
-
const [editingRoleName, setEditingRoleName] = useState("");
|
|
40
|
-
const [editingPolicyId, setEditingPolicyId] = useState(null);
|
|
41
|
-
const [editingPolicyName, setEditingPolicyName] = useState("");
|
|
42
|
-
const [editingPolicyDescription, setEditingPolicyDescription] = useState("");
|
|
43
|
-
const [rowSavingId, setRowSavingId] = useState(null);
|
|
44
|
-
const [permissionPolicyId, setPermissionPolicyId] = useState(null);
|
|
45
|
-
const [permissionInput, setPermissionInput] = useState("");
|
|
46
|
-
const [resourceTypeInput, setResourceTypeInput] = useState("");
|
|
47
|
-
const [permissionRows, setPermissionRows] = useState([]);
|
|
48
|
-
const [permissionsLoading, setPermissionsLoading] = useState(false);
|
|
49
|
-
const [removingPermissionId, setRemovingPermissionId] = useState(null);
|
|
50
|
-
const permissionsGenerationRef = useRef(0);
|
|
51
|
-
const permissionPolicyIdRef = useRef(null);
|
|
52
|
-
const [pendingRoleDelete, setPendingRoleDelete] = useState(null);
|
|
53
|
-
const [pendingPolicyDelete, setPendingPolicyDelete] = useState(null);
|
|
54
|
-
const [pendingPermissionRemove, setPendingPermissionRemove] = useState(null);
|
|
55
|
-
const createRoleMutation = useFetchMutation({
|
|
56
|
-
run: ({ input: name }) => port.createRole({ name: name })
|
|
57
|
-
}, {
|
|
58
|
-
invalidates: [[queryScope, ...KEYS.list]],
|
|
59
|
-
});
|
|
60
|
-
const createPolicyMutation = useFetchMutation({
|
|
61
|
-
run: ({ input }) => port.createPolicy({ name: input.name }, { description: input.description })
|
|
62
|
-
}, {
|
|
63
|
-
invalidates: [[queryScope, ...KEYS.list]],
|
|
64
|
-
});
|
|
65
|
-
const saveRoleMutation = useFetchMutation({
|
|
66
|
-
run: ({ input }) => port.updateRole(input)
|
|
67
|
-
}, {
|
|
68
|
-
invalidates: [[queryScope, ...KEYS.list]],
|
|
69
|
-
});
|
|
70
|
-
const deleteRoleMutation = useFetchMutation({
|
|
71
|
-
run: ({ input: id }) => port.deleteRole({ roleId: id })
|
|
72
|
-
}, {
|
|
73
|
-
invalidates: [[queryScope, ...KEYS.list]],
|
|
74
|
-
});
|
|
75
|
-
const savePolicyMutation = useFetchMutation({
|
|
76
|
-
run: ({ input }) => port.updatePolicy({ policyId: input.policyId }, { name: input.name, description: input.description })
|
|
77
|
-
}, {
|
|
78
|
-
invalidates: [[queryScope, ...KEYS.list]],
|
|
79
|
-
});
|
|
80
|
-
const deletePolicyMutation = useFetchMutation({
|
|
81
|
-
run: ({ input: id }) => port.deletePolicy({ policyId: id })
|
|
82
|
-
}, {
|
|
83
|
-
invalidates: [[queryScope, ...KEYS.list]],
|
|
84
|
-
});
|
|
85
|
-
const writePermissionMutation = useFetchMutation({
|
|
86
|
-
run: ({ input }) => port.writePolicyPermission({ policyId: input.policyId, permission: input.permission }, { resourceType: input.resourceType }),
|
|
87
|
-
});
|
|
88
|
-
const removePermissionMutation = useFetchMutation({
|
|
89
|
-
run: ({ input }) => port.removePolicyPermission(input),
|
|
90
|
-
});
|
|
91
|
-
async function onCreateRole(e) {
|
|
92
|
-
e.preventDefault();
|
|
93
|
-
try {
|
|
94
|
-
await createRoleMutation.mutate({ input: roleName });
|
|
95
|
-
setRoleName("");
|
|
96
|
-
}
|
|
97
|
-
catch {
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
async function onCreatePolicy(e) {
|
|
101
|
-
e.preventDefault();
|
|
102
|
-
try {
|
|
103
|
-
await createPolicyMutation.mutate({ input: { name: policyName, description: policyDescription || undefined } });
|
|
104
|
-
setPolicyName("");
|
|
105
|
-
setPolicyDescription("");
|
|
106
|
-
}
|
|
107
|
-
catch {
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
function startEditRole(role) {
|
|
111
|
-
setRowError(null);
|
|
112
|
-
setEditingRoleId(role.id);
|
|
113
|
-
setEditingRoleName(role.name);
|
|
114
|
-
}
|
|
115
|
-
async function onSaveRole({ roleId }) {
|
|
116
|
-
setRowSavingId(roleId);
|
|
117
|
-
setRowError(null);
|
|
118
|
-
try {
|
|
119
|
-
await saveRoleMutation.mutate({ input: { roleId, name: editingRoleName } });
|
|
120
|
-
setEditingRoleId((current) => (current === roleId ? null : current));
|
|
121
|
-
}
|
|
122
|
-
catch (e) {
|
|
123
|
-
setRowError(describeApiError(e, t("failed to rename role")));
|
|
124
|
-
}
|
|
125
|
-
finally {
|
|
126
|
-
setRowSavingId((current) => (current === roleId ? null : current));
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
async function onDeleteRole() {
|
|
130
|
-
if (!pendingRoleDelete)
|
|
131
|
-
return;
|
|
132
|
-
const role = pendingRoleDelete;
|
|
133
|
-
await runRowDelete(role.id, (id) => deleteRoleMutation.mutate({ input: id }), setRowSavingId, setRowError, () => setPendingRoleDelete((current) => (current?.id === role.id ? null : current)), (e) => describeApiError(e, t("failed to delete role")));
|
|
134
|
-
}
|
|
135
|
-
function startEditPolicy(policy) {
|
|
136
|
-
setRowError(null);
|
|
137
|
-
setEditingPolicyId(policy.id);
|
|
138
|
-
setEditingPolicyName(policy.name);
|
|
139
|
-
setEditingPolicyDescription(policy.description ?? "");
|
|
140
|
-
}
|
|
141
|
-
async function onSavePolicy({ policyId }) {
|
|
142
|
-
setRowSavingId(policyId);
|
|
143
|
-
setRowError(null);
|
|
144
|
-
try {
|
|
145
|
-
await savePolicyMutation.mutate({ input: { policyId, name: editingPolicyName, description: editingPolicyDescription } });
|
|
146
|
-
setEditingPolicyId((current) => (current === policyId ? null : current));
|
|
147
|
-
}
|
|
148
|
-
catch (e) {
|
|
149
|
-
setRowError(describeApiError(e, t("failed to update policy")));
|
|
150
|
-
}
|
|
151
|
-
finally {
|
|
152
|
-
setRowSavingId((current) => (current === policyId ? null : current));
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
async function onDeletePolicy() {
|
|
156
|
-
if (!pendingPolicyDelete)
|
|
157
|
-
return;
|
|
158
|
-
const policy = pendingPolicyDelete;
|
|
159
|
-
await runRowDelete(policy.id, (id) => deletePolicyMutation.mutate({ input: id }), setRowSavingId, setRowError, () => setPendingPolicyDelete((current) => (current?.id === policy.id ? null : current)), (e) => describeApiError(e, t("failed to delete policy")));
|
|
160
|
-
}
|
|
161
|
-
async function loadPermissions(policyId) {
|
|
162
|
-
const generation = ++permissionsGenerationRef.current;
|
|
163
|
-
setPermissionsLoading(true);
|
|
164
|
-
try {
|
|
165
|
-
const { policyPermissions } = await port.listPolicyPermissions({ policyId: policyId });
|
|
166
|
-
if (permissionsGenerationRef.current !== generation)
|
|
167
|
-
return;
|
|
168
|
-
setPermissionRows(policyPermissions);
|
|
169
|
-
}
|
|
170
|
-
catch (e) {
|
|
171
|
-
if (permissionsGenerationRef.current !== generation)
|
|
172
|
-
return;
|
|
173
|
-
setPermissionRows([]);
|
|
174
|
-
setRowError(describeApiError(e, t("failed to load permissions")));
|
|
175
|
-
}
|
|
176
|
-
finally {
|
|
177
|
-
if (permissionsGenerationRef.current !== generation)
|
|
178
|
-
return;
|
|
179
|
-
setPermissionsLoading(false);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
function togglePermissionForm({ policyId }) {
|
|
183
|
-
setRowError(null);
|
|
184
|
-
setPermissionInput("");
|
|
185
|
-
setResourceTypeInput("");
|
|
186
|
-
const closing = permissionPolicyIdRef.current === policyId;
|
|
187
|
-
permissionPolicyIdRef.current = closing ? null : policyId;
|
|
188
|
-
setPermissionPolicyId(permissionPolicyIdRef.current);
|
|
189
|
-
setPermissionRows([]);
|
|
190
|
-
if (!closing)
|
|
191
|
-
void loadPermissions(policyId);
|
|
192
|
-
}
|
|
193
|
-
async function onRemovePermission({ policyId, policyPermissionId }) {
|
|
194
|
-
setRemovingPermissionId(policyPermissionId);
|
|
195
|
-
setRowError(null);
|
|
196
|
-
try {
|
|
197
|
-
await removePermissionMutation.mutate({ input: { policyId, policyPermissionId } });
|
|
198
|
-
if (permissionPolicyIdRef.current === policyId)
|
|
199
|
-
await loadPermissions(policyId);
|
|
200
|
-
}
|
|
201
|
-
catch (e) {
|
|
202
|
-
setRowError(describeApiError(e, t("failed to remove permission")));
|
|
203
|
-
}
|
|
204
|
-
finally {
|
|
205
|
-
setRemovingPermissionId((current) => (current === policyPermissionId ? null : current));
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
async function onConfirmRemovePermission() {
|
|
209
|
-
if (!pendingPermissionRemove)
|
|
210
|
-
return;
|
|
211
|
-
const target = pendingPermissionRemove;
|
|
212
|
-
try {
|
|
213
|
-
await onRemovePermission({ policyId: target.policyId, policyPermissionId: target.row.id });
|
|
214
|
-
}
|
|
215
|
-
finally {
|
|
216
|
-
setPendingPermissionRemove((current) => (current?.row.id === target.row.id ? null : current));
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
async function onWritePermission({ policyId }) {
|
|
220
|
-
if (!permissionInput)
|
|
221
|
-
return;
|
|
222
|
-
const generationAtStart = permissionsGenerationRef.current;
|
|
223
|
-
setRowSavingId(policyId);
|
|
224
|
-
setRowError(null);
|
|
225
|
-
try {
|
|
226
|
-
await writePermissionMutation.mutate({ input: { policyId, permission: permissionInput, resourceType: resourceTypeInput || undefined } });
|
|
227
|
-
if (permissionsGenerationRef.current === generationAtStart) {
|
|
228
|
-
setPermissionInput("");
|
|
229
|
-
setResourceTypeInput("");
|
|
230
|
-
}
|
|
231
|
-
if (permissionPolicyIdRef.current === policyId)
|
|
232
|
-
await loadPermissions(policyId);
|
|
233
|
-
}
|
|
234
|
-
catch (e) {
|
|
235
|
-
setRowError(describeApiError(e, t("failed to add permission")));
|
|
236
|
-
}
|
|
237
|
-
finally {
|
|
238
|
-
setRowSavingId((current) => (current === policyId ? null : current));
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
const roleSaving = createRoleMutation.status === "pending";
|
|
242
|
-
const roleError = createRoleMutation.error ? describeApiError(createRoleMutation.error, t("failed to create role")) : null;
|
|
243
|
-
const policySaving = createPolicyMutation.status === "pending";
|
|
244
|
-
const policyError = createPolicyMutation.error
|
|
245
|
-
? describeApiError(createPolicyMutation.error, t("failed to create policy"))
|
|
246
|
-
: null;
|
|
247
|
-
return {
|
|
248
|
-
roles,
|
|
249
|
-
policies,
|
|
250
|
-
error,
|
|
251
|
-
rowError,
|
|
252
|
-
roleName,
|
|
253
|
-
setRoleName,
|
|
254
|
-
roleSaving,
|
|
255
|
-
roleError,
|
|
256
|
-
onCreateRole,
|
|
257
|
-
policyName,
|
|
258
|
-
setPolicyName,
|
|
259
|
-
policyDescription,
|
|
260
|
-
setPolicyDescription,
|
|
261
|
-
policySaving,
|
|
262
|
-
policyError,
|
|
263
|
-
onCreatePolicy,
|
|
264
|
-
editingRoleId,
|
|
265
|
-
setEditingRoleId,
|
|
266
|
-
editingRoleName,
|
|
267
|
-
setEditingRoleName,
|
|
268
|
-
startEditRole,
|
|
269
|
-
onSaveRole,
|
|
270
|
-
editingPolicyId,
|
|
271
|
-
setEditingPolicyId,
|
|
272
|
-
editingPolicyName,
|
|
273
|
-
setEditingPolicyName,
|
|
274
|
-
editingPolicyDescription,
|
|
275
|
-
setEditingPolicyDescription,
|
|
276
|
-
startEditPolicy,
|
|
277
|
-
onSavePolicy,
|
|
278
|
-
rowSavingId,
|
|
279
|
-
permissionPolicyId,
|
|
280
|
-
permissionInput,
|
|
281
|
-
setPermissionInput,
|
|
282
|
-
resourceTypeInput,
|
|
283
|
-
setResourceTypeInput,
|
|
284
|
-
togglePermissionForm,
|
|
285
|
-
onWritePermission,
|
|
286
|
-
permissionRows,
|
|
287
|
-
permissionsLoading,
|
|
288
|
-
removingPermissionId,
|
|
289
|
-
onRemovePermission,
|
|
290
|
-
pendingPermissionRemove,
|
|
291
|
-
setPendingPermissionRemove,
|
|
292
|
-
onConfirmRemovePermission,
|
|
293
|
-
pendingRoleDelete,
|
|
294
|
-
setPendingRoleDelete,
|
|
295
|
-
onDeleteRole,
|
|
296
|
-
pendingPolicyDelete,
|
|
297
|
-
setPendingPolicyDelete,
|
|
298
|
-
onDeletePolicy,
|
|
299
|
-
t: boundT,
|
|
300
|
-
translate,
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
//# sourceMappingURL=use-roles.hooks.js.map
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import type { AdminRole, AdminPolicy } from "../models.js";
|
|
2
|
-
import type { Translate } from "@jini-ai/ui/panel-kit";
|
|
3
|
-
import type { RowMenuItem } from "@jini-ai/admin/react";
|
|
4
|
-
import type { QueryKey } from "@jini-ai/ui/panel-kit";
|
|
5
|
-
export declare const KEYS: {
|
|
6
|
-
list: QueryKey;
|
|
7
|
-
};
|
|
8
|
-
export declare function describeApiError({ error, fallback, translate }: {
|
|
9
|
-
error: unknown;
|
|
10
|
-
fallback: string;
|
|
11
|
-
translate: Translate;
|
|
12
|
-
}): string;
|
|
13
|
-
export interface RoleRowMenuHandlers {
|
|
14
|
-
onRename: (role: AdminRole) => void;
|
|
15
|
-
onDelete: (role: AdminRole) => void;
|
|
16
|
-
}
|
|
17
|
-
export declare function roleMenuItems({ role, handlers, translate }: {
|
|
18
|
-
role: AdminRole;
|
|
19
|
-
handlers: RoleRowMenuHandlers;
|
|
20
|
-
translate: Translate;
|
|
21
|
-
}): RowMenuItem[];
|
|
22
|
-
export interface PolicyRowMenuHandlers {
|
|
23
|
-
onRename: (policy: AdminPolicy) => void;
|
|
24
|
-
onTogglePermissionForm: (required: {
|
|
25
|
-
policyId: string;
|
|
26
|
-
}) => void;
|
|
27
|
-
onDelete: (policy: AdminPolicy) => void;
|
|
28
|
-
}
|
|
29
|
-
export declare function policyMenuItems({ policy, permissionPolicyId, handlers, translate }: {
|
|
30
|
-
policy: AdminPolicy;
|
|
31
|
-
permissionPolicyId: string | null;
|
|
32
|
-
handlers: PolicyRowMenuHandlers;
|
|
33
|
-
translate: Translate;
|
|
34
|
-
}): RowMenuItem[];
|
|
35
|
-
//# sourceMappingURL=rules.d.ts.map
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { describeIdentityError } from "../errors.js";
|
|
2
|
-
export const KEYS = {
|
|
3
|
-
list: ["roles-and-policies"],
|
|
4
|
-
};
|
|
5
|
-
const STATIC_ERROR_MESSAGES = {
|
|
6
|
-
FORBIDDEN: "You do not have permission to do that.",
|
|
7
|
-
RESOURCE_CONFLICT: "It is still in use — remove that assignment/attachment first.",
|
|
8
|
-
PERMISSION_UNKNOWN: "That permission is not recognized.",
|
|
9
|
-
GRANT_EXCEEDS_ISSUER: "You cannot grant a permission you do not hold.",
|
|
10
|
-
};
|
|
11
|
-
export function describeApiError({ error, fallback, translate }) {
|
|
12
|
-
return describeIdentityError({ error, fallback, translate }, { messages: STATIC_ERROR_MESSAGES });
|
|
13
|
-
}
|
|
14
|
-
export function roleMenuItems({ role, handlers, translate }) {
|
|
15
|
-
return [
|
|
16
|
-
{ key: "rename", label: translate("Rename"), onSelect: () => handlers.onRename(role) },
|
|
17
|
-
{ key: "delete", label: translate("Delete"), destructive: true, onSelect: () => handlers.onDelete(role) },
|
|
18
|
-
];
|
|
19
|
-
}
|
|
20
|
-
export function policyMenuItems({ policy, permissionPolicyId, handlers, translate }) {
|
|
21
|
-
return [
|
|
22
|
-
{ key: "rename", label: translate("Rename"), onSelect: () => handlers.onRename(policy) },
|
|
23
|
-
{
|
|
24
|
-
key: "permission",
|
|
25
|
-
label: permissionPolicyId === policy.id ? translate("Close") : translate("Add permission"),
|
|
26
|
-
onSelect: () => handlers.onTogglePermissionForm({ policyId: policy.id }),
|
|
27
|
-
},
|
|
28
|
-
{ key: "delete", label: translate("Delete"), destructive: true, onSelect: () => handlers.onDelete(policy) },
|
|
29
|
-
];
|
|
30
|
-
}
|
|
31
|
-
//# sourceMappingURL=rules.js.map
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { type Translate } from "@jini-ai/ui/panel-kit";
|
|
2
|
-
export declare function rolesDescriptionParts({ translate }: {
|
|
3
|
-
translate: Translate;
|
|
4
|
-
}): {
|
|
5
|
-
prefix: string;
|
|
6
|
-
suffix: string;
|
|
7
|
-
linkLabel: string;
|
|
8
|
-
};
|
|
9
|
-
export declare function roleDeleteBodyParts({ translate }: {
|
|
10
|
-
translate: Translate;
|
|
11
|
-
}): {
|
|
12
|
-
prefix: string;
|
|
13
|
-
suffix: string;
|
|
14
|
-
};
|
|
15
|
-
export declare function policyDeleteBodyParts({ translate }: {
|
|
16
|
-
translate: Translate;
|
|
17
|
-
}): {
|
|
18
|
-
prefix: string;
|
|
19
|
-
suffix: string;
|
|
20
|
-
};
|
|
21
|
-
export declare function permissionRemoveBodyParts({ translate }: {
|
|
22
|
-
translate: Translate;
|
|
23
|
-
}): {
|
|
24
|
-
prefix: string;
|
|
25
|
-
suffix: string;
|
|
26
|
-
};
|
|
27
|
-
//# sourceMappingURL=text.d.ts.map
|
package/dist/react/roles/text.js
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { splitOnPlaceholders } from "@jini-ai/ui/panel-kit";
|
|
2
|
-
export function rolesDescriptionParts({ translate }) {
|
|
3
|
-
const [prefix = "", suffix = ""] = splitOnPlaceholders({ template: translate("Roles and policies grant access to operator users. Assign a role or policy to a specific user from the {users} screen."), tokens: ["{users}"] });
|
|
4
|
-
return { prefix, suffix, linkLabel: translate("Users") };
|
|
5
|
-
}
|
|
6
|
-
function nameParts(translate, template) {
|
|
7
|
-
const [prefix = "", suffix = ""] = splitOnPlaceholders({ template: translate(template), tokens: ["{name}"] });
|
|
8
|
-
return { prefix, suffix };
|
|
9
|
-
}
|
|
10
|
-
export function roleDeleteBodyParts({ translate }) { return nameParts(translate, 'Delete role "{name}"?'); }
|
|
11
|
-
export function policyDeleteBodyParts({ translate }) { return nameParts(translate, 'Delete policy "{name}"?'); }
|
|
12
|
-
export function permissionRemoveBodyParts({ translate }) { return nameParts(translate, 'Remove "{name}" from this policy? Anyone with this policy loses it.'); }
|
|
13
|
-
//# sourceMappingURL=text.js.map
|