Skip to content

Write guard and #562 token both skip Identity's AccountId-less tables; AspNetUserRoles is live RBAC state with no tenant column #670

Description

@mforce

Found while closing #562 (falsifying review of the seam, 2026-09-02).

The gap

Both the write-side tenant guard (TenantStampInterceptor, #546) and the #562 fix (AccountId as an EF concurrency token on every entity that carries one) match on the AccountId property name. Entity types without that property are outside both mechanisms:

  • DurableJob — deliberate, recorded in TenantBypassDiscoveryTests.
  • Identity's six own types: ApplicationRole, IdentityRoleClaim, IdentityUserClaim, IdentityUserLogin, IdentityUserRole, IdentityUserToken.

Of those, AspNetUserRoles (IdentityUserRole<Guid>) is live RBAC state: a tracked cross-tenant Remove or Add on it — a row naming another farm's user id — is refused by nothing on the write side. Reads are scoped because ApplicationUser carries AccountId and every role lookup goes through the user, but the row itself has no tenant column.

Why it is not a live bypass today

Every AspNetUserRoles write goes through UserManager.AddToRoleAsync / RemoveFromRoleAsync on a user loaded through the tenant-filtered ApplicationUser set, so the user id is always this tenant's. The gap is the same shape #562 had: the guarantee rests on call-site discipline, not on the mechanism.

Options

  1. Add a shadow or real AccountId to IdentityUserRole (and the other five if desired), populated from the user, so the existing name-matched guard and the Write guard trusts OriginalValue as DB provenance; detached Update/Remove can bypass the tenant theft check #562 token cover it by construction. Needs a migration and a backfill.
  2. A guard test that walks Identity's own DbSets and asserts every write to them is reachable only through UserManager on a filtered user (convention, mechanically checked).
  3. Record it as accepted risk in docs/decisions/530-multi-farm-tenancy.md with the premise pinned by a test.

Verify

Whatever the fix: serving farm A, hand-build an IdentityUserRole stub naming farm B's user id, Remove/Add it through the context, and the refusal must be the assertion that goes red when the fix is reverted.

Activity

  1. mforce commented on Sep 3, 2026

    @mforce
    OwnerAuthor

    Closed by fc0552ae (PR #675, squash of five commits). What shipped, against the body above:

  2. added a commit that references this issue on Sep 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:apiAPI/endpoint layer

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions