Files
curriculum-project-hub/docs/adr/0019-principal-set-team-permissions.md
T

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 by feishuOpenId.
  • 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 ROLE means role-derived grants decide;
  • MANAGE_ONLY raises the threshold to MANAGE;
  • DISABLED denies 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:

  1. The actor must pass project agent.trigger for the project.
  2. If no RoleTriggerGrant row has ever existed for (projectId, roleId), the role is open for backward compatibility.
  3. 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.