Skip to content

[finding] resolveUserAuthzGrants applies the ADR-0091 window to sys_member for accessible_org_ids only — the org-admin role projection is unfiltered #8802

Description

@qq9340100

Restart-when: git grep -n "valid_from|valid_until" -- packages/platform-objects/src/identity/sys-member.object.ts returns non-empty (an ADR-0091 window column is declared on sys_member)

Recording only, unassigned, ungraded. Surfaced while building the standing-key correspondence gate for #8734 (PR #8801) and deliberately not fixed there — it is a resolver question, not a guard one, and #8734's rulings forbid changing behaviour inside that tooling PR.

The observation

packages/core/src/security/resolve-authz-context.ts, in resolveUserAuthzGrants, reads sys_memberonce and uses it for two things. The ADR-0091 validity window is applied to only one of them:

constmembers=awaittryFind(ql,'sys_member',{user_id: userId},200);constaccessibleOrgIds=newSet();for(constmofmembers){if(!isGrantActive(m,nowMs))continue;// window appliedconstorg=m.organization_id??m.organizationId;if(typeoforg==='string'&&org)accessibleOrgIds.add(org);}grants.accessible_org_ids=Array.from(accessibleOrgIds);constactiveMembers=tenantId ? members.filter(/* active-org match */) : members;for(constmofactiveMembers){// no window applied// role split + mapMembershipRole -> grants.positions (org_owner / org_admin / ...)}

So a sys_member row outside its window would be excluded from accessible_org_ids (the ADR-0105 D2 group-posture read reach) while still contributingorg_owner / org_admin to positions — and positions is what the auto-org-admin-grant path and downstream permission-set resolution read.

The comment above the first loop states the window intent for the whole read ("Rows outside their ADR-0091 validity window do not resolve"), which is true of accessible_org_ids and not of the role projection directly below it.

Why this is dormant, not broken

sys_member declares no valid_from / valid_until today — checked against packages/platform-objects/src/identity/sys-member.object.ts, zero hits — and isGrantActive treats an absent bound as unbounded, so both halves currently answer identically. Nothing is wrong at runtime.

What makes it worth recording is that the resolver's own comment already anticipates the columns arriving ("this is a no-op until they exist and correct the moment they do"). That sentence is true of the loop it sits above and not of the projection below it, so the day the columns land the two halves silently disagree: an expired membership would stop widening the group-posture read reach while continuing to confer organization-administration standing.

Adjacent, and why it is separate

plugin-auth's break-glass guard counts organization administrators by grade alone (isOrgAdminGrade, no window), which matches the role projection and not accessible_org_ids. #8801 records this in STANDING_KEY_EXCLUSIONS as the reason sys_member's window bounds are not treated as standing-bearing, with the note that the resolver leads and the guard's list follows. If these columns ever land on sys_member, the resolver is where the two halves have to be reconciled first; the guard's list is downstream of that decision.

Not asserting a fix

Both directions are defensible and the choice is a real decision, not a cleanup: window-filter the role projection too (consistent with sys_user_position, which is filtered), or state deliberately that membership grade is not window-bound and align the accessible_org_ids half instead. Recording it while the columns are still absent is the cheap moment to decide.


Generated by Claude Code

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions