Skip to content

feat(server): add access control - #1398

Open
Teingi wants to merge 35 commits into
oceanbase:masterfrom
Teingi:codex/handoff-access-control-implementation
Open

feat(server): add access control#1398
Teingi wants to merge 35 commits into
oceanbase:masterfrom
Teingi:codex/handoff-access-control-implementation

Conversation

@Teingi

@Teingi Teingi commented Aug 30, 2026

Copy link
Copy Markdown
Member

Which issue or RFC does this PR close?

Implements #1395.

Design reference: #1396.

Rationale for this change

The current optional static Bearer token authenticates one deployment-local caller but cannot express per-user, per-scope, or exact-Handoff visibility. Handoff Receipts also record receiver observations; they are not authorization grants.

This change adds an independent Server Access Control boundary so user A can grant user B least-privilege access to one committed Handoff Revision without exposing the surrounding scope. Runtime domain APIs remain free of Principal, role, and permission parameters, and deployments can replace the built-in decision provider and relationship store with an external authorization system.

What changes are included in this PR?

  • Add typed Principal, action, structured resource, fixed role, binding, decision, and data-minimized audit models.
  • Add a built-in hierarchical RBAC provider and Server-owned relational binding/audit schema for SQLite, OceanBase, and seekDB.
  • Add replaceable AuthorizationProvider, RelationshipWriter, and AccessAuditStore boundaries for integrations such as OpenFGA, Casbin, Oso, or an AuthZEN-compatible PDP.
  • Add /v1/access/me, check, batch-check, safe resource listing, role listing, binding management, and audit APIs plus typed Python SDK methods.
  • Annotate protected OpenAPI operations with generated x-powercontext-access requirements and enforce them at one Server PEP before Runtime dependencies or business metrics.
  • Preserve the authenticated Principal across the MCP internal ASGI bridge while still executing authorization for logical MCP operations.
  • Add disabled, legacy-static-admin, and enforced rollout modes; the default legacy mode preserves the existing static-token administrator behavior.
  • Add English and Chinese configuration and HTTP API documentation.
  • Fix OceanBase Handoff trigger cursor creation to avoid an incompatible nested SAVEPOINT while retaining SQLite's concurrent-insert behavior.
  • Add focused Access Control, HTTP, MCP, SDK, OpenAPI, persistence, and Server tests.

Are there any user-facing changes?

Yes. Authenticated deployments gain public Access APIs, stable 403 behavior, exact-Handoff receiver grants, Access audit records, and two Access configuration settings. Existing static-token deployments remain compatible through the default legacy-static-admin mode.

The change adds Server-owned Access tables but does not add identity or ACL fields to Handoff, Source, Memory, Work, or other Runtime domain records. scope_id remains a business partition rather than an authorization credential.

How was this change tested?

  • PRE_COMMIT_HOME=/tmp/powercontext-prek-cache make check
  • .venv/bin/python -m pytest -q -p no:cacheprovider (974 passed, 9 skipped)
  • make contract-test (30 passed)
  • make docs-test
  • Real configured acceptance journey using the local .env OceanBase, generation LLM, and Embedding provider:
    • persisted and vector-searched Memory;
    • generated, finalized, and committed a real Handoff;
    • granted one exact Revision from A to B;
    • verified HTTP allow/deny boundaries, MCP Principal propagation, audit redaction, restart persistence, revocation, and post-revocation denial;
    • revoked the test binding and removed the failed-run Runtime fixture while preserving append-only low-sensitivity audit evidence.

AI usage statement

OpenAI Codex (GPT-5) was used to inspect the repository contracts, implement the Access Control layer and generated API changes, diagnose the real OceanBase SAVEPOINT failure, and run the validation described above. The author directed the design and publication scope.

@Teingi
Teingi marked this pull request as ready for review September 1, 2026 14:17
@Teingi Teingi changed the title feat(server): add Handoff access control feat(server): add access control Sep 3, 2026
…s-control-implementation

# Conflicts:
#	tests/test_transport.py
…s-control-implementation

