@happyvertical/smrt-users 0.51.9 → 0.51.10
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/AGENTS.md +8 -0
- package/README.md +96 -1
- package/dist/chunks/{TerminalAuthService-BfZmKdZW.js → TerminalAuthService-BlDTirYc.js} +197 -8
- package/dist/chunks/TerminalAuthService-BlDTirYc.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/services/AncestorReadPolicy.d.ts +143 -0
- package/dist/services/AncestorReadPolicy.d.ts.map +1 -0
- package/dist/services/PermissionResolver.d.ts +105 -2
- package/dist/services/PermissionResolver.d.ts.map +1 -1
- package/dist/services/index.d.ts +2 -1
- package/dist/services/index.d.ts.map +1 -1
- package/dist/smrt-knowledge.json +7 -7
- package/dist/sveltekit.js +1 -1
- package/package.json +9 -9
- package/dist/chunks/TerminalAuthService-BfZmKdZW.js.map +0 -1
package/AGENTS.md
CHANGED
|
@@ -27,6 +27,14 @@ are not prerequisites for unrelated user-package work.
|
|
|
27
27
|
through `RoleCollection.seedSystemRoles()` at application initialization.
|
|
28
28
|
- Group roles apply only in their own tenant. Use `getGroupIdsForTenant()`,
|
|
29
29
|
never cross-tenant `getGroupIds()`, for authorization.
|
|
30
|
+
- Upward visibility is opt-in and read-only: `permissions.ancestorRead`
|
|
31
|
+
({ roles, collections, maxDepth? }) lets a DESCENDANT membership contribute
|
|
32
|
+
declared `<collection>.read` at an ancestor, only when no membership resolved
|
|
33
|
+
there, intersected with BOTH the role's own catalog grants and the
|
|
34
|
+
principal's effective permissions in that descendant, and only for SYSTEM
|
|
35
|
+
roles (a tenant-scoped role sharing a declared slug is ignored). Off by default, never
|
|
36
|
+
write, never lateral, and never row visibility — sibling rows stay scoped by
|
|
37
|
+
tenancy/RLS. Read the permissions module before changing it.
|
|
30
38
|
- Membership DENY always wins. Direct inactive membership blocks inherited
|
|
31
39
|
authority; a direct active membership pins resolution instead of unioning it
|
|
32
40
|
with ancestors. Read the permissions module before changing these rules.
|
package/README.md
CHANGED
|
@@ -646,6 +646,100 @@ returned by `resolvePermissions()` and `SessionService.loadSessionContext()`.
|
|
|
646
646
|
|
|
647
647
|
Tenants support parent-child trees (max depth 10). Two flags control inheritance: `cascadePermissions` (parent pushes down) and `inheritPermissions` (child accepts). Both must be true for permissions to flow.
|
|
648
648
|
|
|
649
|
+
### Read-only ancestor visibility (opt-in, off by default)
|
|
650
|
+
|
|
651
|
+
Membership authority travels DOWN the hierarchy: a direct membership in the
|
|
652
|
+
tenant being resolved, or — per role, via `inheritsToDescendants` — the nearest
|
|
653
|
+
active ancestor membership. Nothing a user holds on a DESCENDANT contributes
|
|
654
|
+
anything at an ancestor, so a principal whose only membership is on a child
|
|
655
|
+
tenant resolves to **no permissions** at the root. That is the safe default and
|
|
656
|
+
it stays the default.
|
|
657
|
+
|
|
658
|
+
Some hierarchies legitimately need the other direction for *reading*: a
|
|
659
|
+
network-level list that members of the network's child publications are meant
|
|
660
|
+
to see. Declare it explicitly:
|
|
661
|
+
|
|
662
|
+
```typescript
|
|
663
|
+
// smrt.config.ts
|
|
664
|
+
export default defineConfig({
|
|
665
|
+
packages: {
|
|
666
|
+
users: {
|
|
667
|
+
permissions: {
|
|
668
|
+
ancestorRead: {
|
|
669
|
+
// Descendant SYSTEM role slugs allowed to contribute upward.
|
|
670
|
+
// Exact match, and only `tenantId: null, isSystem: true` roles
|
|
671
|
+
// (what `seedSystemRoles()` creates) can match.
|
|
672
|
+
roles: ['member', 'editor'],
|
|
673
|
+
// Collections whose `read` may travel up. `*` matches any run of
|
|
674
|
+
// characters and may appear anywhere (`'site_*'`, `'*_pages'`);
|
|
675
|
+
// `'*'` alone means every collection the declared role can read.
|
|
676
|
+
collections: ['publications', 'tenants'],
|
|
677
|
+
// Hierarchy hops from the membership up to the tenant being
|
|
678
|
+
// resolved. Default 1 (immediate parent only).
|
|
679
|
+
maxDepth: 2,
|
|
680
|
+
},
|
|
681
|
+
},
|
|
682
|
+
},
|
|
683
|
+
},
|
|
684
|
+
});
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
With that declared, `resolvePermissions(user, networkRootId)` for a
|
|
688
|
+
publication-only `member` returns `publications.read` and `tenants.read` — and
|
|
689
|
+
nothing else. The bounds are hard:
|
|
690
|
+
|
|
691
|
+
| Rule | Behavior |
|
|
692
|
+
|---|---|
|
|
693
|
+
| Default | Off. Undeclared, malformed, or empty-on-either-axis policies resolve exactly as before. |
|
|
694
|
+
| Action | `read` only (`list`/`get` normalize to `read`). `create`/`update`/`delete`/custom actions can never travel upward. |
|
|
695
|
+
| Escalation | Intersected with **both** the declared role's own catalog grants **and** the principal's effective permissions in the contributing tenant. The role bound means a descendant administrator cannot widen the contribution with a tenant GRANT, a group role, or a membership GRANT; the effective bound means a DENY that removed the permission at home removes it at the ancestor too. |
|
|
696
|
+
| Role identity | Only a governed SYSTEM role (`tenantId` null, `isSystem: true`) can match a declared slug. Slugs are not unique across a hierarchy, so a tenant-scoped custom role named `member` contributes nothing — a descendant's administrator cannot mint its way into the allow-list. |
|
|
697
|
+
| Direction | Strictly upward, to verified ancestors only. Siblings share no ancestor relationship and are unreachable. |
|
|
698
|
+
| Precedence | Applies only when NO membership authorized the tenant. A direct membership (even inactive) still pins resolution; an ancestor tenant-level DENY still subtracts. |
|
|
699
|
+
| Hierarchy | The materialized `hierarchyPath` is verified link-by-link against real `parentTenantId` rows; stale, over-deep, or inconsistent paths fail closed. |
|
|
700
|
+
| Bypass | Super-admin and system-context bypass are unchanged. |
|
|
701
|
+
|
|
702
|
+
**This grants the operation, not the rows.** An ancestor-read grant authorizes
|
|
703
|
+
`publications.read` *at the ancestor*. It is not visibility of any sibling
|
|
704
|
+
tenant's rows: row scoping remains with the `@happyvertical/smrt-tenancy`
|
|
705
|
+
interceptor and the generated Postgres RLS policies, which still bind reads to
|
|
706
|
+
the tenant the context is entered with. A member of publication A authorized at
|
|
707
|
+
the network root still cannot read publication B's rows.
|
|
708
|
+
|
|
709
|
+
Resolution happens inside `PermissionResolver.resolvePermissions()`, the single
|
|
710
|
+
point `SessionService.loadSessionContext()`,
|
|
711
|
+
`withPrincipalPermissionContext()`, `assertOperationPermission()`, the
|
|
712
|
+
generated REST/MCP surfaces, and the published `smrt.permissions` RLS variable
|
|
713
|
+
all flow through — so every consumer sees one answer. Resolution is uncached:
|
|
714
|
+
a membership, role, or policy change takes effect on the next resolution, and
|
|
715
|
+
there is no permission cache to invalidate.
|
|
716
|
+
|
|
717
|
+
**Known limitation.** The grant carries no membership, so
|
|
718
|
+
`PermissionResolutionResult.membershipId` and
|
|
719
|
+
`loadSessionContext().tenantAuthorization.membershipId` stay `null`. A surface
|
|
720
|
+
that requires a non-empty `membershipId` for a required-tenant session — such as
|
|
721
|
+
`@happyvertical/smrt-app-runtime`'s deployed runtime — therefore still refuses
|
|
722
|
+
an ancestor-read-only principal (it fails closed, with
|
|
723
|
+
`tenant_context_unauthorized`). Consume the grant through
|
|
724
|
+
`withPrincipalPermissionContext()` / `assertOperationPermission()`, which read
|
|
725
|
+
the resolved permission set. Carrying `ancestorReadFromTenantIds` into
|
|
726
|
+
`tenantAuthorization` is tracked in #2947.
|
|
727
|
+
|
|
728
|
+
Bind a policy to one resolver instead of the global config (tests, embedded
|
|
729
|
+
runtimes) with `PermissionResolver.create(options, { ancestorReadPolicy })`;
|
|
730
|
+
pass `null` to force it off regardless of configuration.
|
|
731
|
+
|
|
732
|
+
#### Adoption note
|
|
733
|
+
|
|
734
|
+
This is additive and off by default — no migration is required, and no existing
|
|
735
|
+
deployment changes behavior until `permissions.ancestorRead` is declared. When
|
|
736
|
+
adopting it, declare the **narrowest** role and collection lists that make the
|
|
737
|
+
ancestor-level list work, and keep `maxDepth` at the smallest value your
|
|
738
|
+
hierarchy needs. `roles` must name system-role slugs; a tenant-scoped role with
|
|
739
|
+
the same slug is ignored by design. Do not reach for it to grant an ancestor-level action: if a
|
|
740
|
+
principal needs to *act* at the root, give it a root membership or a role
|
|
741
|
+
grant, not a read policy.
|
|
742
|
+
|
|
649
743
|
### Tenant policies
|
|
650
744
|
|
|
651
745
|
TenantService supports three modes: `flexible` (no auto-create), `personal` (auto-create on first login, deletable), `required` (auto-create, must keep at least one).
|
|
@@ -683,7 +777,8 @@ TenantService supports three modes: `flexible` (no auto-create), `personal` (aut
|
|
|
683
777
|
|
|
684
778
|
| Export | Description |
|
|
685
779
|
|--------|-------------|
|
|
686
|
-
| `PermissionResolver` | Resolves effective permissions via 4-level cascade. `hasPermission()`, `resolvePermissions()`. |
|
|
780
|
+
| `PermissionResolver` | Resolves effective permissions via 4-level cascade. `hasPermission()`, `resolvePermissions()`. Honors the opt-in `permissions.ancestorRead` policy. |
|
|
781
|
+
| `normalizeAncestorReadPolicy()`, `getConfiguredAncestorReadPolicy()`, `isAncestorReadableSlug()` | Validate and apply the declared read-only ancestor-visibility policy. |
|
|
687
782
|
| `PermissionCatalogService`, `syncPermissionCatalog()` | Discovers manifest/config/runtime permissions and upserts them into `Permission` rows. |
|
|
688
783
|
| `registerPermissionDefinitions()` | Register app or integration permissions at runtime and receive an unregister cleanup function. |
|
|
689
784
|
| `generatePostgresPermissionSql()`, `applyPostgresPermissionPolicies()` | Preview or apply Postgres RLS helper functions and table policies. |
|