forked from EduCraft/curriculum-project-hub
feat(hub): 支持教师团队权限
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
# 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:
|
||||
|
||||
```text
|
||||
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:
|
||||
|
||||
```ts
|
||||
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.
|
||||
Reference in New Issue
Block a user