@happyvertical/smrt-users 0.38.7 → 0.38.9

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 CHANGED
@@ -11,7 +11,7 @@ Multi-tenant user management with RBAC, hierarchical tenants, session handling,
11
11
  | Tenant | **STI** + hierarchical parent-child. `hierarchyPath` (materialized path), `hierarchyLevel`. Max depth 10. |
12
12
  | Session | Server-side. Secure UUID. TTL in **seconds** (not ms). Status auto-updates to EXPIRED on access. |
13
13
  | MagicLinkToken | Single-use email login token. Backed by `MagicLinkService`. |
14
- | Role | `tenantId = null` → system role (available to all tenants). `isSystem: true` blocks deletion. |
14
+ | Role | `tenantId = null` → system role (available to all tenants). `isSystem: true` blocks deletion. `inheritsToDescendants: true` (default false) opts the role's membership authority into descendant tenants. |
15
15
  | Permission | Slug format: `resource.action`. Parsed by PermissionResolver. |
16
16
  | Membership | User + Tenant + Role junction. UNIQUE(userId, tenantId). |
17
17
  | Group | Team within a tenant. Multiple roles via GroupRole. |
@@ -44,6 +44,48 @@ The hard block reflects the tenant cascade's **net** resolution, not an
44
44
  unconditional union of every DENY in the chain — so a more-specific tenant GRANT
45
45
  (e.g. a child sub-tenant re-granting a permission its parent DENYs) still wins.
46
46
 
47
+ ### Membership selection — hierarchical inheritance (opt-in, #1866)
48
+
49
+ Which membership feeds step 2 above:
50
+
51
+ 1. **A direct membership row in the target tenant always pins resolution to
52
+ itself** — active rows resolve normally; an inactive (pending/suspended)
53
+ row resolves to the **empty set**. A direct row therefore *attenuates*
54
+ rather than unions: "viewer here, despite being network admin" gives
55
+ viewer, and a suspension in the child is effective even for users holding
56
+ inheritable authority on an ancestor.
57
+ 2. **No direct row** → the resolver walks the tenant's ancestors (nearest
58
+ first, from `hierarchyPath`) and resolves through the **nearest ACTIVE
59
+ ancestor membership whose role has `inheritsToDescendants: true`**.
60
+ Unflagged or inactive ancestor memberships are skipped (they neither confer
61
+ nor block); there is **no union across the chain** — the nearest flagged
62
+ membership alone is used. To attenuate a specific descendant, create a
63
+ direct membership there or use a tenant-level DENY.
64
+ 3. **No qualifying ancestor** → empty set (byte-identical to the
65
+ pre-inheritance resolver; with no role flagged, nothing changes).
66
+
67
+ All later layers run unchanged against the **target** tenant: the tenant
68
+ cascade and tenant-DENY hard block come from the target tenant (a child can
69
+ carve authority out of an inherited role), group roles stay exact-tenant (only
70
+ target-tenant groups contribute; ancestor groups never flow down), and
71
+ membership GRANT/DENY overrides travel with the ancestor membership used.
72
+ `PermissionResolutionResult.inheritedFromTenantId` reports the ancestor tenant
73
+ when inheritance was used (`null` for direct resolution).
74
+
75
+ Safety: resolution is bounded by `MAX_TENANT_HIERARCHY_DEPTH`; malformed
76
+ `hierarchyPath` values fail closed to the empty set — too deep,
77
+ self-referential, duplicate ancestors, or inconsistent with the actual
78
+ `parentTenantId` chain (the path is verified link-by-link against the loaded
79
+ ancestor rows before it is trusted as an authorization source). Tenant `status` is not consulted (parity with direct
80
+ resolution). Caching: a long-lived `(user, tenant)` permission cache must also
81
+ invalidate on ancestor-membership changes and on `Role.inheritsToDescendants`
82
+ flips — request-scoped caches (the common pattern) are unaffected.
83
+
84
+ Flag roles at seed time with
85
+ `seedSystemRoles({ inheritsToDescendants: ['owner', 'admin'] })` (additive:
86
+ listed slugs are flagged, omitted slugs are never unflagged; unknown slugs
87
+ throw). The default seed leaves every role exact-tenant.
88
+
47
89
  ## Operation Permission Guards
48
90
 
49
91
  - Use `assertOperationPermission()` for hand-written mutations in SvelteKit form
@@ -54,9 +96,25 @@ unconditional union of every DENY in the chain — so a more-specific tenant GRA
54
96
  - `assertOperationPermission()` throws fail-closed by default. Use
55
97
  `{ onDeny: 'return' }`, `checkOperationPermission()`, or
56
98
  `hasOperationPermission()` when a structured/boolean result is needed.
99
+ - **Resource-tenant calling convention**: for resource-anchored authorization,
100
+ pass the **resource's** tenant id — `tenantId: resourceTenantId` — not the
101
+ session's current tenant. With `Role.inheritsToDescendants` flagged, a
102
+ root-tenant admin then passes for any descendant resource with no app-side
103
+ authority logic (and no membership fan-out), while per-child delegation and
104
+ DENY precedence keep working. Do NOT pass a session-scoped `membership`
105
+ alongside a different resource `tenantId` — a membership/tenant mismatch
106
+ fails closed by design; omit `membership` and let the resolver look it up.
57
107
  - System context and super-admin bypass context are honored for parity with
58
108
  Postgres RLS. Pass `{ allowSuperAdminBypass: false }` on money-class or
59
109
  separation-of-duties operations that must require an explicit permission grant.
110
+ - **Postgres RLS and membership inheritance**: RLS policies check the
111
+ session-injected `smrt.permissions` list (resolved app-side by
112
+ `PermissionResolver`), so a session whose `smrt.tenant_id` IS the child
113
+ tenant gets inherited authority in RLS automatically. But RLS row filtering
114
+ stays bound to the session's tenant setting — a root-tenant session acting
115
+ on child-tenant rows is authorized by the app-level guard (resource-tenant
116
+ convention above), not by RLS. This is a documented divergence, mirroring
117
+ how #1829 handled bypass parity.
60
118
  - Seed role mappings with `RolePermissionCollection.seedRolePermissions()` or
61
119
  `RoleCollection.seedSystemRoles({ seedPermissions: true })`. The default
62
120
  matrix maps owner/admin to all catalog permissions, member to read/create for
@@ -74,6 +132,11 @@ unconditional union of every DENY in the chain — so a more-specific tenant GRA
74
132
  - `moveToParent()` updates tenant + ALL descendants' paths/levels
75
133
  - `cascadePermissions` (parent pushes down) + `inheritPermissions` (child accepts) — both must be true
76
134
  - `getTree(rootId?)` returns nested structure for UI
135
+ - Two independent downward flows: the `TenantPermissionOverride` **cascade**
136
+ (tenant-level permission config, flags above) and **membership-role
137
+ inheritance** (`Role.inheritsToDescendants`, per-role opt-in — see
138
+ "Membership selection" above). The cascade flags do not gate membership
139
+ inheritance.
77
140
 
78
141
  ## SvelteKit Integration
79
142