Files
Agent-System-Documents/docs/proposals/2026-10-15-iam-focused-role-permission-implementation-plan.md
T
weichen e9ce9a539e 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
2026-10-14 16:42:24 +00:00

21 KiB

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

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
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
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

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
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

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
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

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
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
export type RolePermissionValue = { allowed: boolean; capAllowed: boolean }
export type RolePermissionDraft = Record<string, RolePermissionValue>
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
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, <details>, 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
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.