Skip to content

Scoped Permission + RolePermission base shapes for Foundation overlays #43

Description

@wangkanai

Scoped Permission + RolePermission base shapes for Foundation overlays

Filing target: github.com/wangkanai/federation (or split to
github.com/wangkanai/foundation if upstream prefers
the base entity home), Priority P1 (per research
009 §8
P1 Permission + RolePermission row).

Motivated by: research
agile/research/009-refactoring-shared-federation-domain-from-account/report.md
§8 P1 row. Drafted from epic
031-oss-contribution-parallel-track/031-02-p1-scoped-permission-role-permission-models.


Problem statement

wangkanai/federation (and the underlying wangkanai/foundation base it
presumably composes) does not today expose a scope-aware RolePermission
link entity
or a scope-free Permission base shape suitable for a
multi-app product platform to overlay onto a thin Common layer. The
Federation repo's published models (per 009 §5 F6) are still
NotImplementedException stubs under "Planned Features," and Foundation
ships Entity / AuditableEntity bases but no permission-shaped entities.
Downstream multi-tenant OIDC platforms that want a composable
permission/grant matrix have nothing to bind against upstream — they end
up re-declaring a private Permission and RolePermission, which is the
exact duplication this project was created to prevent.

This is the P1 sibling of the P0 license issue (F7 / 031-01) and the
Sister-P1 of the multi-tenant Tenant + (email, tenant) composite
identity ask (031-03). All three sit in the same upstream gap: the base
shapes a multi-app platform needs are either missing or planned, not
shipped.

Two half-asks, one upstream gap

The Permission + RolePermission shape is two halves of one matrix:

  1. Permission — the grant key. A scope-free base with Id +
    Name (human-readable grant key like "users.read") + optional
    Description. FK-free, no tenancy, no scope. Anchor: RiverSync's own
    Account/src/Domain/Authorization/Permission.cs:9-28.
  2. RolePermission — the link entity carrying the scope-aware
    Allowed + ScopeRegionId + ScopeSiteId triple and the one-way
    narrowing contract that binds them. Anchor:
    Account/src/Domain/Authorization/RolePermission.cs:39-70.

Both halves belong on the same upstream issue because they share the
matrix semantics; if the base Permission ships without a contract for
how scope attaches to its link, the consumer ends up inventing a
half-formed contract locally. Filing them as a pair lets the maintainer
reject, accept, or split the matrix semantics as a single decision.

RiverSync's matrix evidence (the shape we would overlay)

The downstream shape exists today, locally, in RiverSync's Account
IdP. We are NOT asking upstream to take this exact shape — only that
a base shape be available that lets a thin Common overlay express it
without a hard IdP-package dependency.

// Account/src/Domain/Authorization/RolePermission.cs:39-70 (verbatim, the link entity)
/// <summary>
/// Whether this role↔permission grant is effective. Defaults to <c>true</c> so
/// existing role/permission rows authored pre-015 continue to behave as grants.
/// <para>
/// Per FR-005 / FR-003: an explicit <c>false</c> is a <b>role/permission-layer
/// denial</b>. The scope layer (epic 015, 013-03 addendum) may further narrow
/// the grant on a per-region/per-site basis, but MUST NOT re-introduce a
/// permission that is denied at the role/permission layer — the narrowing
/// invariant is one-way.
/// </para>
/// </summary>
public bool Allowed { get; set; } = true;

/// <summary>
/// Per FR-004 (layer-5 scope override): explicit region-narrowing. <c>null</c>
/// ⇔ no region override (grant is org-wide or, if <see cref="ScopeSiteId"/>
/// is set, site-scoped). ...
/// </summary>
public Guid? ScopeRegionId { get; set; }

/// <summary>
/// Per FR-004 (layer-5 scope override): explicit site-narrowing. <c>null</c>
/// ⇔ no site override. Set to an <see cref="OrganizationSite.Id"/> to
/// narrow the grant to a single site. ...
/// </summary>
public Guid? ScopeSiteId { get; set; }
// Account/src/Domain/Authorization/Permission.cs:9-28 (verbatim, the grant-key entity)
public class Permission
{
   /// <summary>Stable Guid key.</summary>
   public Guid Id { get; set; }

