Skip to content

approvals: the manager approver resolves sys_user.manager_id with no organization screen, while every sibling approver expansion is org-scoped #10153

Description

@baozhoutao

Observed while implementing the read-only crossing-manager_id audit for cloud#1369 (not fixed there — that card is explicitly read-only and files no writes). Recording it here because a fix would land in plugin-approvals, not in cloud.

What was measured

In packages/plugins/plugin-approvals/src/approval-service.ts, expandApprovers hands the directory organization to every graph-shaped approver expansion:

  • teamexpandTeamUsers(value, directoryOrg)
  • departmentexpandBusinessUnitUsers(value, directoryOrg)
  • positionexpandPositionUsers(value, directoryOrg)
  • org_membership_levelexpandMembershipTierUsers(value, directoryOrg)

The manager branch does not:

}elseif(type==='manager'&&record){constsubject=(recordasany)[a.value]??(recordasany).owner_id;if(subject){constmgr=awaitthis.lookupManager(String(subject));if(mgr)returnthis.applyOooDelegation(mgr,now,organizationId,substitutions);}}

and lookupManager (same file) reads the column directly under a system context, taking no organization argument at all:

privateasynclookupManager(userId: string): Promise<string|null>{
const rows=awaitthis.engine.find('sys_user',{where: {id: userId},fields: ['id','manager_id'],limit: 1,context: SYSTEM_CTX,}asany);
...
}

Why it matters

sys_user is a global identity table with no organization_id column, so nothing else in this path supplies the tenancy fact. If a sys_user.manager_id crosses an organization boundary, an approval step with approverType: 'manager' routes the submission to an approver in another organization — an out-of-tenant person granted approval authority over the record.

The hierarchy consumer of the same column has been screened since cloud#1195 (HierarchyScopeResolver.screenToTenant drops users sys_member proves are outside the caller's org). Approvals is a different consumer of the same column and was not covered by that work, so the same row behaves in opposite directions: it silently narrows an own_and_reports owner set and widens approval routing.

Not asserting the fix shape

Two directions exist and they are not equivalent, so this is filed for triage rather than argued:

  1. screen lookupManager the way the sibling expansions are screened (refuse / fall through when provablyDisjointOrgs holds), which makes the approval slot resolve to nobody and needs a story for that;
  2. treat it as an authoring-time invariant only, on the grounds that cloud#1195's write gate now refuses new crossing links — which leaves rows that predate the gate, the exact residual cloud#1369 exists to report.

Whether approver routing should imply tenancy at all overlaps with the open design question in #7497.

Related

Verified against 907c11d2cee5fcdd420e2eedf79dc6081d08f5c3 (the SHA cloud pins today).

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions