4.3 KiB
ADR 0019: Resolve Actors to Principal Sets for Team Permissions
Status
Accepted.
Context
ADR-0004 deliberately chose a Feishu Docs-like permission model:
grant(resource, principal, role) plus separate resource settings. The first
Hub implementation stored principal as an opaque string and the Feishu trigger
used the sender's open_id directly. That was enough for individual teacher
grants, but not for production collaboration:
- a curriculum team needs one grant to apply to many teachers;
- Feishu departments, user groups, and project chats should be grantable principals;
- role-specific agent grants (
/review,/draft) must compose with project permissions consistently; - audit/debug output must explain which principal and grant allowed or denied a run.
Decision
Introduce typed principals and resolve every actor to a principal set before authorization.
Principal types
PrincipalType is:
USER: a Feishu user, identified byfeishuOpenId.TEAM: a Hub-managed teacher team.FEISHU_CHAT: a Feishu chat/group.FEISHU_DEPARTMENT: a Feishu department.FEISHU_USER_GROUP: a Feishu user group.APP: an integration or bot principal.
PermissionGrant and RoleTriggerGrant store principalType and
principalId. Legacy opaque principal strings are backfilled as
USER/<old principal>.
Team membership
Hub teams are first-class. A user can be a direct active member of a team. A team can also be bound to external Feishu principals: chat, department, or user group. If an actor resolves to any bound external principal, the actor also resolves to that Hub team.
Membership changes are effective immediately because authorization resolves the principal set at request time.
External Feishu principal sync
Feishu department, user group, and chat memberships are not scattered through
callers. They are synchronized into ExternalPrincipalMembership rows and then
read by PrincipalResolver. The Feishu message context can also contribute the
current FEISHU_CHAT principal directly because receiving a group event is
already contextual proof that the actor is speaking in that group.
Grant merging
Authorization evaluates all active grants matching any resolved principal for the requested resource. There is no explicit deny in this ADR. Among matching grants, the highest role wins:
READ < EDIT < MANAGE
Action thresholds:
- project read requires
READ. - project edit requires
EDIT. - collaborator management requires
MANAGE. - agent trigger requires
EDIT. - normal agent cancel requires
MANAGE.
PermissionSettings never grants access beyond grants. Settings only constrain
an already-granted capability. For agentTrigger:
- missing setting or
ROLEmeans role-derived grants decide; MANAGE_ONLYraises the threshold toMANAGE;DISABLEDdenies the action.
Other settings keep their existing string storage, but the same rule applies: settings are policy constraints, not positive grants.
Role-trigger composition
Role-trigger grants are a second gate after the project-level agent.trigger
gate:
- The actor must pass project
agent.triggerfor the project. - If no
RoleTriggerGrantrow has ever existed for(projectId, roleId), the role is open for backward compatibility. - Once any row exists for
(projectId, roleId), including a revoked row, the role is configured. The actor must match an active role grant by any resolved principal.
This keeps /review restrictions orthogonal to ordinary edit access while
preventing a fully revoked role from silently reopening.
Module seam
Callers use a single authorization module:
authorizer.can({
actor,
action,
resource,
roleId,
})
The caller does not know how users, teams, Feishu departments, user groups, or
chats expand. PrincipalResolver owns that implementation, and
PermissionAuthorizer owns grant/settings/role-trigger composition.
Consequences
- Teacher teams become production-grade principals rather than UI sugar.
- Feishu external organization concepts can be synchronized and granted without leaking Feishu-specific checks into the trigger path.
- A single audit decision can report actor, principal set, matched grant, and matched role grant.
- Backward compatibility is explicit: old principal strings become user principals, and unconfigured role grants remain open.