   /// <summary>
   /// Human-readable permission name, e.g. <c>"users.read"</c>, <c>"billing.write"</c>.
   /// The grant key in the RolePermission matrix.
   /// </summary>
   public string Name { get; set; } = string.Empty;

   /// <summary>Optional human-readable description.</summary>
   public string? Description { get; set; }

   /// <summary>UTC timestamp of creation.</summary>
   public DateTimeOffset CreatedAt { get; set; }

   /// <summary>UTC timestamp of last update.</summary>
   public DateTimeOffset UpdatedAt { get; set; }
}

Why this is a composition blocker, not a feature request

  1. No scope-free Permission base upstream. Consumers wanting a
    thin PermissionInfo overlay cannot use Foundation's Entity base
    alone — Entity carries Id + audit metadata but no
    Name-as-grant-key shape. They have to re-declare Permission
    locally. The thin-Common strategy (epic 030) wants to consume, not
    re-declare.
  2. No RolePermission link with one-way narrowing contract. Without
    upstream committing to "scope narrows, never widens; a denied grant
    cannot be re-introduced at the scope layer," consumers have to
    invent the contract themselves and police it in their own resolver.
    The contract is small but load-bearing.
  3. The DM-33 schema-only guard pattern is locally proven. A
    reflection-based test
    (Account/test/Unit/Authorization/RolePermissionTests.cs:39-56)
    bans the runtime method names Compile / Evaluate / Build on
    RolePermission so a future contributor cannot smuggle compiler
    logic into the link entity. If upstream's base carried
    Compile/Evaluate/Build methods, every consumer of the base
    would have to write a mirror of this guard test. We'd like upstream
    to ship the same shape with the same absence, so the guard becomes
    a project convention rather than a per-consumer tax.

RiverSync usage context

RiverSync is a six-app product platform. Its OIDC identity-provider role
is centralized in a single application (Account); the other five
(Portal, Admin, Partners, Pipeline, Field) participate strictly
as OIDC relying parties. The following four anchors explain why a
scope-free Permission base and a scope-aware RolePermission link matter
to us — and why we are asking for the shapes rather than quietly forking.

  • Thin Common foundation (epic 029).
    Epic 029 — Common OSS Foundation Layer
    establishes RiverSync.Common.Domain as a thin shell depending on
    Wangkanai.Domain and Wangkanai.System, with client-side federation
    pieces (claims codec, OIDC RP wiring) already extracted into
    Common/src/Claims/ and Common/src/Web/ without issuer-side
    leakage. This is the Option A recommendation from research 009 §7:
    thin extraction in Common, not a full internal fork.

  • Planned thin Common overlays on Foundation (epic 030).
    Epic 030 — Extract Federation Domain Models To Common
    is the follow-on that would push consumable model shapes
    (PermissionInfo, RolePermissionInfo overlays on
    Foundation.Entity) into Common.Domain, leaving Account as the
    persisted IdP matrix and issuer. Neither 029 nor 030 pins
    Wangkanai.Federation today — both are blocked on the license
    question being resolved first (F7, see 031-01) AND on the
    Permission/RolePermission base shapes being available upstream.

  • Cross-tenant ViewAsContext precedent (epic 018).
    Epic 018 — Cross-Tenant Access (View-As)
    established the ViewAsContext pattern — anchored at
    Common/src/Web/ViewAsContext.cs
    for safely traversing tenant boundaries in the platform IdP without
    leaking issuer responsibilities. The same "shape lives in Common,
    writer lives in Account" pattern applies to the permission matrix
    that the scope layer reads at runtime.

  • ARCH:VI — Account is the sole writer; Common provides consumption shapes.
    Per architecture rule
    .claude/rules/architecture/federation-auth.md
    (extract ARCH:VI):

    • Account is the sole platform OIDC/OAuth2 identity provider — and
      the only writer of the persisted role↔permission matrix.
    • The other five product apps (Portal, Admin, Partners, Pipeline, Field)
      participate strictly as OIDC relying parties; none runs its own
      authorization server, credential store, OR role↔permission matrix.
    • Common provides consumable shapes (PermissionInfo /
      RolePermissionInfo overlays, claims codec, RP wiring,
      ViewAsContext); it does NOT host a token issuer or persist a
      matrix. The base shapes in this issue are what Common would
      overlay against.

No timeline promises (explicit)

To be unambiguous about what this issue is not asking:

  • No timeline promise. This issue does not commit RiverSync to any
    particular release date for adopting any upstream release. Internal
    sequencing is governed separately by our own epic planning and may
    change without notice to this issue. Concretely: no timeline or
    commitment by RiverSync is tied to the resolution of this issue.
  • No adoption commitment. RiverSync has not committed to taking a
    hard Wangkanai.Federation (or Wangkanai.Foundation) dependency
    even after this issue is resolved. The thin Common strategy in
    029/030 is OSS-agnostic and is being executed on Wangkanai.Domain
    • Wangkanai.System today; any deeper Federation adoption is
      contingent on legal/architectural review on our side.
  • No licensing-philosophy request. We are not asking upstream to
    change their licensing philosophy — only that the raw Permission /
    RolePermission base shapes be available for thin-overlay
    consumption. The license of record (Apache-2.0 vs SSPL v1 vs
    something else) is a project governance decision and not ours to
    make; it is a sibling concern tracked in the P0 issue (F7 / 031-01).
  • No scope-on-Permission request. Scope belongs on the link
    entity (RolePermission.Allowed + ScopeRegionId / ScopeSiteId),
    not on Permission itself. Putting scope on Permission would couple
    the base to a tenancy model that Foundation doesn't otherwise own,
    and would push Foundation toward IdP-matrix semantics — exactly the
    shape the thin-overlay contract is trying to avoid.

Request

We respectfully ask the wangkanai/federation (and, if appropriate,
wangkanai/foundation) maintainers to consider shipping the following
two base shapes as thin, composable, schema-only POCOs. We have ordered
them so that the maintainer can accept, reject, or split them as a
single matrix decision.

1. Permission — scope-free grant-key base

A base entity (or POCO) with exactly:

  • Guid Id — stable key (convention with the rest of the Foundation
    Entity line).
  • string Name — human-readable grant key, e.g. "users.read",
    "billing.write". No FK leakage: Permission carries NO
    RoleId, NO TenantId, NO scope FKs. Relationships are configured
    at the consumer's ORM mapping, not declared on the type.
  • string? Description — optional human-readable description.
  • CreatedAt / UpdatedAt timestamps — present and explicit in the
    base contract (or absent by design, with the choice documented) so
    consumer overlays don't silently lose audit metadata.

Mirrors RiverSync's
Account/src/Domain/Authorization/Permission.cs:9-28.
Scope does NOT live on Permission. It belongs on the link entity
(§2 below).

2. RolePermission — scope-aware link with one-way narrowing contract

A schema-only base (or POCO) link entity with:

  • Guid Id — stable key.
  • Guid RoleId — FK scalar to ApplicationRole.Id. No navigation
    property (FK-scalar convention, configurable in the consumer's
    mapping).
  • Guid PermissionId — FK scalar to Permission.Id. No navigation
    property.
  • bool Allowed (default true) — explicit false is a
    role/permission-layer denial.
  • Guid? ScopeRegionId — nullable region-narrowing override. null
    ⇔ no region override (grant is org-wide or, if ScopeSiteId is set,
    site-scoped).
  • Guid? ScopeSiteId — nullable site-narrowing override. null ⇔ no
    site override. Setting a site implies its region.
  • CreatedAt / UpdatedAt — UTC timestamps.

The one-way narrowing contract (verifiable property, not just a
comment).
The resolver intersects the role/permission-layer decision
with the scope override; the operation is set-intersection, not
set-union. Three properties bind:

  1. Scope can narrow, never widen. A scope override may further
    restrict where/when a grant applies, but it cannot extend it.
  2. A denied grant cannot be re-introduced at the scope layer. If
    Allowed = false, no combination of ScopeRegionId /
    ScopeSiteId may create an effective grant.
  3. The resolver intersects, never unions. DM-10 + §12 resolved on
    the RiverSync side; the same intersection-only contract is what
    we are asking upstream to commit to.

No embedded compiler. Any claims-compiler runtime the consumer
needs lives in a separate type or package — never on the base
Permission / RolePermission POCOs. This is the
RolePermission_Has_No_ClaimsCompiler_Logic guard pattern
(Account/test/Unit/Authorization/RolePermissionTests.cs:39-56)
that bans the runtime method names Compile / Evaluate / Build
on the link entity. We would love to see upstream ship the same
shape with the same absence, so the guard becomes a project
convention rather than a per-consumer tax.

3. Documentation in a matrix contract note (companion to LICENSE-COMPATIBILITY)

Regardless of which package the base shapes live in, ship a short
PERMISSION-MATRIX.md (or equivalent) in the repo that documents:

  • Which entity holds the grant key (Permission.Name).
  • Which entity holds the scope (RolePermission.Allowed +
    ScopeRegionId + ScopeSiteId).
  • The one-way narrowing contract, with the three verifiable
    properties above.
  • The schema-only guard pattern (no Compile / Evaluate /
    Build on the link entity).

This is the same posture RiverSync needs for ARCH:VI compliance: a
written, citable contract on the upstream side that consumers can
quote in their own compliance documentation.

What we are NOT asking for

  • No change to upstream's licensing philosophy (tracked separately in
    the P0 issue, F7 / 031-01).
  • No scope fields on Permission itself (scope belongs on the link
    entity per §2).
  • No specific ORM shape (FK-scalar convention, configurable in the
    consumer's mapping; no embedded navigations).
  • No commitment from RiverSync to take a hard dependency on the
    shipped shape once available.

Why now

This is a long-standing gap (the Federation repo's permission models
are still "Planned Features" per 009 §5 F6), but the timing is right
for three converging reasons that make the upstream base-shape gap
visible from a multi-app product platform's perspective:

  1. The thin Common overlay plan is concrete, not hypothetical.
    Epic 030's PermissionInfo / RolePermissionInfo overlays on
    Foundation.Entity are the next concrete step in the
    thin-extraction plan from 009 §7 Option A. The overlay work
    surfaces exactly the missing base shapes upstream needs to ship.
  2. The cross-tenant / ViewAsContext precedent (epic 018) makes
    the multi-tenant IdP pattern of "shape lives in Common, writer
    lives in Account" a real, shipped pattern on the RiverSync side —
    not a roadmap sketch. The same pattern now applies to the
    permission matrix, and the upstream gap is the missing piece.
  3. The DM-33 schema-only guard pattern is locally proven. A
    reflection-based test bans the runtime method names
    Compile / Evaluate / Build on RolePermission, which
    means the upstream guard is a one-paragraph copy of a pattern
    that already exists in a shipping project. The cost of shipping
    the same shape upstream is small; the cost of every consumer
    re-writing the same guard test is cumulative.

Without these base shapes upstream, multi-app OIDC platforms either
re-declare a private Permission / RolePermission matrix (duplicating
what Foundation was created to share) or invent a scope-narrowing
contract locally with no upstream authority to anchor it. Neither is a
good place for the platform to be.


References


Drafted by RiverSync for upstream filing against wangkanai/federation
(or wangkanai/foundation if upstream prefers the split). Not yet
filed. Once filed, the upstream URL will replace this header note and
the tracker row in docs/oss-contributions.md
will be updated to Filed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions