From e9ce9a539ee630797f4a58783ac680a24b0aa016 Mon Sep 17 00:00:00 2001 From: weichen Date: Wed, 14 Oct 2026 16:42:24 +0000 Subject: [PATCH] Plan the focused role and permission implementation Why: The approved scope spans Backend authority, RDM project enforcement, and Frontend configuration and needs exact contracts and verification boundaries. Evidence: The plan was checked against every specification section, fixed interface names, failure cases, and placeholder rules. Impact: Implementation is limited to one effective role, system and project authorization, and the requested configuration views. Refs: Focused role and permission system spec --- ...sed-role-permission-implementation-plan.md | 393 ++++++++++++++++++ ...-10-15-iam-focused-role-permission-spec.md | 4 +- 2 files changed, 396 insertions(+), 1 deletion(-) create mode 100644 docs/proposals/2026-10-15-iam-focused-role-permission-implementation-plan.md diff --git a/docs/proposals/2026-10-15-iam-focused-role-permission-implementation-plan.md b/docs/proposals/2026-10-15-iam-focused-role-permission-implementation-plan.md new file mode 100644 index 0000000..57dff33 --- /dev/null +++ b/docs/proposals/2026-10-15-iam-focused-role-permission-implementation-plan.md @@ -0,0 +1,393 @@ +# Focused Role and Permission System Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Deliver the requested single-effective-role position model and enforce system, role, and per-project permissions from one server-side action catalog. + +**Architecture:** Backend owns roles, the single active user binding, the permission catalog, system grants, and role defaults/caps. RDM consumes that public authority while retaining project membership, project overrides, creator ownership, and existing document duties. Frontend uses Backend role APIs for the single-role and matrix views and uses RDM only for project-scoped configuration. + +**Tech Stack:** Go 1.27, SQLite, PostgreSQL, HTTP JSON APIs, compiled extension Host contracts, React 19, TypeScript, Vitest, Playwright. + +**Spec:** `docs/proposals/2026-10-15-iam-focused-role-permission-spec.md` + +## Global Constraints + +- One user has at most one effective role; one role may be assigned to many users. +- Role and user stable IDs never change when display data changes. +- System administrators do not gain implicit access to project business content. +- A project creator can manage only the project they created. +- Unknown actions, missing providers, stale versions, disabled accounts, and disabled roles fail closed. +- Role defaults never exceed role caps; project allows never exceed role or platform caps. +- UI configuration uses a draft, preview, and atomic batch commit; hidden rows do not lose draft edits. +- Account recovery, multi-role bindings, Agent permissions, workflow redesign, production migration, and deployment remain outside this plan. + +## Review Focus + +- Reassigning a user while two administrators submit concurrently must leave exactly one active binding and return one conflict. +- Disabling a role between preview and commit must reject the commit without partial writes. +- A project creator switching from project A to B must not retain A-derived management authority in B. +- A filtered matrix batch must contain explicit action IDs and must not apply to hidden or newly registered actions. +- RDM provider absence or version mismatch must deny authorization and must not fall back to the legacy local role table. + +--- + +### Task 1: Backend action catalog and role schema + +**Files:** +- Create: `Agent-System-Backend/internal/authz/catalog.go` +- Create: `Agent-System-Backend/internal/store/role_policy.go` +- Modify: `Agent-System-Backend/internal/store/migrate.go` +- Test: `Agent-System-Backend/internal/store/role_policy_migration_test.go` +- Test: `Agent-System-Backend/internal/authz/catalog_test.go` + +**Interfaces:** +- Produces: `authz.Descriptor`, `authz.DescriptorByID(string) (Descriptor, bool)`, `store.RoleRecord`, `store.RoleBinding`, and schema v35 tables. +- Consumes: existing `users(id, owner, enabled, state_version)` and migration transaction machinery. + +- [ ] **Step 1: Add failing catalog tests** + +```go +func TestCatalogRejectsUnknownAndProtectsManagementActions(t *testing.T) { + if _, ok := DescriptorByID("invented.action"); ok { t.Fatal("unknown action registered") } + got, ok := DescriptorByID("system.roles.manage") + if !ok || got.Scope != ScopeSystem || !got.Protected { t.Fatalf("descriptor=%+v", got) } + create, ok := DescriptorByID("project.create") + if !ok || create.Scope != ScopeSystem || create.Protected { t.Fatalf("project.create=%+v", create) } +} +``` + +- [ ] **Step 2: Run the focused test and confirm the missing package failure** + +Run: `GOWORK=off go test ./internal/authz -run TestCatalogRejectsUnknownAndProtectsManagementActions -count=1` +Expected: FAIL because `internal/authz` does not exist. + +- [ ] **Step 3: Implement the fixed action catalog** + +```go +type Scope string +const ( ScopeSystem Scope = "system"; ScopeProject Scope = "project" ) +type Descriptor struct { ActionID, Label, Description, Group string; Scope Scope; Protected bool } +func DescriptorByID(id string) (Descriptor, bool) +func Catalog() []Descriptor +``` + +Register only the system and project actions listed in the spec. Return copies so callers cannot mutate the registry. + +- [ ] **Step 4: Add schema v35 and migration tests for both dialects** + +Create `roles`, `role_permissions`, `user_role_bindings`, `system_grants`, `authority_state`, and `policy_receipts`. Enforce one active binding per user with a unique `user_id` key; preserve inactive history through versioned replacement rows in the same transaction. Use the existing boolean representation per dialect and foreign keys to `users(id)`. + +Run: `GOWORK=off go test ./internal/store -run 'TestRolePolicyMigration|TestMigrate' -count=1` +Expected: PASS on SQLite; the existing PG migration suite later verifies PostgreSQL. + +- [ ] **Step 5: Commit the schema and catalog** + +```bash +git add internal/authz/catalog.go internal/authz/catalog_test.go internal/store/role_policy.go internal/store/role_policy_migration_test.go internal/store/migrate.go +git commit -m "Add the role permission catalog and schema" +``` + +### Task 2: Atomic role, binding, and permission store + +**Files:** +- Modify: `Agent-System-Backend/internal/store/role_policy.go` +- Test: `Agent-System-Backend/internal/store/role_policy_test.go` +- Test: `Agent-System-Backend/internal/store/role_policy_pg_test.go` + +**Interfaces:** +- Consumes: `authz.DescriptorByID`, current account state, store transaction helpers. +- Produces: + - `CreateRole(actor string, in RoleCreate) (RoleRecord, error)` + - `PatchRole(actor, roleID string, expected int64, patch RolePatch) (RoleRecord, error)` + - `ReplaceRolePermissions(actor, roleID string, expected int64, changes []RolePermissionChange, key string) (PolicyReceipt, error)` + - `AssignUserRole(actor, userID, roleID string, expectedState int64, key string) (RoleBinding, error)` + - `RoleForUser(userID string) (RoleBindingView, error)` + - `ReplaceSystemGrants(actor, userID string, expectedEpoch int64, actions []string, key string) (PolicyReceipt, error)` + +- [ ] **Step 1: Write failing transaction and concurrency tests** + +```go +func TestAssignUserRoleKeepsOneEffectiveBinding(t *testing.T) { + first := mustAssign(t, st, admin, userID, engineerID, 1, "assign-1") + second := mustAssign(t, st, admin, userID, reviewerID, first.UserStateVersion, "assign-2") + if second.RoleID != reviewerID { t.Fatalf("binding=%+v", second) } + if got := countActiveBindings(t, st, userID); got != 1 { t.Fatalf("active=%d", got) } +} +``` + +Also test two concurrent replacements with the same expected state, duplicate idempotency keys, disabled users, disabled roles, unknown actions, `allowed=true/capAllowed=false`, stale role versions, a multi-change batch containing one invalid item, and rejection when a system-grant batch removes the final effective `system.permissions.manage` holder. + +- [ ] **Step 2: Run tests and confirm missing methods fail** + +Run: `GOWORK=off go test ./internal/store -run 'TestAssignUserRole|TestReplaceRolePermissions' -count=1` +Expected: FAIL with undefined store methods. + +- [ ] **Step 3: Implement transaction-scoped mutations** + +Lock the actor, target account, role, and `authority_state` in the existing account-before-policy lock order. Validate the complete batch before any write. Increment `role_version` and `authority_epoch` once per successful transaction and persist one `policy_receipts` row per idempotency key. + +- [ ] **Step 4: Verify SQLite and PostgreSQL behavior** + +Run SQLite: `GOWORK=off go test ./internal/store -run 'Role|Policy|Binding' -count=1` +Run PostgreSQL through the isolated PG harness after it creates the mode-0600 DSN file: `AGENT_SYSTEM_TEST_PG_DSN_FILE=/tmp/iam-u4-role-policy-pg-dsn GOWORK=off go test ./internal/store -run 'Role|Policy|Binding' -count=1` +Expected: both pass with identical conflict and rollback assertions. + +- [ ] **Step 5: Commit the store service** + +```bash +git add internal/store/role_policy.go internal/store/role_policy_test.go internal/store/role_policy_pg_test.go +git commit -m "Add atomic role and binding policies" +``` + +### Task 3: Backend authorization service and HTTP API + +**Files:** +- Create: `Agent-System-Backend/internal/authz/service.go` +- Create: `Agent-System-Backend/internal/server/roles.go` +- Modify: `Agent-System-Backend/internal/server/server.go` +- Modify: `Agent-System-Backend/internal/server/accounts.go` +- Test: `Agent-System-Backend/internal/server/roles_test.go` +- Test: `Agent-System-Backend/internal/server/role_authorization_test.go` + +**Interfaces:** +- Consumes: Task 2 store methods and existing authenticated actor resolution. +- Produces: the HTTP endpoints in the spec and `Service.AuthorizeSystem(actor, actionID string) Decision`. + +- [ ] **Step 1: Write failing authorization and HTTP tests** + +```go +func TestRoleManagerCannotGrantProtectedPermission(t *testing.T) { + cookie := loginAs(t, h, "role-manager") + rec := do(h, "PUT", "/api/admin/roles/"+roleID+"/permissions", + `{"expectedRoleVersion":1,"idempotencyKey":"p1","changes":[{"actionId":"system.permissions.manage","allowed":true,"capAllowed":true}]}`, cookie) + if rec.Code != http.StatusForbidden { t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) } +} +``` + +Cover role description create/edit, exact and fuzzy role search, status/action filtering, pagination, one role assigned to two users, user reassignment, stale versions, fixed-ID batches, disabled role assignment, system-grant listing/replacement, final permission-manager protection, and account profile fields remaining outside authorization. + +- [ ] **Step 2: Run tests and confirm routes are absent** + +Run: `GOWORK=off go test ./internal/server -run 'Role|PermissionCatalog|UserRole' -count=1` +Expected: FAIL with 404 or missing handlers. + +- [ ] **Step 3: Implement service authorization and handlers** + +Protected actions require explicit `system_grants`; `project.create` may also use the current enabled role permission. Provide `GET/PUT /api/admin/users/{userId}/system-grants` with expected epoch and idempotency key. Replace `requireAdmin` only for the new role endpoints in this task; existing unrelated admin endpoints remain unchanged until their explicit action is wired in Task 4. + +- [ ] **Step 4: Wire account management actions without changing recovery** + +Map account list/create/profile/state handlers to `system.users.view/create/edit/disable`. Do not touch recovery routes or credential behavior. Preserve first-admin bootstrap as the protected root recovery path. + +- [ ] **Step 5: Run server and full Backend tests** + +Run: `GOWORK=off go test ./internal/authz ./internal/server ./internal/store -count=1` +Run: `GOWORK=off go test ./... -count=1` +Expected: PASS. + +- [ ] **Step 6: Commit the Backend API** + +```bash +git add internal/authz/service.go internal/server/roles.go internal/server/server.go internal/server/accounts.go internal/server/roles_test.go internal/server/role_authorization_test.go +git commit -m "Expose role and system permission APIs" +``` + +### Task 4: Public role provider for RDM + +**Files:** +- Modify: `Agent-System-Backend/pkg/extension/extension.go` +- Modify: `Agent-System-Backend/internal/app/hostcaps.go` +- Test: `Agent-System-Backend/internal/app/role_provider_test.go` +- Create: `Agent-System-RDM/internal/perms/role_provider.go` +- Modify: `Agent-System-RDM/internal/rdmapp/perms_api.go` +- Test: `Agent-System-RDM/internal/rdmapp/role_provider_test.go` + +**Interfaces:** +- Produces public `extension.RolePolicyProvider` methods `RolePolicyForActor(actor string)`, `RolePolicyForUser(userID string)`, and `CheckRoleAction(userID, actionID string)` returning versioned, non-secret snapshots. +- Consumes the actual request actor from the trusted Host; caller-supplied actor fields never select authority. + +- [ ] **Step 1: Add provider contract tests on both sides** + +Backend tests verify stable user IDs, enabled state, role version, action default/cap, authority epoch, and rejection when the Host has no authenticated actor. RDM tests verify provider missing, wrong version, disabled role, and unknown action all deny without consulting the legacy tables. + +- [ ] **Step 2: Run the contract tests red** + +Run Backend: `GOWORK=off go test ./internal/app -run RoleProvider -count=1` +Run RDM: `GOWORK=off go test ./internal/rdmapp -run RoleProvider -count=1` +Expected: FAIL with missing public provider. + +- [ ] **Step 3: Implement the public interface and RDM adapter** + +The provider returns only role and permission metadata needed for authorization. It must not return user contact fields. RDM's evaluator must use the provider whenever a role binding exists; absence is a denial after migration readiness is enabled. + +- [ ] **Step 4: Run both component suites** + +Run Backend: `GOWORK=off go test ./... -count=1` +Run RDM with the reviewed Backend module overlay during development: `GOWORK=off go test ./... -count=1` +Expected: PASS; overlay evidence remains SELF until a trusted Backend tag is published. + +- [ ] **Step 5: Commit each repository separately** + +Backend commit: `Expose the role policy provider`. +RDM commit: `Consume the Backend role authority`. + +### Task 5: RDM migration adapter and creator-only project management + +**Files:** +- Create: `Agent-System-RDM/internal/perms/role_migration.go` +- Modify: `Agent-System-RDM/internal/perms/roles.go` +- Modify: `Agent-System-RDM/internal/rdmapp/perms_api.go` +- Modify: `Agent-System-RDM/plugins/rdm/plugin.go` +- Test: `Agent-System-RDM/internal/perms/role_migration_test.go` +- Test: `Agent-System-RDM/internal/rdmapp/project_creator_permissions_test.go` +- Test: `Agent-System-RDM/plugins/rdm/roles_test.go` + +**Interfaces:** +- Consumes Task 4 provider and Backend import endpoint/command. +- Produces a versioned export manifest for existing RDM roles and a compatibility forwarding layer for `rdm.role.*` writes. + +- [ ] **Step 1: Write migration and cross-project denial tests** + +```go +func TestCreatorCannotManageAnotherCreatorsProject(t *testing.T) { + projectA := createProjectAs(t, "alice") + projectB := createProjectAs(t, "bob") + requireAllowed(t, setProjectPermission("alice", projectA, "carol", ActDocRead, "allow")) + requireDenied(t, setProjectPermission("alice", projectB, "carol", ActDocRead, "allow")) +} +``` + +Also assert role IDs, descriptions, bindings, defaults, caps, disabled state, and counts survive export/import; once provider readiness is set, legacy local writes and fallback reads are rejected. + +- [ ] **Step 2: Implement export and forwarding without production migration** + +Add dry-run export and compare functions. Forward `rdm.role.create/action/assign/disable/list` to Backend through the trusted Host. Keep old tables read-only for verification; do not delete them. + +- [ ] **Step 3: Tighten project management to immutable creator ownership** + +For member, permission, and duty configuration, load the actual project and compare its immutable `created_by` to the authenticated actor before applying role and platform caps. Do not add delegated project-manager behavior. + +- [ ] **Step 4: Run RDM tests** + +Run: `GOWORK=off go test ./internal/perms ./internal/rdmapp ./plugins/rdm -count=1` +Run: `GOWORK=off go test ./... -count=1` +Expected: PASS. + +- [ ] **Step 5: Commit the RDM consumer** + +```bash +git add internal/perms/role_migration.go internal/perms/roles.go internal/rdmapp/perms_api.go plugins/rdm/plugin.go internal/perms/role_migration_test.go internal/rdmapp/project_creator_permissions_test.go plugins/rdm/roles_test.go +git commit -m "Move RDM role writes to the Backend authority" +``` + +### Task 6: Frontend role API and shared draft model + +**Files:** +- Modify: `Agent-System-Frontend/src/api.ts` +- Create: `Agent-System-Frontend/src/rolePermissions.ts` +- Test: `Agent-System-Frontend/test/role-permissions.test.ts` + +**Interfaces:** +- Consumes Backend HTTP endpoints from Task 3. +- Produces `RoleRecord`, `PermissionDescriptor`, `RolePermissionDraft`, `mergeRoleDraft`, `visiblePatch`, and typed API methods. + +- [ ] **Step 1: Write failing draft-model tests** + +Test that filtered rows retain edits, `visiblePatch` includes only explicit action IDs, single-role and matrix edits produce identical changes, stale server refresh does not replace a newer draft generation, and `allowed=true/capAllowed=false` is rejected before submit. + +- [ ] **Step 2: Implement the typed API and pure draft helpers** + +```ts +export type RolePermissionValue = { allowed: boolean; capAllowed: boolean } +export type RolePermissionDraft = Record +export function visiblePatch(draft: RolePermissionDraft, visibleActionIds: string[]): RolePermissionChange[] +``` + +- [ ] **Step 3: Run tests and build** + +Run: `npx vitest run test/role-permissions.test.ts` +Run: `npm run build` +Expected: PASS. + +- [ ] **Step 4: Commit the client contract** + +```bash +git add src/api.ts src/rolePermissions.ts test/role-permissions.test.ts +git commit -m "Add the role permission client model" +``` + +### Task 7: Single-role and matrix configuration UI + +**Files:** +- Create: `Agent-System-Frontend/src/components/RolePermissionsPanel.tsx` +- Modify: `Agent-System-Frontend/src/components/AdminZone.tsx` +- Modify: `Agent-System-Frontend/src/components/AccountUsersPanel.tsx` +- Modify: `Agent-System-Frontend/src/styles.css` +- Test: `Agent-System-Frontend/test/role-permissions-ui.test.tsx` +- Test: `Agent-System-Frontend/test/design-guard.test.ts` + +**Interfaces:** +- Consumes Task 6 API and draft helpers plus existing `@ui` components and tokens. +- Produces the “角色与权限” system-management section and one-role assignment in user details. + +- [ ] **Step 1: Re-read Dev `DESIGN.md` and add failing interaction tests** + +Cover exact/fuzzy role search, state filtering, description edit, binding count, single-role edit, matrix edit, grouped paging, hidden-row draft retention, diff preview, conflict preservation, one user role replacement, system-grant editing with final-manager protection, and a late response discarded after switching roles. + +- [ ] **Step 2: Implement the panel using existing design tokens** + +Use `Collapsible` from `@ui`; consume `@ui/tokens.css` and `@ui/motion.css`; do not add raw colors, private keyframes, `
`, or inline font sizes. Keep one request sequence owner for list/detail/preview/commit results. + +- [ ] **Step 3: Replace the RDM role admin entry** + +Change the section label from “RDM 角色权限” to “角色与权限” and bind it to Backend APIs. Keep RDM project permission and document-duty components in project settings. + +- [ ] **Step 4: Add the user role selector** + +Display the current role and explicit system grants in account details. Allow role replacement only when the server advertises `system.roles.assign`; submit stable `userId`, `roleId`, expected state version, and idempotency key. Allow system-grant editing only with `system.permissions.manage`, using the server epoch and fixed action IDs. + +- [ ] **Step 5: Run Frontend and design checks** + +Run: `npm test` +Run: `npm run build` +Run paired UI: `npm test` in `Agent-System-UI`. +Expected: all pass with no design-guard regression. + +- [ ] **Step 6: Commit the UI** + +```bash +git add src/components/RolePermissionsPanel.tsx src/components/AdminZone.tsx src/components/AccountUsersPanel.tsx src/styles.css test/role-permissions-ui.test.tsx +git commit -m "Add role and permission configuration views" +``` + +### Task 8: Fixed-combination verification + +**Files:** +- Modify: `Agent-System-Dev/modules/backend` +- Modify: `Agent-System-Dev/modules/rdm` +- Modify: `Agent-System-Dev/modules/frontend` +- Modify: `Agent-System-Dev/modules/ui` +- Test evidence: `Agent-System-Dev/.task/feature-iam-u4-role-authority.md` + +**Interfaces:** +- Consumes reviewed component commits and trusted published tags where required. +- Produces one fixed Dev combination and requirement-to-evidence map. + +- [ ] **Step 1: Run component verification on exact commits** + +Backend: full SQLite, isolated PostgreSQL, migration rollback/retry, build. +RDM: full tests/build against the reviewed Backend public version. +Frontend/UI: full tests/build and design guards. + +- [ ] **Step 2: Run a real browser scenario on an isolated database** + +Create two ordinary users, one “软件工程师” role with a description, assign both users, replace one user's role, configure `project.create`, create projects A and B under different creators, and verify A's creator cannot manage B. Verify an unassigned system administrator cannot read either project's business content. Exercise exact/fuzzy search, filters, single-role edit, matrix edit, preview, conflict, and draft retention. + +- [ ] **Step 3: Run the Dev combination** + +Run: `make test` +Expected: PASS on the exact component SHAs recorded by `make evidence`. + +- [ ] **Step 4: Record remaining boundaries** + +Mark production migration, deployment, account recovery, multi-role, Agent permissions, and workflow changes `NOT_RUN / out of scope`. Do not claim them from component tests. diff --git a/docs/proposals/2026-10-15-iam-focused-role-permission-spec.md b/docs/proposals/2026-10-15-iam-focused-role-permission-spec.md index 0f13819..25f78c9 100644 --- a/docs/proposals/2026-10-15-iam-focused-role-permission-spec.md +++ b/docs/proposals/2026-10-15-iam-focused-role-permission-spec.md @@ -85,6 +85,8 @@ Backend 成为角色和系统权限的唯一写入权威,提供版本化接口 - `PUT /api/admin/roles/{roleId}/permissions`,批量提交固定 actionId 列表和 `expectedRoleVersion` - `PUT /api/admin/users/{userId}/role`,替换该用户唯一有效角色,使用用户状态版本和幂等键 - `GET /api/admin/users/{userId}/role` +- `GET /api/admin/users/{userId}/system-grants` +- `PUT /api/admin/users/{userId}/system-grants`,批量提交明确系统动作并使用 `expectedAuthorityEpoch` 和幂等键 - `GET /api/authz/explain`,仅解释调用者自己或其有权管理的对象 批量权限修改在单库事务内全部成功或全部失败;任一未知动作、越权、越上限或版本冲突不得部分写入。响应返回新版本、实际差异和审计回执标识。 @@ -102,7 +104,7 @@ RDM 通过公开、固定版本的 Backend 角色读取/判定契约消费角 - 过滤隐藏的单元格不丢失草稿;全选只作用于当前明确显示的固定 actionId 集合。 - 提交前显示差异;冲突保留草稿并要求刷新,不逐格即时写库。 -用户详情显示当前角色,并允许有 `system.roles.assign` 权限的操作者从有效角色中选择。用户可选资料继续用于展示、搜索和辨识,不参与权限计算。 +用户详情显示当前角色和明确系统授权来源,并允许有 `system.roles.assign` 权限的操作者从有效角色中选择。有 `system.permissions.manage` 权限的操作者可以编辑目标用户的系统授权,但不能撤销最后一个有效权限管理者。用户可选资料继续用于展示、搜索和辨识,不参与权限计算。 项目设置继续使用现有成员和权限入口,显示角色默认、角色上限、项目覆盖及最终允许/拒绝原因。项目创建者无法切换到其他项目后沿用原项目管理权。