# Conflicts:
#	src/powercontext/builtin/runtime/application.py
#	src/powercontext/builtin/runtime/composition.py
#	src/powercontext/client/__init__.py
#	src/powercontext/client/client.py
#	src/powercontext/server/app.py
…s-control-implementation

# Conflicts:
#	docs/en/docs/reference/configuration.md
#	docs/zh/docs/reference/configuration.md
#	integrations/dsh/plugins/powercontext/lib/index.js
#	integrations/dsh/plugins/powercontext/openapi/powercontext.yaml
#	integrations/dsh/plugins/powercontext/src/operations.generated.ts
#	integrations/opencode/plugins/powercontext/lib/index.js
#	integrations/opencode/plugins/powercontext/src/operations.generated.ts
#	integrations/pi/plugins/powercontext/src/operations.generated.ts
#	openapi/powercontext.yaml
#	src/powercontext/builtin/persistence/cursors.py
#	src/powercontext/client/__init__.py
#	src/powercontext/client/client.py
#	src/powercontext/http/__init__.py
#	src/powercontext/http/_generated/models.py
#	src/powercontext/http/_generated/operations.py
#	src/powercontext/http/_generated/schema.py
#	src/powercontext/server/app.py
#	src/powercontext/server/factory.py
#	src/powercontext/server/static/skills.js
#	src/powercontext/server/templates/pages/skills.html
#	src/powercontext/server/web.py
#	tests/e2e/real_experience_skill/harness.py
#	tests/test_api_contract.py
#	tests/test_dashboard.py
@Teingi

Teingi commented Sep 3, 2026

Copy link
Copy Markdown
Member Author
方法 接口 用途 成功返回
GET /v1/access/me 获取当前 Principal 及 Access 能力 AccessMeResponse
POST /v1/access/check 检查一组 all/any 组合权限要求 AccessCheckResponse
POST /v1/access/resources/list 列出当前 Principal 已经可见的资源 AccessResourcePage
POST /v1/access/roles/list 列出系统内置角色定义 AccessRolePage
POST /v1/access/bindings/list 查询管理边界内的授权关系 AccessBindingPage
POST /v1/access/bindings/create 幂等创建授权关系 AccessBinding,HTTP 201
POST /v1/access/bindings/revoke 通过版本号 CAS 撤销授权关系 更新后的 AccessBinding
POST /v1/access/bindings/replace 原子替换授权关系 AccessBindingReplacement
POST /v1/access/audit/list 查询最小化的 Access 审计记录 AccessAuditPage

Comment thread src/powercontext/server/authz/models.py
Comment thread src/powercontext/server/app.py Outdated
Comment thread src/powercontext/server/settings.py
@thunguo
thunguo self-requested a review September 3, 2026 10:24
Comment thread src/powercontext/http/_generated/models.py
Comment thread src/powercontext/server/app.py
Comment thread openapi/powercontext.yaml Outdated
Comment thread src/powercontext/server/authz/service.py
Comment thread src/powercontext/server/authz/authzen.py
Comment thread src/powercontext/server/authz/models.py
…s-control-implementation

# Conflicts:
#	integrations/dsh/plugins/powercontext/lib/index.js
#	integrations/dsh/plugins/powercontext/openapi/powercontext.yaml
#	integrations/dsh/plugins/powercontext/src/operations.generated.ts
#	integrations/opencode/plugins/powercontext/lib/index.js
#	integrations/opencode/plugins/powercontext/src/operations.generated.ts
#	integrations/pi/plugins/powercontext/src/operations.generated.ts
#	openapi/powercontext.yaml
#	scripts/generate_api.py
#	src/powercontext/builtin/artifacts/handoff/service.py
#	src/powercontext/client/client.py
#	src/powercontext/http/__init__.py
#	src/powercontext/http/_generated/models.py
#	src/powercontext/http/_generated/operations.py
#	src/powercontext/http/_generated/schema.py
#	src/powercontext/server/app.py
#	src/powercontext/server/settings.py
#	src/powercontext/server/web.py
#	tests/test_api_contract.py
#	tests/test_client.py
#	tests/test_dashboard.py
@tlyyxjz

tlyyxjz commented Sep 4, 2026

Copy link
Copy Markdown

Following up from #1395 — congrats on landing this so quickly, the boundary design looks clean (single PEP, fail-closed modes, data-minimized audit are all the right calls).

Since the AuthorizationProvider boundary explicitly names Casbin as an integration target, a note from the Casbin ecosystem side on what a first integration would look like:

Fit check against the built-in provider's model. The PR's hierarchical RBAC (subject / action / resource / context + role bindings) maps 1:1 onto casbin's rbac_with_domains pattern — p, role, action, resource, dom policy rows + g, user, role, scope bindings, with the same semantics as your built-in hierarchical resolution. Two things casbin would add beyond the built-in provider:

  1. Wildcard/ABAC matchers for cases like "readonly can GET everything under a scope" without enumerating rows, and attribute conditions (e.g. revision-time windows) in the matcher instead of pre-filtering.
  2. Policy storage via existing adapters — for PowerContext the natural one is the SQLAlchemy/Python adapter (pycasbin), reusing your OceanBase/seekDB connection config so policies live beside the binding tables you already created.

Concrete offer: I'm happy to prototype a casbin_provider.py implementing your AuthorizationProvider protocol (check + batch-check backed by pycasbin, with your existing binding schema imported into policy on first load) as a reference integration — it would double as a validation that the boundary is truly engine-agnostic. We also have casbin-config-doctor (offline model/policy diagnostics with near-miss attribution) which would help your users debug "why was this 403" once custom providers land — 403-without-explanation is the #1 support burden of embedded authz engines.

One question: for the handoff.receiver exact-revision grant — do you intend those to stay in the built-in relationship store only, or should external providers be able to write grants via RelationshipWriter too? That determines whether a casbin integration should treat casbin as the source of truth for bindings or as a read-only decision mirror.

@Teingi

Teingi commented Sep 4, 2026

Copy link
Copy Markdown
Member Author

@tlyyxjz Thanks for the thoughtful offer. This PR already includes an embedded CasbinAuthorizationProvider and open_casbin_access_control. In the first version, the canonical relational Access repository remains the source of truth for bindings and ownership; the adapter materializes active relationships into a fresh Casbin enforcer for decisions instead of maintaining a second persisted Casbin policy store.

RelationshipWriter is intentionally separate from AuthorizationProvider. An external decision provider may supply a paired writer and declare relationship_management=true; without one, binding mutations fail with relationship_management_unavailable and relationships are administered in the external system. Receiver and other bindings are therefore not restricted to the built-in store. Also, handoff.receiver targets the logical Handoff identity across revisions, rather than an exact-revision grant.

I clarified this composition and source-of-truth boundary in f6d2600. A native Casbin-backed writer could be a useful follow-up if it preserves the same idempotency, versioning, ownership, audit, and safe-filtering contracts.

…s-control-implementation

# Conflicts:
#	docs/en/docs/reference/configuration.md
#	docs/zh/docs/reference/configuration.md
#	integrations/dsh/plugins/powercontext/lib/index.js
#	integrations/dsh/plugins/powercontext/src/operations.generated.ts
#	integrations/opencode/plugins/powercontext/src/operations.generated.ts
#	integrations/pi/plugins/powercontext/src/operations.generated.ts
#	openapi/powercontext.yaml
#	scripts/generate_api.py
#	src/powercontext/client/client.py
#	src/powercontext/http/__init__.py
#	src/powercontext/http/_generated/models.py
#	src/powercontext/http/_generated/operations.py
#	src/powercontext/http/_generated/schema.py
#	src/powercontext/server/app.py
#	src/powercontext/server/factory.py
#	tests/test_client.py
#	uv.lock
@tlyyxjz

tlyyxjz commented Sep 5, 2026

Copy link
Copy Markdown

Thanks for the detailed breakdown — that composition makes sense: enforcer materialized fresh from the relational source of truth on each decision cycle avoids the dual-write consistency problem entirely.

Two follow-ups on the native Casbin-backed RelationshipWriter you mentioned:

  1. We'd like to take a shot at it. A writer backed by pycasbin (the Python port, same org, actively maintained) can preserve those contracts:

    • Idempotency: add_grouping_policy is a no-op on duplicates, and we'd key mutations on the binding's (principal, role, resource) triple so retries converge.
    • Versioning/ownership: writes flow through your existing binding mutation path with the writer acting as the policy store behind it — audit events emitted at the same boundary as the built-in writer.
    • Safe-filtering: Casbin's filtered adapter (load_filtered_policy) maps cleanly to your per-principal view materialization, so the enforcer only ever loads the subject's reachable subtree.
  2. One design question first: for handoff.receiver, the writer would need to express "logical Handoff identity across revisions" as a stable p/g subject — is there a doc or test fixture for the RelationshipWriter contract (the relationship_management=true declaration path) we should read first? We'd like to sanity-check the scope against those contracts before promising a shape.

If the spec looks like a fit, we're glad to prototype a writer + filtered-adapter sync and open a draft PR for review.

@Teingi

Teingi commented Sep 5, 2026

Copy link
Copy Markdown
Member Author

@tlyyxjz Thanks for offering to work on this. I'd be happy to review a separate draft PR for a native Casbin writer. Starting with the contract and a small working example makes sense.

The most useful references are:

For the Handoff identity, this is the resource we use:

ResourceRef.artifact(
    "scope-a",
    family="handoff",
    artifact_id="handoff-a",
)

Its canonical key includes the scope, family, and artifact ID. It has no revision field. A handoff.receiver binding therefore applies to that logical Handoff across revisions. If revision 2 is created, the receiver does not need another grant. The binding's own version is separate and tracks changes to the access relationship. The Casbin model can choose how to encode this resource, as long as it preserves that identity and keeps different scopes separate.

On idempotency, add_grouping_policy handles duplicate rules, but the public API also needs to remember the request. A retry with the same caller, idempotency key, and payload must return the same binding. Reusing that key with a different payload must return 409, even if the principal/role/resource triple is unchanged. Changing the expiry is one example. We also need to distinguish a retry of an old grant from a new grant after revocation. The triple alone doesn't carry enough information for those cases; the writer will need durable binding and request metadata somewhere.

Your suggestion to keep writes behind the existing service boundary works for authorization checks and audit emission. The storage side still has to implement the version checks and atomic replacement. For handoff.receiver, there can be at most one active direct receiver binding per logical Handoff. Replacing Bob with Alice must check expected_version and revoke the old binding and create the new one atomically. A stale competing update must fail, and retrying a successful replacement must return the same result. These guarantees currently live in the relational repository, so passing through the service alone won't supply them to a new backend. Ownership also has its own operations and is system-managed; the public Binding APIs cannot assign or transfer it.

For filtering, load_filtered_policy looks useful for limiting what an enforcer loads. We also need the provider to implement resolve_resource_filter: given the caller and action, return the authorized logical resource keys and parent constraints, with a policy revision. The repository applies that filter before counts, sorting, and pagination. Building it needs to account for active bindings, expiry, ownership, trusted groups where supported, and the fixed parent-role implications. Loading a subject's policy rows is only part of that work. If the adapter cannot produce a complete, safe filter for a list operation, that operation must fail closed and the capability declaration must reflect the limitation.

For the proposed sync, please describe where the binding IDs, request metadata, versions, and ownership records would live, and how writes become visible to decisions. I want to avoid ending up with two independently writable copies of the same relationship. A short explanation of the storage layout and failure behavior would help us review that before the implementation gets large.

I'd start the prototype with one Handoff: establish its owner, grant Bob receiver access, publish another revision, replace Bob with Alice, retry that replacement, then revoke it. Run the permission checks and resource listing throughout, including a caller with no grant and a Handoff in another scope. That would give us something concrete to review early. The relationship_management=true declaration can follow once the complete relationship contract is covered.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants