Skip to content

feat(admin)!: remove the Admin API resource write path - #915

Merged
moonming merged 5 commits into
mainfrom
feat/remove-admin-write-path
Aug 11, 2026
Merged

feat(admin)!: remove the Admin API resource write path#915
moonming merged 5 commits into
mainfrom
feat/remove-admin-write-path

Conversation

@moonming

@moonmingmoonming commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Removes the Admin API resource write path. The admin listener (:3001) keeps its read surface — lists/gets for all 8 resource kinds (including the former apikeys spelling), /admin/v1/models/status, /admin/v1/health, OpenAPI + Scalar UI, playground, /livez, /readyz — but resources are now managed exclusively through the declarative paths: a resources_file (resources.yaml, reloaded on SIGHUP) or direct etcd writes.

This is the final step of the deprecation announced in v0.4.0 (RFC 9745 Deprecation headers) and executes the removal scheduled for v0.5.0+ after #848 lifted the write-path-exclusive validations into the canonical schemas so the declarative paths enforce them.

⚠️ Breaking changes

BeforeAfter
POST /admin/v1/<kind>, PUT/DELETE /admin/v1/<kind>/{id} (deprecated, functional)405 with Allow: GET
POST /admin/v1/api_keys/{id}/rotate (and apikeys spelling)404 — the route is gone
File mode: writes rejected with 409 naming the resources file405, same as every other mode
Rotate returned a fresh plaintext keyNo DP-side plaintext rotation. Rotate declaratively: write the same resource id with a new key_hash — the old plaintext stops authenticating as soon as the write propagates (pinned in apikey-lifecycle-e2e)

What to update:

  • Scripts that created/updated/deleted resources via :3001 → write resources.yaml (validate offline with aisix validate --resources <file>, reload with SIGHUP) or write entity-value JSON to etcd at {prefix}/{kind}/{id}.
  • Scripts that rotated caller keys via /rotate → hash the new secret client-side and update the resource's key_hash.
  • The published OpenAPI (/admin/openapi.json) no longer documents write operations; the write-only component schemas (ApiKeyRequest, ApiKeyEntry, ApiKeyRotateResponse, DeleteResponse) are gone from components.schemas.

What changed

Router / handlers — every /admin/v1/* resource route serves get(...) only; both rotate routes deleted; the file-managed write guard and the RFC 9745 deprecation-header middleware deleted (nothing left to mark). 43 write/rotate/uniqueness handler fns removed across the 8 handler modules.

Store layerConfigStore is now a read-only trait (16 put_*/delete_* methods removed, StoreError::ReadOnly gone). EtcdConfigStore keeps only reads (module doc rewritten: resources reach etcd through the declarative paths). FileManagedStore::new(snapshot) drops the path parameter — read-only by construction. InMemoryStore keeps #[cfg(test)] inherent write methods used by unit-test seeding.

OpenAPI — 24 write operations and the rotate path removed from the base document; the no-op write-deprecation marker pass deleted; unreachable component schemas pruned via a reachability walk from paths. New gate test pins that the published reference documents zero non-GET operations under /admin/v1/ and zero deprecated marks anywhere.

Tests — write-path unit tests (CRUD flows, rotate atomicity, write-auth, write-validation) deleted; read tests re-seeded through InMemoryStore; new contract tests pin 405 + Allow: GET on every collection/:id route (auth'd and unauthenticated) and 404 on both rotate spellings. The etcd integration test now seeds via direct etcd_client puts — the path operators actually use.

e2eAdminClient write helpers removed (reads stay; SeedClient is the write front door). file-resource-source-e2e pins the new file-mode contract (reads serve the file, writes 405, rotate 404, unauthenticated write 405 with no file-path leak). apikey-lifecycle-e2e's rotate coverage became a declarative secret-swap test (old plaintext dies immediately, id unchanged). Deleted: seed-vs-admin-characterization-e2e (its own comment scheduled retirement once the seed migration completed), apikey-budget-e2e (tested write-path 400s), the sdk-compat deprecation-header test, and forward-compat's admin strict-write test.

Docs — README, config.example.yaml, crate module docs updated to the read-only story.

Non-goals / follow-ups (filed internally)

  • 3 OpenAPI-parsing validations (mcp validate_spec, HeaderName typing, duplicate tool names) still lack declarative-path enforcement — blocked on a dependency-direction extraction, tracked internally.
  • etcd watch keeps last-good on an invalid PUT while resync drops the row — policy decision tracked internally.

Verification

  • cargo fmt --check, cargo clippy --workspace --all-targets -- -D warnings, cargo test --workspace — all green.
  • Real binary, file mode: boots with resources.yaml, GET list serves the file, POST /admin/v1/models → 405 + Allow: GET, rotate → 404, /admin/openapi.json documents no admin writes.
  • Local e2e stack run before push.

Summary by CodeRabbit

  • Changes
    • The Admin API is now read-only for managed resources.
    • Resource listing and detail views remain available through GET requests.
    • Creation, updates, deletion, and API-key rotation through the Admin API are no longer supported.
    • Unsupported write requests return 405 responses, while rotation routes return 404.
    • Manage resources through resources_file reloads or direct etcd writes.
    • Updated configuration, validation guidance, and end-to-end coverage to reflect the read-only administration model.

CopilotAI balanced review requested due to automatic review settings August 10, 2026 05:09
@coderabbitai

coderabbitaiBot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9acb9ae1-7d4d-42ce-83f0-61de46c03b1a

📥 Commits

Reviewing files that changed from the base of the PR and between f9c10cc and f43eab2.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (25)
  • README.md
  • config.example.yaml
  • crates/aisix-admin/src/a2a_agents_handlers.rs
  • crates/aisix-admin/src/apikeys_handlers.rs
  • crates/aisix-admin/src/cache_policies_handlers.rs
  • crates/aisix-admin/src/guardrails_handlers.rs
  • crates/aisix-admin/src/lib.rs
  • crates/aisix-admin/src/mcp_servers_handlers.rs
  • crates/aisix-admin/src/models_handlers.rs
  • crates/aisix-admin/src/observability_exporters_handlers.rs
  • crates/aisix-admin/src/openapi.rs
  • crates/aisix-admin/src/provider_keys_handlers.rs
  • crates/aisix-admin/src/store.rs
  • crates/aisix-admin/tests/etcd_integration.rs
  • crates/aisix-core/src/bin/dump-schema.rs
  • crates/aisix-core/src/models/mod.rs
  • crates/aisix-core/src/models/schema.rs
  • crates/aisix-server/src/main.rs
  • schemas/README.md
  • tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts
  • tests/e2e/src/cases/config-forward-compat-e2e.test.ts
  • tests/e2e/src/cases/openai-sdk-compat.test.ts
  • tests/e2e/src/cases/smoke.test.ts
  • tests/e2e/src/harness/app.ts
  • tests/e2e/src/harness/seed.ts
💤 Files with no reviewable changes (1)
  • tests/e2e/src/cases/smoke.test.ts
🚧 Files skipped from review as they are similar to previous changes (12)
  • crates/aisix-server/src/main.rs
  • config.example.yaml
  • crates/aisix-admin/src/provider_keys_handlers.rs
  • crates/aisix-admin/src/models_handlers.rs
  • schemas/README.md
  • crates/aisix-core/src/bin/dump-schema.rs
  • tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts
  • tests/e2e/src/cases/config-forward-compat-e2e.test.ts
  • crates/aisix-admin/src/apikeys_handlers.rs
  • README.md
  • crates/aisix-admin/tests/etcd_integration.rs
  • crates/aisix-admin/src/lib.rs

📝 Walkthrough

Walkthrough

The admin listener now exposes read-only resource routes. Resource writes use resources_file reloads, direct etcd writes, or declarative seed helpers. Stores, server wiring, tests, and documentation were updated.

Changes

Read-only admin resource surface

Layer / File(s)Summary
Read-only store contracts
crates/aisix-admin/src/store.rs, crates/aisix-admin/src/etcd_store.rs, crates/aisix-admin/src/file_store.rs, crates/aisix-admin/src/error.rs, crates/aisix-admin/src/state.rs
Store traits and implementations now support resource reads only.
GET-only admin routes
crates/aisix-admin/src/lib.rs, crates/aisix-admin/src/*_handlers.rs
Create, update, delete, and API-key rotation routes were removed.
Read-only server wiring
crates/aisix-server/src/main.rs
The server constructs read-only file-backed admin state and removes the file-managed write guard.
Direct etcd setup and integration coverage
crates/aisix-admin/tests/etcd_integration.rs, crates/aisix-admin/src/etcd_store.rs
Tests seed canonical documents directly in etcd and verify read-only behavior.
Declarative E2E flows and documentation
tests/e2e/src/cases/*, tests/e2e/src/harness/*, README.md, config.example.yaml, schemas/README.md
E2E setup and documentation now use declarative resource-management paths.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
participant EtcdClient
participant AdminAPI
participant ConfigLoader
EtcdClient->>EtcdClient: write canonical resource document
AdminAPI->>EtcdClient: list/get resource
EtcdClient-->>AdminAPI: resource document
ConfigLoader->>EtcdClient: load canonical resource documents
EtcdClient-->>ConfigLoader: configuration data
Loading

Possibly related PRs

  • api7/aisix#792: Related through direct etcd seeding and readiness migration.
  • api7/aisix#800: Related through removal of Admin API writes and E2E harness changes.
  • api7/aisix#848: Related through A2A and MCP admin-handler changes.

Suggested reviewers:jarvis9443


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error, 1 warning)

Check nameStatusExplanationResolution
Security Check❌ ErrorCategory 1: admin GETs serialize ProviderKey.api_key, McpServer.secret, and A2aAgent.secret; PublicApiKey also returns key_hash without redaction.Return redacted DTOs for every secret-bearing resource, omit key_hash and credential fields, and replace backend-detail error responses with generic messages.
E2e Test Quality Review⚠️ WarningE2E quality issue: config-forward-compat-e2e.test.ts test 3 deletes yellowKeyId created only by test 1, with no ordering declaration, creating a hidden test dependency.Make each test self-contained by seeding and cleaning its own rows, or explicitly declare and document sequential execution; also keep route-matrix coverage at the process level if all aliases are contractual.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly and concisely identifies the main breaking change: removing the Admin API resource write path.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/remove-admin-write-path

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/aisix-admin/tests/etcd_integration.rs (1)

383-489: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Add the missing a2a_agents canonical document.

The PR retains eight resource kinds, but writes contains seven entries. It omits a2a_agents. Add a valid A2A agent document, assert stats.accepted == 8, and assert snap.a2a_agents.len() == 1.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/aisix-admin/tests/etcd_integration.rs` around lines 383 - 489, Extend
the writes array with a valid a2a_agents canonical document using the existing
seed flow, then update the accepted-entry assertion from 7 to 8. Add a
corresponding snap.a2a_agents length assertion expecting exactly one loaded
agent, leaving the other resource assertions unchanged.
🧹 Nitpick comments (1)
crates/aisix-admin/src/lib.rs (1)

1131-1198: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Read coverage now spans only two of eight resource kinds. Deleting the write handlers also deleted each handler module's test module. The replacement read tests in lib.rs seed only models and api_keys. The other six kinds — provider_keys, guardrails, cache_policies, observability_exporters, mcp_servers, a2a_agents — have 405 write-refusal coverage but no test proving that GET serves a seeded entry. These handler bodies are hand-written per module, not macro-generated, so a wrong store call or a wrong response shape in one of the six would pass CI.

  • crates/aisix-admin/src/lib.rs#L1131-L1198: extend build_seedable_state with seed helpers for the remaining six kinds, then add list and get-by-id assertions for each, mirroring list_models_returns_seeded_entries and get_model_serves_seeded_entry.
  • crates/aisix-admin/src/a2a_agents_handlers.rs#L12: add a test that list_a2a_agents and get_a2a_agent return a seeded A2aAgent, or confirm the new lib.rs tests cover this module.
  • crates/aisix-admin/src/mcp_servers_handlers.rs#L9: add the same list/get coverage for McpServer, or confirm the new lib.rs tests cover this module.

Note that InMemoryStore currently exposes only put_model and put_apikey as #[cfg(test)] helpers, so seeding the other six kinds requires adding matching helpers in crates/aisix-admin/src/store.rs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/aisix-admin/src/lib.rs` around lines 1131 - 1198, Extend
crates/aisix-admin/src/store.rs with cfg(test) put helpers for provider_keys,
guardrails, cache_policies, observability_exporters, mcp_servers, and
a2a_agents, then update build_seedable_state and the tests in
crates/aisix-admin/src/lib.rs#L1131-L1198 to seed each kind and assert both list
and get-by-id responses, mirroring the model tests. Ensure
crates/aisix-admin/src/a2a_agents_handlers.rs#L12 and
crates/aisix-admin/src/mcp_servers_handlers.rs#L9 are covered by these lib.rs
tests; no direct handler changes are required unless coverage cannot exercise
their list/get functions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/aisix-admin/src/lib.rs`:
- Around line 1200-1230: Remove the stale rotation-coverage comment immediately
before openapi_apikey_schema_excludes_max_budget_usd, and delete the empty
CachePolicy CRUD and Health endpoint section comments. Preserve the
guardrail_payload function, the ObservabilityExporter CRUD comment, and all
surrounding tests and formatting.
In `@crates/aisix-admin/tests/etcd_integration.rs`:
- Around line 317-354: Extend the resource-route assertions in the integration
test to issue both PUT and DELETE requests to /admin/v1/models, asserting each
returns METHOD_NOT_ALLOWED and includes GET in the Allow header. Keep the
existing POST check and final etcd emptiness assertion so all refused write
methods verify that no data is written.
In `@crates/aisix-server/src/main.rs`:
- Around line 898-902: Update the file-source match arm in the admin_store
initialization to bind the second tuple element as Some(_) instead of
Some(path), preserving the existing condition and FileManagedStore construction.
In `@tests/e2e/src/cases/file-resource-source-e2e.test.ts`:
- Around line 179-213: Update the rejected-write assertions in the e2e test to
require the exact Allow header value "GET" instead of merely containing GET,
covering both the authenticated POST/DELETE responses and the unauthenticated
POST response. Preserve the existing status and response-body assertions.
In `@tests/e2e/src/cases/openai-sdk-compat.test.ts`:
- Around line 54-84: After seeding the API key in the test setup, add an
independent readiness poll using the seeded caller credentials against
authenticated GET /v1/models, continuing until it returns 200. Remove the
client.chat.completions.create-based propagation gate and keep that call
exclusively for the SDK chat behavior under test.
---
Outside diff comments:
In `@crates/aisix-admin/tests/etcd_integration.rs`:
- Around line 383-489: Extend the writes array with a valid a2a_agents canonical
document using the existing seed flow, then update the accepted-entry assertion
from 7 to 8. Add a corresponding snap.a2a_agents length assertion expecting
exactly one loaded agent, leaving the other resource assertions unchanged.
---
Nitpick comments:
In `@crates/aisix-admin/src/lib.rs`:
- Around line 1131-1198: Extend crates/aisix-admin/src/store.rs with cfg(test)
put helpers for provider_keys, guardrails, cache_policies,
observability_exporters, mcp_servers, and a2a_agents, then update
build_seedable_state and the tests in crates/aisix-admin/src/lib.rs#L1131-L1198
to seed each kind and assert both list and get-by-id responses, mirroring the
model tests. Ensure crates/aisix-admin/src/a2a_agents_handlers.rs#L12 and
crates/aisix-admin/src/mcp_servers_handlers.rs#L9 are covered by these lib.rs
tests; no direct handler changes are required unless coverage cannot exercise
their list/get functions.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f3cea19a-3b55-4ad2-b83e-bc29037270c5

📥 Commits

Reviewing files that changed from the base of the PR and between b77f1a6 and 152c57d.

📒 Files selected for processing (26)
  • README.md
  • config.example.yaml
  • crates/aisix-admin/src/a2a_agents_handlers.rs
  • crates/aisix-admin/src/apikeys_handlers.rs
  • crates/aisix-admin/src/cache_policies_handlers.rs
  • crates/aisix-admin/src/error.rs
  • crates/aisix-admin/src/etcd_store.rs
  • crates/aisix-admin/src/file_store.rs
  • crates/aisix-admin/src/guardrails_handlers.rs
  • crates/aisix-admin/src/lib.rs
  • crates/aisix-admin/src/mcp_servers_handlers.rs
  • crates/aisix-admin/src/models_handlers.rs
  • crates/aisix-admin/src/observability_exporters_handlers.rs
  • crates/aisix-admin/src/openapi.rs
  • crates/aisix-admin/src/provider_keys_handlers.rs
  • crates/aisix-admin/src/state.rs
  • crates/aisix-admin/src/store.rs
  • crates/aisix-admin/tests/etcd_integration.rs
  • crates/aisix-server/src/main.rs
  • tests/e2e/src/cases/apikey-budget-e2e.test.ts
  • tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts
  • tests/e2e/src/cases/config-forward-compat-e2e.test.ts
  • tests/e2e/src/cases/file-resource-source-e2e.test.ts
  • tests/e2e/src/cases/openai-sdk-compat.test.ts
  • tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts
  • tests/e2e/src/harness/admin.ts
💤 Files with no reviewable changes (8)
  • tests/e2e/src/cases/apikey-budget-e2e.test.ts
  • tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts
  • crates/aisix-admin/src/state.rs
  • crates/aisix-admin/src/observability_exporters_handlers.rs
  • crates/aisix-admin/src/cache_policies_handlers.rs
  • crates/aisix-admin/src/models_handlers.rs
  • crates/aisix-admin/src/guardrails_handlers.rs
  • crates/aisix-admin/src/provider_keys_handlers.rs

Comment threadcrates/aisix-admin/src/lib.rs Outdated
Comment threadcrates/aisix-admin/tests/etcd_integration.rs Outdated
Comment threadcrates/aisix-server/src/main.rs
Comment on lines +179 to +213
expect(postRes.status).toBe(405);
expect(postRes.headers.get("allow")).toContain("GET");
await postRes.text();

// The refused write did not change the resource set.
const relist = await fetch(`${app.adminUrl}/admin/v1/models`, { headers: auth });
expect(((await relist.json()) as unknown[]).length).toBe(2);

// DELETE and rotate are covered by the same guard.
const delRes = await fetch(`${app.adminUrl}/admin/v1/models/any-id`, {
method: "DELETE",
headers: auth,
});
expect(delRes.status).toBe(409);
expect(delRes.status).toBe(405);
expect(delRes.headers.get("allow")).toContain("GET");
await delRes.text();

// The rotate route was removed outright (it was POST-only), so the
// path no longer exists at all.
const rotateRes = await fetch(`${app.adminUrl}/admin/v1/api_keys/any-id/rotate`, {
method: "POST",
headers: auth,
});
expect(rotateRes.status).toBe(409);
expect(rotateRes.status).toBe(404);
await rotateRes.text();

// Auth ordering: an UNAUTHENTICATED write still gets 401, and the
// 401 body must not leak the resources-file path (that detail is
// only for authenticated admins).
// Method routing answers before auth: an unauthenticated write gets
// the same 405 — there is no write endpoint left to protect, and
// the 405 body carries no resources-file detail to leak.
const unauthed = await fetch(`${app.adminUrl}/admin/v1/models`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ display_name: "nope" }),
});
expect(unauthed.status).toBe(401);
const unauthedBody = (await unauthed.json()) as { error_msg: string };
expect(unauthedBody.error_msg).not.toContain(app.resourcesPath!);
expect(unauthed.status).toBe(405);
expect((await unauthed.text())).not.toContain(app.resourcesPath!);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert the exact Allow header value.

Use toBe("GET") for each rejected write. Add the same assertion for the unauthenticated request. toContain("GET") also accepts an invalid value such as GET, POST.

Based on PR objectives, rejected resource writes must return 405 with Allow: GET.

Proposed test update
- expect(postRes.headers.get("allow")).toContain("GET");+ expect(postRes.headers.get("allow")).toBe("GET");
...
- expect(delRes.headers.get("allow")).toContain("GET");+ expect(delRes.headers.get("allow")).toBe("GET");
...
expect(unauthed.status).toBe(405);
+ expect(unauthed.headers.get("allow")).toBe("GET");
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
expect(postRes.status).toBe(405);
expect(postRes.headers.get("allow")).toContain("GET");
awaitpostRes.text();
// The refused write did not change the resource set.
constrelist=awaitfetch(`${app.adminUrl}/admin/v1/models`,{headers: auth});
expect(((awaitrelist.json())asunknown[]).length).toBe(2);
// DELETE and rotate are covered by the same guard.
constdelRes=awaitfetch(`${app.adminUrl}/admin/v1/models/any-id`,{
method: "DELETE",
headers: auth,
});
expect(delRes.status).toBe(409);
expect(delRes.status).toBe(405);
expect(delRes.headers.get("allow")).toContain("GET");
awaitdelRes.text();
// The rotate route was removed outright (it was POST-only), so the
// path no longer exists at all.
constrotateRes=awaitfetch(`${app.adminUrl}/admin/v1/api_keys/any-id/rotate`,{
method: "POST",
headers: auth,
});
expect(rotateRes.status).toBe(409);
expect(rotateRes.status).toBe(404);
awaitrotateRes.text();
// Auth ordering: an UNAUTHENTICATED write still gets 401, and the
// 401 body must not leak the resources-file path (that detail is
// only for authenticated admins).
// Method routing answers before auth: an unauthenticated write gets
// the same 405 — there is no write endpoint left to protect, and
// the 405 body carries no resources-file detail to leak.
constunauthed=awaitfetch(`${app.adminUrl}/admin/v1/models`,{
method: "POST",
headers: {"content-type": "application/json"},
body: JSON.stringify({display_name: "nope"}),
});
expect(unauthed.status).toBe(401);
constunauthedBody=(awaitunauthed.json())as{error_msg: string};
expect(unauthedBody.error_msg).not.toContain(app.resourcesPath!);
expect(unauthed.status).toBe(405);
expect((awaitunauthed.text())).not.toContain(app.resourcesPath!);
expect(postRes.status).toBe(405);
expect(postRes.headers.get("allow")).toBe("GET");
awaitpostRes.text();
// The refused write did not change the resource set.
constrelist=awaitfetch(`${app.adminUrl}/admin/v1/models`,{headers: auth});
expect(((awaitrelist.json())asunknown[]).length).toBe(2);
constdelRes=awaitfetch(`${app.adminUrl}/admin/v1/models/any-id`,{
method: "DELETE",
headers: auth,
});
expect(delRes.status).toBe(405);
expect(delRes.headers.get("allow")).toBe("GET");
awaitdelRes.text();
// The rotate route was removed outright (it was POST-only), so the
// path no longer exists at all.
constrotateRes=awaitfetch(`${app.adminUrl}/admin/v1/api_keys/any-id/rotate`,{
method: "POST",
headers: auth,
});
expect(rotateRes.status).toBe(404);
awaitrotateRes.text();
// Method routing answers before auth: an unauthenticated write gets
// the same 405 — there is no write endpoint left to protect, and
// the 405 body carries no resources-file detail to leak.
constunauthed=awaitfetch(`${app.adminUrl}/admin/v1/models`,{
method: "POST",
headers: {"content-type": "application/json"},
body: JSON.stringify({display_name: "nope"}),
});
expect(unauthed.status).toBe(405);
expect(unauthed.headers.get("allow")).toBe("GET");
expect((awaitunauthed.text())).not.toContain(app.resourcesPath!);
🧰 Tools
🪛 ast-grep (0.45.0)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { execFile } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/e2e/src/cases/file-resource-source-e2e.test.ts` around lines 179 - 213,
Update the rejected-write assertions in the e2e test to require the exact Allow
header value "GET" instead of merely containing GET, covering both the
authenticated POST/DELETE responses and the unauthenticated POST response.
Preserve the existing status and response-body assertions.

Comment threadtests/e2e/src/cases/openai-sdk-compat.test.ts

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Removes Admin API resource writes, leaving read-only resource endpoints and moving management to declarative file or etcd paths.

Changes:

  • Removes write handlers, routes, store operations, rotation, and deprecation middleware.
  • Updates OpenAPI, documentation, and tests for the read-only contract.
  • Migrates test setup to direct etcd seeding.

Reviewed changes

Copilot reviewed 25 out of 26 changed files in this pull request and generated 12 comments.

Show a summary per file
FileDescription
README.mdDocuments the read-only Admin API.
config.example.yamlClarifies declarative resource management.
crates/aisix-server/src/main.rsWires read-only admin stores.
crates/aisix-admin/src/lib.rsRemoves write routes and middleware.
crates/aisix-admin/src/openapi.rsRemoves write operations and schemas.
crates/aisix-admin/src/store.rsMakes ConfigStore read-only.
crates/aisix-admin/src/state.rsRemoves file-write guard state.
crates/aisix-admin/src/error.rsRemoves write-related errors.
crates/aisix-admin/src/file_store.rsRetains snapshot reads only.
crates/aisix-admin/src/etcd_store.rsRetains etcd reads only.
crates/aisix-admin/src/models_handlers.rsRemoves model writes.
crates/aisix-admin/src/apikeys_handlers.rsRemoves API-key writes and rotation.
crates/aisix-admin/src/provider_keys_handlers.rsRemoves provider-key writes.
crates/aisix-admin/src/guardrails_handlers.rsRemoves guardrail writes.
crates/aisix-admin/src/cache_policies_handlers.rsRemoves cache-policy writes.
crates/aisix-admin/src/observability_exporters_handlers.rsRemoves exporter writes.
crates/aisix-admin/src/mcp_servers_handlers.rsRemoves MCP-server writes.
crates/aisix-admin/src/a2a_agents_handlers.rsRemoves A2A-agent writes.
crates/aisix-admin/tests/etcd_integration.rsTests direct-etcd writes and admin reads.
tests/e2e/src/harness/admin.tsRemoves Admin API write helpers.
tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.tsRemoves obsolete path comparison tests.
tests/e2e/src/cases/openai-sdk-compat.test.tsMigrates setup to etcd seeding.
tests/e2e/src/cases/file-resource-source-e2e.test.tsTests file-mode read-only behavior.
tests/e2e/src/cases/config-forward-compat-e2e.test.tsRemoves Admin write validation coverage.
tests/e2e/src/cases/apikey-lifecycle-e2e.test.tsReplaces rotation with declarative secret swapping.
tests/e2e/src/cases/apikey-budget-e2e.test.tsRemoves obsolete write-path validation test.
Suppressed comments (1)

crates/aisix-admin/src/mcp_servers_handlers.rs:9

  • This removal also eliminates the only production call to aisix_mcp::validate_spec. The file and etcd loaders only run the core JSON Schema, which does not reject specs with zero generatable operations or colliding sanitized tool names, so the newly exclusive declarative paths accept configurations the former write API rejected. Add the semantic validation to both loaders before removing this path.
use aisix_core::McpServer;

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment threadcrates/aisix-server/src/main.rs Outdated
use serde::{Deserialize, Serialize};
use serde_json::Value;
use uuid::Uuid;
use serde::Serialize;
Comment threadcrates/aisix-server/src/main.rs Outdated
Comment on lines 9 to 10
//! replaces the `key` field with a freshly-generated `sk-*` value and
//! bumps the revision, invalidating the old credential.
Comment on lines 8 to 9
//!
//! ids are UUID v4s generated on POST; PUT preserves the existing id.
@@ -38,7 +38,7 @@ const OPENAPI_JSON_BASE: &str = r##"{
"info": {
"title": "AISIX Admin API",
"version": "dev",
"description": "The AISIX Admin API configures an open-source AISIX gateway at runtime. Use it when you operate the gateway directly and need to create or update models, caller API keys, provider credentials, guardrails, cache policies, and observability exporters.\n\nThe write endpoints (POST, PUT, DELETE) are deprecated in favor of declarative configuration: load resources from a `resources_file` (`resources.yaml`) or write them to etcd directly. Write endpoints remain functional, and every mutating response carries a `Deprecation` header (RFC 9745) plus a `Link` header with `rel=\"deprecation\"` pointing at the migration documentation. Read endpoints are not deprecated.\n\nGateways connected to AISIX Cloud do not expose this listener. Configure them through AISIX Cloud."
"description": "The AISIX Admin API is the read-only operational surface of an open-source AISIX gateway: list and inspect the loaded models, caller API keys, provider credentials, guardrails, MCP servers, A2A agents, cache policies, and observability exporters, check per-model upstream health, and drive the playground.\n\nResource write endpoints were removed in favor of declarative configuration: declare resources in a `resources_file` (`resources.yaml`) and reload with SIGHUP, or write them to etcd directly. See the resources file reference at https://docs.api7.ai/ai-gateway/reference/resources-file.\n\nGateways connected to AISIX Cloud do not expose this listener. Configure them through AISIX Cloud."
Comment on lines 4 to 5
//! validate against the JSON schema, reject duplicate names (409),
//! generate a uuid v4 on POST, bump revision on PUT.
Comment on lines 4 to 5
//! validate against the JSON schema, reject duplicate names (409),
//! generate a uuid v4 on POST, bump revision on PUT.
Comment on lines 5 to 6
//! PUT. Additionally rejects a name containing the reserved tool-namespace
//! separator `__`, since the name prefixes the server's tools.
Comment on lines 8 to 9
//! every configuration path rejects an incomplete credential set; the checks
//! below are defense in depth.
@moonmingmoonming self-assigned this Aug 10, 2026
@moonming

Copy link
Copy Markdown
CollaboratorAuthor

Review triage — every inline comment dispositioned; fixes landed in f9c10cc and e34694b.

Fixed

  • 7 handler module docs rewritten to the read-only contract; stale rotate/CRUD section comments removed (e34694b)
  • OpenAPI Caller API Keys tag no longer says "and key rotation"; path/Entry descriptions no longer claim ids are generated by the Admin API or that update/rotate bumps revisions (f9c10cc, e34694b)
  • main.rs: unused Some(path) binding → Some(_); canonical product name; removed a comment referencing write rejections (e34694b)
  • etcd integration: refused-writes test now covers PUT and DELETE with Allow assertions, and the rotate-absence checks GET the rotate URIs too — POST-only 404 couldn't distinguish a deleted route from the old handler's unknown-id 404 (f9c10cc, e34694b)
  • sdk-compat e2e: readiness gate switched from the SDK chat path to an independent authenticated GET /v1/models probe per the harness gate rules (e34694b)
  • config.example.yaml: dead docs/api-admin.md link and Admin-API management claims replaced with the declarative sources (f9c10cc)

Not adopted, with reasons

  • Assert Allow with toBe("GET"): the real binary answers Allow: GET,HEAD (axum's MethodRouter advertises HEAD alongside GET), so an exact "GET" match fails against actual behavior. toContain("GET") plus the 405 status is the contract; the 405 itself already proves no write method is routed.
  • Duplicate key_hash uniqueness on the direct-etcd path (assert_unique_key removal): real gap, but pre-existing — the admin-side check never guarded direct etcd writes, which existed before this PR and are used by the control plane (which enforces uniqueness org-side) and the file source (which has its own per-kind identity check). Filed internally for loader-side conflict detection with /status/config visibility; tracked as a follow-up rather than blocking the removal.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts (1)

276-301: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Use an independent readiness check for key setup.

seedKey gates propagation with a POST /v1/chat/completions request. The rotation and deletion flows also use the chat authorization path for their assertions. A chat failure can stop the test before it checks the key transition.

Seed the caller keys, then verify readiness with GET /v1/models and require 200. Keep chat requests for the actual rotation and revocation assertions.

As per coding guidelines, E2E readiness gates must use an independent condition, and caller API keys must be checked with GET /v1/models returning 200.

Also applies to: 311-312

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts` around lines 276 - 301,
Update the key setup readiness flow around seedKey and the related
rotation/deletion cases to use an independent GET /v1/models request requiring
HTTP 200, rather than POST /v1/chat/completions. Keep chat requests in the
secret-swap and revocation assertions only, so readiness failures cannot mask
key-transition checks.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/aisix-core/src/bin/dump-schema.rs`:
- Around line 44-47: Update the comment near the STRICT shape documentation to
qualify unknown-field rejection as applying only where the resource schema is
closed. Preserve the existing distinction between declarative write contracts
and lenient etcd reads, while acknowledging resource-specific exceptions such as
open fields and custom guardrail validation documented in the schema guidance.
In `@tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts`:
- Around line 304-307: Update the combined setup guard in the affected E2E test
to also check that otlp is available, skipping and returning when any shared
setup value—including etcdReachable, app, seed, or otlp—is missing.
---
Outside diff comments:
In `@tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts`:
- Around line 276-301: Update the key setup readiness flow around seedKey and
the related rotation/deletion cases to use an independent GET /v1/models request
requiring HTTP 200, rather than POST /v1/chat/completions. Keep chat requests in
the secret-swap and revocation assertions only, so readiness failures cannot
mask key-transition checks.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: aa61ea23-b8c7-4503-b1c7-e50f6378ee7d

📥 Commits

Reviewing files that changed from the base of the PR and between 152c57d and f9c10cc.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (12)
  • README.md
  • config.example.yaml
  • crates/aisix-admin/Cargo.toml
  • crates/aisix-admin/src/apikeys_handlers.rs
  • crates/aisix-admin/src/error.rs
  • crates/aisix-admin/src/lib.rs
  • crates/aisix-admin/src/openapi.rs
  • crates/aisix-admin/tests/etcd_integration.rs
  • crates/aisix-core/src/bin/dump-schema.rs
  • crates/aisix-etcd/src/provider.rs
  • schemas/README.md
  • tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts
💤 Files with no reviewable changes (2)
  • crates/aisix-admin/Cargo.toml
  • crates/aisix-admin/src/error.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • config.example.yaml
  • README.md
  • crates/aisix-admin/src/apikeys_handlers.rs
  • crates/aisix-admin/src/lib.rs

Comment threadcrates/aisix-core/src/bin/dump-schema.rs
Comment on lines +304 to 307
if (!etcdReachable || !app || !seed) {
ctx.skip();
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Include otlp in the setup guard.

The guard omits otlp. Add it to the combined setup check so the test does not run with incomplete shared setup.

Based on learnings, E2E cases must preserve if (!etcdReachable || !app || !seed || !otlp) { ctx.skip(); return; }.

Proposed fix
- if (!etcdReachable || !app || !seed) {+ if (!etcdReachable || !app || !seed || !otlp) {
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if(!etcdReachable||!app||!seed){
ctx.skip();
return;
}
if(!etcdReachable||!app||!seed||!otlp){
ctx.skip();
return;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts` around lines 304 - 307,
Update the combined setup guard in the affected E2E test to also check that otlp
is available, skipping and returning when any shared setup value—including
etcdReachable, app, seed, or otlp—is missing.

Source: Learnings

The admin listener keeps its read surface (lists/gets for all 8
resource kinds incl. the former apikeys spelling, models/status,
health, OpenAPI + Scalar, playground, livez/readyz); resources are
managed exclusively through the declarative paths — resources_file
(SIGHUP reload) or direct etcd writes.
BREAKING CHANGE: POST/PUT/DELETE on /admin/v1/* answer 405 with
Allow: GET (409 file-managed rejection included); the api-key rotate
route is gone (404) — rotate declaratively by writing the same
resource id with a new key_hash. The published OpenAPI documents no
write operations; write-only component schemas (ApiKeyRequest,
ApiKeyEntry, ApiKeyRotateResponse, DeleteResponse) are removed.
- router: every /admin/v1/* resource route serves get() only; rotate
routes, file-managed write guard, and RFC 9745 deprecation-header
middleware deleted; 43 write/rotate/uniqueness handler fns removed
- store: ConfigStore is read-only (16 put_*/delete_* methods and
StoreError::ReadOnly removed); EtcdConfigStore keeps reads only;
FileManagedStore::new(snapshot) drops the path param; InMemoryStore
keeps #[cfg(test)] inherent writes for unit-test seeding
- openapi: 24 write ops + rotate path removed from the base document;
no-op deprecation-marker pass deleted; unreachable component
schemas pruned; new gate pins GET-only /admin/v1/* and zero
deprecated marks
- tests: write-path suites deleted; read tests seed via InMemoryStore;
new 405/Allow + rotate-404 contract tests; etcd integration tests
seed via direct etcd writes (the declarative front door) and pin
that refused writes never touch etcd
- e2e: AdminClient write helpers removed (SeedClient is the write
front door); file-resource-source pins the new read-only contract;
apikey-lifecycle rotate coverage became a declarative secret-swap
test; obsolete write-path cases deleted
- docs: README, config.example.yaml, crate module docs updated
Second-auditor findings on the removal PR, all test/doc-level (no
runtime changes):
- rotate-404 tests now GET the rotate URIs too — POST-only 404 could
not distinguish a deleted route from the old handler's unknown-id
404; GET answers 405 on a surviving POST-only route
- e2e: deleting a key's etcd entry revokes an in-use bearer
(fail-closed, unknown-token 401, other keys unaffected) — the
deletion branch had lost its only end-to-end proof
- etcd integration: a2a_agents round-trip + loader coverage (7 -> 8
kinds)
- apikeys read test pins the full PublicApiKey projection
(allowed_tools/disabled/expires_at), not just the id
- OpenAPI descriptions stop claiming ids are generated by the Admin
API and revisions increment on update/rotate
- stale write-path narrative removed: config.example.yaml (dead
docs/api-admin.md link), schemas/README, dump-schema comment,
apikeys_handlers module doc, aisix-etcd provider doc, README RBAC
row + e2e counts
- dead code: AdminError::{BadRequest,Conflict,Schema} variants and
the aisix-mcp/uuid dependencies left over from the write path
Per-comment review triage:
- 7 handler module docs rewritten from CRUD-era text to the surviving
read-only contract; stale rotate/CRUD section comments in the
aisix-admin test module removed
- OpenAPI 'Caller API Keys' tag no longer advertises key rotation
- main.rs: unused match binding -> Some(_); canonical product name;
dropped a comment referencing the removed write-rejection path
- etcd integration: refused-writes test now covers PUT and DELETE
(405 + Allow), not just POST
- sdk-compat e2e: readiness gate switched from the SDK chat path (the
behavior under test) to an independent authenticated GET /v1/models
probe, per the harness gate rules
Cold-audit closeout:
- removed_resource_writes_answer_405_with_allow_get now generates the
FULL matrix (9 route spellings x POST/PUT/DELETE) instead of a
sampled subset — a partial revert (e.g. PUT re-added on one {id}
route) previously passed the whole suite
- last stale write-path narrative: store.rs module doc (read-only
trait), aisix-core schema.rs/models docs (declarative writers, not
'Admin API ... 400'), e2e smoke/seed/app/forward-compat headers no
longer cite deleted characterization or held-back write cases,
openapi.rs base-doc comment names a surviving schema
Round-two auditor findings:
- OpenAPI: the two remaining Entry revision descriptions (McpServer,
A2aAgent) stop describing create/update lifecycle; a regression
assertion now rejects write-lifecycle prose on any documented
revision field
- schemas/README + schema.rs + dump-schema: scope the strict-contract
claim to the in-repo writers (aisix validate, file source) — the
control plane validates its own API schema, a raw direct etcd put
gets no synchronous validation (lenient read only), and
unknown-field rejection applies only where a resource closes fields;
the previous rewrite overclaimed all three
- deletion-revocation e2e: propagation barrier is now a fresh key
seeded after the delete (later etcd revision), so the revocation
assertion is a real assertion instead of a gate poll; a regression
fails the assert, not a 30s timeout
- apikeys projection test seeds rate_limit + allowed_agents and pins
the list entry's projection too; module doc describes PublicApiKey
as an explicit allowlist (not 'minus nothing')
- lifecycle prose: secret-swap invalidates 'as soon as the write
propagates' (not 'immediately'); README e2e counts scoped to
scenario files (183/496)
@moonming
moonmingforce-pushed the feat/remove-admin-write-path branch from 74b0db5 to f43eab2CompareAugust 11, 2026 01:59
@moonming
moonming merged commit b61d270 into mainAug 11, 2026
30 of 32 checks passed
@moonming
moonming deleted the feat/remove-admin-write-path branch August 11, 2026 02:20
Sign up for freeto 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.

2 participants

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
feat(admin)!: remove the Admin API resource write path by moonming · Pull Request #915 · api7/aisix · GitHub
Skip to content

feat(admin)!: remove the Admin API resource write path - #915

Merged
moonming merged 5 commits into
mainfrom
feat/remove-admin-write-path
Aug 11, 2026
Merged

feat(admin)!: remove the Admin API resource write path#915
moonming merged 5 commits into
mainfrom
feat/remove-admin-write-path

Conversation

@moonming

@moonmingmoonming commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Removes the Admin API resource write path. The admin listener (:3001) keeps its read surface — lists/gets for all 8 resource kinds (including the former apikeys spelling), /admin/v1/models/status, /admin/v1/health, OpenAPI + Scalar UI, playground, /livez, /readyz — but resources are now managed exclusively through the declarative paths: a resources_file (resources.yaml, reloaded on SIGHUP) or direct etcd writes.

This is the final step of the deprecation announced in v0.4.0 (RFC 9745 Deprecation headers) and executes the removal scheduled for v0.5.0+ after #848 lifted the write-path-exclusive validations into the canonical schemas so the declarative paths enforce them.

⚠️ Breaking changes

BeforeAfter
POST /admin/v1/<kind>, PUT/DELETE /admin/v1/<kind>/{id} (deprecated, functional)405 with Allow: GET
POST /admin/v1/api_keys/{id}/rotate (and apikeys spelling)404 — the route is gone
File mode: writes rejected with 409 naming the resources file405, same as every other mode
Rotate returned a fresh plaintext keyNo DP-side plaintext rotation. Rotate declaratively: write the same resource id with a new key_hash — the old plaintext stops authenticating as soon as the write propagates (pinned in apikey-lifecycle-e2e)

What to update:

  • Scripts that created/updated/deleted resources via :3001 → write resources.yaml (validate offline with aisix validate --resources <file>, reload with SIGHUP) or write entity-value JSON to etcd at {prefix}/{kind}/{id}.
  • Scripts that rotated caller keys via /rotate → hash the new secret client-side and update the resource's key_hash.
  • The published OpenAPI (/admin/openapi.json) no longer documents write operations; the write-only component schemas (ApiKeyRequest, ApiKeyEntry, ApiKeyRotateResponse, DeleteResponse) are gone from components.schemas.

What changed

Router / handlers — every /admin/v1/* resource route serves get(...) only; both rotate routes deleted; the file-managed write guard and the RFC 9745 deprecation-header middleware deleted (nothing left to mark). 43 write/rotate/uniqueness handler fns removed across the 8 handler modules.

Store layerConfigStore is now a read-only trait (16 put_*/delete_* methods removed, StoreError::ReadOnly gone). EtcdConfigStore keeps only reads (module doc rewritten: resources reach etcd through the declarative paths). FileManagedStore::new(snapshot) drops the path parameter — read-only by construction. InMemoryStore keeps #[cfg(test)] inherent write methods used by unit-test seeding.

OpenAPI — 24 write operations and the rotate path removed from the base document; the no-op write-deprecation marker pass deleted; unreachable component schemas pruned via a reachability walk from paths. New gate test pins that the published reference documents zero non-GET operations under /admin/v1/ and zero deprecated marks anywhere.

Tests — write-path unit tests (CRUD flows, rotate atomicity, write-auth, write-validation) deleted; read tests re-seeded through InMemoryStore; new contract tests pin 405 + Allow: GET on every collection/:id route (auth'd and unauthenticated) and 404 on both rotate spellings. The etcd integration test now seeds via direct etcd_client puts — the path operators actually use.

e2eAdminClient write helpers removed (reads stay; SeedClient is the write front door). file-resource-source-e2e pins the new file-mode contract (reads serve the file, writes 405, rotate 404, unauthenticated write 405 with no file-path leak). apikey-lifecycle-e2e's rotate coverage became a declarative secret-swap test (old plaintext dies immediately, id unchanged). Deleted: seed-vs-admin-characterization-e2e (its own comment scheduled retirement once the seed migration completed), apikey-budget-e2e (tested write-path 400s), the sdk-compat deprecation-header test, and forward-compat's admin strict-write test.

Docs — README, config.example.yaml, crate module docs updated to the read-only story.

Non-goals / follow-ups (filed internally)

  • 3 OpenAPI-parsing validations (mcp validate_spec, HeaderName typing, duplicate tool names) still lack declarative-path enforcement — blocked on a dependency-direction extraction, tracked internally.
  • etcd watch keeps last-good on an invalid PUT while resync drops the row — policy decision tracked internally.

Verification

  • cargo fmt --check, cargo clippy --workspace --all-targets -- -D warnings, cargo test --workspace — all green.
  • Real binary, file mode: boots with resources.yaml, GET list serves the file, POST /admin/v1/models → 405 + Allow: GET, rotate → 404, /admin/openapi.json documents no admin writes.
  • Local e2e stack run before push.

Summary by CodeRabbit

  • Changes
    • The Admin API is now read-only for managed resources.
    • Resource listing and detail views remain available through GET requests.
    • Creation, updates, deletion, and API-key rotation through the Admin API are no longer supported.
    • Unsupported write requests return 405 responses, while rotation routes return 404.
    • Manage resources through resources_file reloads or direct etcd writes.
    • Updated configuration, validation guidance, and end-to-end coverage to reflect the read-only administration model.

CopilotAI balanced review requested due to automatic review settings August 10, 2026 05:09
@coderabbitai

coderabbitaiBot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9acb9ae1-7d4d-42ce-83f0-61de46c03b1a

📥 Commits

Reviewing files that changed from the base of the PR and between f9c10cc and f43eab2.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (25)
  • README.md
  • config.example.yaml
  • crates/aisix-admin/src/a2a_agents_handlers.rs
  • crates/aisix-admin/src/apikeys_handlers.rs
  • crates/aisix-admin/src/cache_policies_handlers.rs
  • crates/aisix-admin/src/guardrails_handlers.rs
  • crates/aisix-admin/src/lib.rs
  • crates/aisix-admin/src/mcp_servers_handlers.rs
  • crates/aisix-admin/src/models_handlers.rs
  • crates/aisix-admin/src/observability_exporters_handlers.rs
  • crates/aisix-admin/src/openapi.rs
  • crates/aisix-admin/src/provider_keys_handlers.rs
  • crates/aisix-admin/src/store.rs
  • crates/aisix-admin/tests/etcd_integration.rs
  • crates/aisix-core/src/bin/dump-schema.rs
  • crates/aisix-core/src/models/mod.rs
  • crates/aisix-core/src/models/schema.rs
  • crates/aisix-server/src/main.rs
  • schemas/README.md
  • tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts
  • tests/e2e/src/cases/config-forward-compat-e2e.test.ts
  • tests/e2e/src/cases/openai-sdk-compat.test.ts
  • tests/e2e/src/cases/smoke.test.ts
  • tests/e2e/src/harness/app.ts
  • tests/e2e/src/harness/seed.ts
💤 Files with no reviewable changes (1)
  • tests/e2e/src/cases/smoke.test.ts
🚧 Files skipped from review as they are similar to previous changes (12)
  • crates/aisix-server/src/main.rs
  • config.example.yaml
  • crates/aisix-admin/src/provider_keys_handlers.rs
  • crates/aisix-admin/src/models_handlers.rs
  • schemas/README.md
  • crates/aisix-core/src/bin/dump-schema.rs
  • tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts
  • tests/e2e/src/cases/config-forward-compat-e2e.test.ts
  • crates/aisix-admin/src/apikeys_handlers.rs
  • README.md
  • crates/aisix-admin/tests/etcd_integration.rs
  • crates/aisix-admin/src/lib.rs

📝 Walkthrough

Walkthrough

The admin listener now exposes read-only resource routes. Resource writes use resources_file reloads, direct etcd writes, or declarative seed helpers. Stores, server wiring, tests, and documentation were updated.

Changes

Read-only admin resource surface

Layer / File(s)Summary
Read-only store contracts
crates/aisix-admin/src/store.rs, crates/aisix-admin/src/etcd_store.rs, crates/aisix-admin/src/file_store.rs, crates/aisix-admin/src/error.rs, crates/aisix-admin/src/state.rs
Store traits and implementations now support resource reads only.
GET-only admin routes
crates/aisix-admin/src/lib.rs, crates/aisix-admin/src/*_handlers.rs
Create, update, delete, and API-key rotation routes were removed.
Read-only server wiring
crates/aisix-server/src/main.rs
The server constructs read-only file-backed admin state and removes the file-managed write guard.
Direct etcd setup and integration coverage
crates/aisix-admin/tests/etcd_integration.rs, crates/aisix-admin/src/etcd_store.rs
Tests seed canonical documents directly in etcd and verify read-only behavior.
Declarative E2E flows and documentation
tests/e2e/src/cases/*, tests/e2e/src/harness/*, README.md, config.example.yaml, schemas/README.md
E2E setup and documentation now use declarative resource-management paths.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
participant EtcdClient
participant AdminAPI
participant ConfigLoader
EtcdClient->>EtcdClient: write canonical resource document
AdminAPI->>EtcdClient: list/get resource
EtcdClient-->>AdminAPI: resource document
ConfigLoader->>EtcdClient: load canonical resource documents
EtcdClient-->>ConfigLoader: configuration data
Loading

Possibly related PRs

  • api7/aisix#792: Related through direct etcd seeding and readiness migration.
  • api7/aisix#800: Related through removal of Admin API writes and E2E harness changes.
  • api7/aisix#848: Related through A2A and MCP admin-handler changes.

Suggested reviewers:jarvis9443


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error, 1 warning)

Check nameStatusExplanationResolution
Security Check❌ ErrorCategory 1: admin GETs serialize ProviderKey.api_key, McpServer.secret, and A2aAgent.secret; PublicApiKey also returns key_hash without redaction.Return redacted DTOs for every secret-bearing resource, omit key_hash and credential fields, and replace backend-detail error responses with generic messages.
E2e Test Quality Review⚠️ WarningE2E quality issue: config-forward-compat-e2e.test.ts test 3 deletes yellowKeyId created only by test 1, with no ordering declaration, creating a hidden test dependency.Make each test self-contained by seeding and cleaning its own rows, or explicitly declare and document sequential execution; also keep route-matrix coverage at the process level if all aliases are contractual.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly and concisely identifies the main breaking change: removing the Admin API resource write path.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/remove-admin-write-path

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/aisix-admin/tests/etcd_integration.rs (1)

383-489: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Add the missing a2a_agents canonical document.

The PR retains eight resource kinds, but writes contains seven entries. It omits a2a_agents. Add a valid A2A agent document, assert stats.accepted == 8, and assert snap.a2a_agents.len() == 1.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/aisix-admin/tests/etcd_integration.rs` around lines 383 - 489, Extend
the writes array with a valid a2a_agents canonical document using the existing
seed flow, then update the accepted-entry assertion from 7 to 8. Add a
corresponding snap.a2a_agents length assertion expecting exactly one loaded
agent, leaving the other resource assertions unchanged.
🧹 Nitpick comments (1)
crates/aisix-admin/src/lib.rs (1)

1131-1198: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Read coverage now spans only two of eight resource kinds. Deleting the write handlers also deleted each handler module's test module. The replacement read tests in lib.rs seed only models and api_keys. The other six kinds — provider_keys, guardrails, cache_policies, observability_exporters, mcp_servers, a2a_agents — have 405 write-refusal coverage but no test proving that GET serves a seeded entry. These handler bodies are hand-written per module, not macro-generated, so a wrong store call or a wrong response shape in one of the six would pass CI.

  • crates/aisix-admin/src/lib.rs#L1131-L1198: extend build_seedable_state with seed helpers for the remaining six kinds, then add list and get-by-id assertions for each, mirroring list_models_returns_seeded_entries and get_model_serves_seeded_entry.
  • crates/aisix-admin/src/a2a_agents_handlers.rs#L12: add a test that list_a2a_agents and get_a2a_agent return a seeded A2aAgent, or confirm the new lib.rs tests cover this module.
  • crates/aisix-admin/src/mcp_servers_handlers.rs#L9: add the same list/get coverage for McpServer, or confirm the new lib.rs tests cover this module.

Note that InMemoryStore currently exposes only put_model and put_apikey as #[cfg(test)] helpers, so seeding the other six kinds requires adding matching helpers in crates/aisix-admin/src/store.rs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/aisix-admin/src/lib.rs` around lines 1131 - 1198, Extend
crates/aisix-admin/src/store.rs with cfg(test) put helpers for provider_keys,
guardrails, cache_policies, observability_exporters, mcp_servers, and
a2a_agents, then update build_seedable_state and the tests in
crates/aisix-admin/src/lib.rs#L1131-L1198 to seed each kind and assert both list
and get-by-id responses, mirroring the model tests. Ensure
crates/aisix-admin/src/a2a_agents_handlers.rs#L12 and
crates/aisix-admin/src/mcp_servers_handlers.rs#L9 are covered by these lib.rs
tests; no direct handler changes are required unless coverage cannot exercise
their list/get functions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/aisix-admin/src/lib.rs`:
- Around line 1200-1230: Remove the stale rotation-coverage comment immediately
before openapi_apikey_schema_excludes_max_budget_usd, and delete the empty
CachePolicy CRUD and Health endpoint section comments. Preserve the
guardrail_payload function, the ObservabilityExporter CRUD comment, and all
surrounding tests and formatting.
In `@crates/aisix-admin/tests/etcd_integration.rs`:
- Around line 317-354: Extend the resource-route assertions in the integration
test to issue both PUT and DELETE requests to /admin/v1/models, asserting each
returns METHOD_NOT_ALLOWED and includes GET in the Allow header. Keep the
existing POST check and final etcd emptiness assertion so all refused write
methods verify that no data is written.
In `@crates/aisix-server/src/main.rs`:
- Around line 898-902: Update the file-source match arm in the admin_store
initialization to bind the second tuple element as Some(_) instead of
Some(path), preserving the existing condition and FileManagedStore construction.
In `@tests/e2e/src/cases/file-resource-source-e2e.test.ts`:
- Around line 179-213: Update the rejected-write assertions in the e2e test to
require the exact Allow header value "GET" instead of merely containing GET,
covering both the authenticated POST/DELETE responses and the unauthenticated
POST response. Preserve the existing status and response-body assertions.
In `@tests/e2e/src/cases/openai-sdk-compat.test.ts`:
- Around line 54-84: After seeding the API key in the test setup, add an
independent readiness poll using the seeded caller credentials against
authenticated GET /v1/models, continuing until it returns 200. Remove the
client.chat.completions.create-based propagation gate and keep that call
exclusively for the SDK chat behavior under test.
---
Outside diff comments:
In `@crates/aisix-admin/tests/etcd_integration.rs`:
- Around line 383-489: Extend the writes array with a valid a2a_agents canonical
document using the existing seed flow, then update the accepted-entry assertion
from 7 to 8. Add a corresponding snap.a2a_agents length assertion expecting
exactly one loaded agent, leaving the other resource assertions unchanged.
---
Nitpick comments:
In `@crates/aisix-admin/src/lib.rs`:
- Around line 1131-1198: Extend crates/aisix-admin/src/store.rs with cfg(test)
put helpers for provider_keys, guardrails, cache_policies,
observability_exporters, mcp_servers, and a2a_agents, then update
build_seedable_state and the tests in crates/aisix-admin/src/lib.rs#L1131-L1198
to seed each kind and assert both list and get-by-id responses, mirroring the
model tests. Ensure crates/aisix-admin/src/a2a_agents_handlers.rs#L12 and
crates/aisix-admin/src/mcp_servers_handlers.rs#L9 are covered by these lib.rs
tests; no direct handler changes are required unless coverage cannot exercise
their list/get functions.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f3cea19a-3b55-4ad2-b83e-bc29037270c5

📥 Commits

Reviewing files that changed from the base of the PR and between b77f1a6 and 152c57d.

📒 Files selected for processing (26)
  • README.md
  • config.example.yaml
  • crates/aisix-admin/src/a2a_agents_handlers.rs
  • crates/aisix-admin/src/apikeys_handlers.rs
  • crates/aisix-admin/src/cache_policies_handlers.rs
  • crates/aisix-admin/src/error.rs
  • crates/aisix-admin/src/etcd_store.rs
  • crates/aisix-admin/src/file_store.rs
  • crates/aisix-admin/src/guardrails_handlers.rs
  • crates/aisix-admin/src/lib.rs
  • crates/aisix-admin/src/mcp_servers_handlers.rs
  • crates/aisix-admin/src/models_handlers.rs
  • crates/aisix-admin/src/observability_exporters_handlers.rs
  • crates/aisix-admin/src/openapi.rs
  • crates/aisix-admin/src/provider_keys_handlers.rs
  • crates/aisix-admin/src/state.rs
  • crates/aisix-admin/src/store.rs
  • crates/aisix-admin/tests/etcd_integration.rs
  • crates/aisix-server/src/main.rs
  • tests/e2e/src/cases/apikey-budget-e2e.test.ts
  • tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts
  • tests/e2e/src/cases/config-forward-compat-e2e.test.ts
  • tests/e2e/src/cases/file-resource-source-e2e.test.ts
  • tests/e2e/src/cases/openai-sdk-compat.test.ts
  • tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts
  • tests/e2e/src/harness/admin.ts
💤 Files with no reviewable changes (8)
  • tests/e2e/src/cases/apikey-budget-e2e.test.ts
  • tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts
  • crates/aisix-admin/src/state.rs
  • crates/aisix-admin/src/observability_exporters_handlers.rs
  • crates/aisix-admin/src/cache_policies_handlers.rs
  • crates/aisix-admin/src/models_handlers.rs
  • crates/aisix-admin/src/guardrails_handlers.rs
  • crates/aisix-admin/src/provider_keys_handlers.rs

Comment threadcrates/aisix-admin/src/lib.rs Outdated
Comment threadcrates/aisix-admin/tests/etcd_integration.rs Outdated
Comment threadcrates/aisix-server/src/main.rs
Comment on lines +179 to +213
expect(postRes.status).toBe(405);
expect(postRes.headers.get("allow")).toContain("GET");
await postRes.text();

// The refused write did not change the resource set.
const relist = await fetch(`${app.adminUrl}/admin/v1/models`, { headers: auth });
expect(((await relist.json()) as unknown[]).length).toBe(2);

// DELETE and rotate are covered by the same guard.
const delRes = await fetch(`${app.adminUrl}/admin/v1/models/any-id`, {
method: "DELETE",
headers: auth,
});
expect(delRes.status).toBe(409);
expect(delRes.status).toBe(405);
expect(delRes.headers.get("allow")).toContain("GET");
await delRes.text();

// The rotate route was removed outright (it was POST-only), so the
// path no longer exists at all.
const rotateRes = await fetch(`${app.adminUrl}/admin/v1/api_keys/any-id/rotate`, {
method: "POST",
headers: auth,
});
expect(rotateRes.status).toBe(409);
expect(rotateRes.status).toBe(404);
await rotateRes.text();

// Auth ordering: an UNAUTHENTICATED write still gets 401, and the
// 401 body must not leak the resources-file path (that detail is
// only for authenticated admins).
// Method routing answers before auth: an unauthenticated write gets
// the same 405 — there is no write endpoint left to protect, and
// the 405 body carries no resources-file detail to leak.
const unauthed = await fetch(`${app.adminUrl}/admin/v1/models`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ display_name: "nope" }),
});
expect(unauthed.status).toBe(401);
const unauthedBody = (await unauthed.json()) as { error_msg: string };
expect(unauthedBody.error_msg).not.toContain(app.resourcesPath!);
expect(unauthed.status).toBe(405);
expect((await unauthed.text())).not.toContain(app.resourcesPath!);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert the exact Allow header value.

Use toBe("GET") for each rejected write. Add the same assertion for the unauthenticated request. toContain("GET") also accepts an invalid value such as GET, POST.

Based on PR objectives, rejected resource writes must return 405 with Allow: GET.

Proposed test update
- expect(postRes.headers.get("allow")).toContain("GET");+ expect(postRes.headers.get("allow")).toBe("GET");
...
- expect(delRes.headers.get("allow")).toContain("GET");+ expect(delRes.headers.get("allow")).toBe("GET");
...
expect(unauthed.status).toBe(405);
+ expect(unauthed.headers.get("allow")).toBe("GET");
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
expect(postRes.status).toBe(405);
expect(postRes.headers.get("allow")).toContain("GET");
awaitpostRes.text();
// The refused write did not change the resource set.
constrelist=awaitfetch(`${app.adminUrl}/admin/v1/models`,{headers: auth});
expect(((awaitrelist.json())asunknown[]).length).toBe(2);
// DELETE and rotate are covered by the same guard.
constdelRes=awaitfetch(`${app.adminUrl}/admin/v1/models/any-id`,{
method: "DELETE",
headers: auth,
});
expect(delRes.status).toBe(409);
expect(delRes.status).toBe(405);
expect(delRes.headers.get("allow")).toContain("GET");
awaitdelRes.text();
// The rotate route was removed outright (it was POST-only), so the
// path no longer exists at all.
constrotateRes=awaitfetch(`${app.adminUrl}/admin/v1/api_keys/any-id/rotate`,{
method: "POST",
headers: auth,
});
expect(rotateRes.status).toBe(409);
expect(rotateRes.status).toBe(404);
awaitrotateRes.text();
// Auth ordering: an UNAUTHENTICATED write still gets 401, and the
// 401 body must not leak the resources-file path (that detail is
// only for authenticated admins).
// Method routing answers before auth: an unauthenticated write gets
// the same 405 — there is no write endpoint left to protect, and
// the 405 body carries no resources-file detail to leak.
constunauthed=awaitfetch(`${app.adminUrl}/admin/v1/models`,{
method: "POST",
headers: {"content-type": "application/json"},
body: JSON.stringify({display_name: "nope"}),
});
expect(unauthed.status).toBe(401);
constunauthedBody=(awaitunauthed.json())as{error_msg: string};
expect(unauthedBody.error_msg).not.toContain(app.resourcesPath!);
expect(unauthed.status).toBe(405);
expect((awaitunauthed.text())).not.toContain(app.resourcesPath!);
expect(postRes.status).toBe(405);
expect(postRes.headers.get("allow")).toBe("GET");
awaitpostRes.text();
// The refused write did not change the resource set.
constrelist=awaitfetch(`${app.adminUrl}/admin/v1/models`,{headers: auth});
expect(((awaitrelist.json())asunknown[]).length).toBe(2);
constdelRes=awaitfetch(`${app.adminUrl}/admin/v1/models/any-id`,{
method: "DELETE",
headers: auth,
});
expect(delRes.status).toBe(405);
expect(delRes.headers.get("allow")).toBe("GET");
awaitdelRes.text();
// The rotate route was removed outright (it was POST-only), so the
// path no longer exists at all.
constrotateRes=awaitfetch(`${app.adminUrl}/admin/v1/api_keys/any-id/rotate`,{
method: "POST",
headers: auth,
});
expect(rotateRes.status).toBe(404);
awaitrotateRes.text();
// Method routing answers before auth: an unauthenticated write gets
// the same 405 — there is no write endpoint left to protect, and
// the 405 body carries no resources-file detail to leak.
constunauthed=awaitfetch(`${app.adminUrl}/admin/v1/models`,{
method: "POST",
headers: {"content-type": "application/json"},
body: JSON.stringify({display_name: "nope"}),
});
expect(unauthed.status).toBe(405);
expect(unauthed.headers.get("allow")).toBe("GET");
expect((awaitunauthed.text())).not.toContain(app.resourcesPath!);
🧰 Tools
🪛 ast-grep (0.45.0)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { execFile } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/e2e/src/cases/file-resource-source-e2e.test.ts` around lines 179 - 213,
Update the rejected-write assertions in the e2e test to require the exact Allow
header value "GET" instead of merely containing GET, covering both the
authenticated POST/DELETE responses and the unauthenticated POST response.
Preserve the existing status and response-body assertions.

Comment threadtests/e2e/src/cases/openai-sdk-compat.test.ts

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Removes Admin API resource writes, leaving read-only resource endpoints and moving management to declarative file or etcd paths.

Changes:

  • Removes write handlers, routes, store operations, rotation, and deprecation middleware.
  • Updates OpenAPI, documentation, and tests for the read-only contract.
  • Migrates test setup to direct etcd seeding.

Reviewed changes

Copilot reviewed 25 out of 26 changed files in this pull request and generated 12 comments.

Show a summary per file
FileDescription
README.mdDocuments the read-only Admin API.
config.example.yamlClarifies declarative resource management.
crates/aisix-server/src/main.rsWires read-only admin stores.
crates/aisix-admin/src/lib.rsRemoves write routes and middleware.
crates/aisix-admin/src/openapi.rsRemoves write operations and schemas.
crates/aisix-admin/src/store.rsMakes ConfigStore read-only.
crates/aisix-admin/src/state.rsRemoves file-write guard state.
crates/aisix-admin/src/error.rsRemoves write-related errors.
crates/aisix-admin/src/file_store.rsRetains snapshot reads only.
crates/aisix-admin/src/etcd_store.rsRetains etcd reads only.
crates/aisix-admin/src/models_handlers.rsRemoves model writes.
crates/aisix-admin/src/apikeys_handlers.rsRemoves API-key writes and rotation.
crates/aisix-admin/src/provider_keys_handlers.rsRemoves provider-key writes.
crates/aisix-admin/src/guardrails_handlers.rsRemoves guardrail writes.
crates/aisix-admin/src/cache_policies_handlers.rsRemoves cache-policy writes.
crates/aisix-admin/src/observability_exporters_handlers.rsRemoves exporter writes.
crates/aisix-admin/src/mcp_servers_handlers.rsRemoves MCP-server writes.
crates/aisix-admin/src/a2a_agents_handlers.rsRemoves A2A-agent writes.
crates/aisix-admin/tests/etcd_integration.rsTests direct-etcd writes and admin reads.
tests/e2e/src/harness/admin.tsRemoves Admin API write helpers.
tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.tsRemoves obsolete path comparison tests.
tests/e2e/src/cases/openai-sdk-compat.test.tsMigrates setup to etcd seeding.
tests/e2e/src/cases/file-resource-source-e2e.test.tsTests file-mode read-only behavior.
tests/e2e/src/cases/config-forward-compat-e2e.test.tsRemoves Admin write validation coverage.
tests/e2e/src/cases/apikey-lifecycle-e2e.test.tsReplaces rotation with declarative secret swapping.
tests/e2e/src/cases/apikey-budget-e2e.test.tsRemoves obsolete write-path validation test.
Suppressed comments (1)

crates/aisix-admin/src/mcp_servers_handlers.rs:9

  • This removal also eliminates the only production call to aisix_mcp::validate_spec. The file and etcd loaders only run the core JSON Schema, which does not reject specs with zero generatable operations or colliding sanitized tool names, so the newly exclusive declarative paths accept configurations the former write API rejected. Add the semantic validation to both loaders before removing this path.
use aisix_core::McpServer;

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment threadcrates/aisix-server/src/main.rs Outdated
use serde::{Deserialize, Serialize};
use serde_json::Value;
use uuid::Uuid;
use serde::Serialize;
Comment threadcrates/aisix-server/src/main.rs Outdated
Comment on lines 9 to 10
//! replaces the `key` field with a freshly-generated `sk-*` value and
//! bumps the revision, invalidating the old credential.
Comment on lines 8 to 9
//!
//! ids are UUID v4s generated on POST; PUT preserves the existing id.
@@ -38,7 +38,7 @@ const OPENAPI_JSON_BASE: &str = r##"{
"info": {
"title": "AISIX Admin API",
"version": "dev",
"description": "The AISIX Admin API configures an open-source AISIX gateway at runtime. Use it when you operate the gateway directly and need to create or update models, caller API keys, provider credentials, guardrails, cache policies, and observability exporters.\n\nThe write endpoints (POST, PUT, DELETE) are deprecated in favor of declarative configuration: load resources from a `resources_file` (`resources.yaml`) or write them to etcd directly. Write endpoints remain functional, and every mutating response carries a `Deprecation` header (RFC 9745) plus a `Link` header with `rel=\"deprecation\"` pointing at the migration documentation. Read endpoints are not deprecated.\n\nGateways connected to AISIX Cloud do not expose this listener. Configure them through AISIX Cloud."
"description": "The AISIX Admin API is the read-only operational surface of an open-source AISIX gateway: list and inspect the loaded models, caller API keys, provider credentials, guardrails, MCP servers, A2A agents, cache policies, and observability exporters, check per-model upstream health, and drive the playground.\n\nResource write endpoints were removed in favor of declarative configuration: declare resources in a `resources_file` (`resources.yaml`) and reload with SIGHUP, or write them to etcd directly. See the resources file reference at https://docs.api7.ai/ai-gateway/reference/resources-file.\n\nGateways connected to AISIX Cloud do not expose this listener. Configure them through AISIX Cloud."
Comment on lines 4 to 5
//! validate against the JSON schema, reject duplicate names (409),
//! generate a uuid v4 on POST, bump revision on PUT.
Comment on lines 4 to 5
//! validate against the JSON schema, reject duplicate names (409),
//! generate a uuid v4 on POST, bump revision on PUT.
Comment on lines 5 to 6
//! PUT. Additionally rejects a name containing the reserved tool-namespace
//! separator `__`, since the name prefixes the server's tools.
Comment on lines 8 to 9
//! every configuration path rejects an incomplete credential set; the checks
//! below are defense in depth.
@moonmingmoonming self-assigned this Aug 10, 2026
@moonming

Copy link
Copy Markdown
CollaboratorAuthor

Review triage — every inline comment dispositioned; fixes landed in f9c10cc and e34694b.

Fixed

  • 7 handler module docs rewritten to the read-only contract; stale rotate/CRUD section comments removed (e34694b)
  • OpenAPI Caller API Keys tag no longer says "and key rotation"; path/Entry descriptions no longer claim ids are generated by the Admin API or that update/rotate bumps revisions (f9c10cc, e34694b)
  • main.rs: unused Some(path) binding → Some(_); canonical product name; removed a comment referencing write rejections (e34694b)
  • etcd integration: refused-writes test now covers PUT and DELETE with Allow assertions, and the rotate-absence checks GET the rotate URIs too — POST-only 404 couldn't distinguish a deleted route from the old handler's unknown-id 404 (f9c10cc, e34694b)
  • sdk-compat e2e: readiness gate switched from the SDK chat path to an independent authenticated GET /v1/models probe per the harness gate rules (e34694b)
  • config.example.yaml: dead docs/api-admin.md link and Admin-API management claims replaced with the declarative sources (f9c10cc)

Not adopted, with reasons

  • Assert Allow with toBe("GET"): the real binary answers Allow: GET,HEAD (axum's MethodRouter advertises HEAD alongside GET), so an exact "GET" match fails against actual behavior. toContain("GET") plus the 405 status is the contract; the 405 itself already proves no write method is routed.
  • Duplicate key_hash uniqueness on the direct-etcd path (assert_unique_key removal): real gap, but pre-existing — the admin-side check never guarded direct etcd writes, which existed before this PR and are used by the control plane (which enforces uniqueness org-side) and the file source (which has its own per-kind identity check). Filed internally for loader-side conflict detection with /status/config visibility; tracked as a follow-up rather than blocking the removal.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts (1)

276-301: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Use an independent readiness check for key setup.

seedKey gates propagation with a POST /v1/chat/completions request. The rotation and deletion flows also use the chat authorization path for their assertions. A chat failure can stop the test before it checks the key transition.

Seed the caller keys, then verify readiness with GET /v1/models and require 200. Keep chat requests for the actual rotation and revocation assertions.

As per coding guidelines, E2E readiness gates must use an independent condition, and caller API keys must be checked with GET /v1/models returning 200.

Also applies to: 311-312

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts` around lines 276 - 301,
Update the key setup readiness flow around seedKey and the related
rotation/deletion cases to use an independent GET /v1/models request requiring
HTTP 200, rather than POST /v1/chat/completions. Keep chat requests in the
secret-swap and revocation assertions only, so readiness failures cannot mask
key-transition checks.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/aisix-core/src/bin/dump-schema.rs`:
- Around line 44-47: Update the comment near the STRICT shape documentation to
qualify unknown-field rejection as applying only where the resource schema is
closed. Preserve the existing distinction between declarative write contracts
and lenient etcd reads, while acknowledging resource-specific exceptions such as
open fields and custom guardrail validation documented in the schema guidance.
In `@tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts`:
- Around line 304-307: Update the combined setup guard in the affected E2E test
to also check that otlp is available, skipping and returning when any shared
setup value—including etcdReachable, app, seed, or otlp—is missing.
---
Outside diff comments:
In `@tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts`:
- Around line 276-301: Update the key setup readiness flow around seedKey and
the related rotation/deletion cases to use an independent GET /v1/models request
requiring HTTP 200, rather than POST /v1/chat/completions. Keep chat requests in
the secret-swap and revocation assertions only, so readiness failures cannot
mask key-transition checks.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: aa61ea23-b8c7-4503-b1c7-e50f6378ee7d

📥 Commits

Reviewing files that changed from the base of the PR and between 152c57d and f9c10cc.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (12)
  • README.md
  • config.example.yaml
  • crates/aisix-admin/Cargo.toml
  • crates/aisix-admin/src/apikeys_handlers.rs
  • crates/aisix-admin/src/error.rs
  • crates/aisix-admin/src/lib.rs
  • crates/aisix-admin/src/openapi.rs
  • crates/aisix-admin/tests/etcd_integration.rs
  • crates/aisix-core/src/bin/dump-schema.rs
  • crates/aisix-etcd/src/provider.rs
  • schemas/README.md
  • tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts
💤 Files with no reviewable changes (2)
  • crates/aisix-admin/Cargo.toml
  • crates/aisix-admin/src/error.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • config.example.yaml
  • README.md
  • crates/aisix-admin/src/apikeys_handlers.rs
  • crates/aisix-admin/src/lib.rs

Comment threadcrates/aisix-core/src/bin/dump-schema.rs
Comment on lines +304 to 307
if (!etcdReachable || !app || !seed) {
ctx.skip();
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Include otlp in the setup guard.

The guard omits otlp. Add it to the combined setup check so the test does not run with incomplete shared setup.

Based on learnings, E2E cases must preserve if (!etcdReachable || !app || !seed || !otlp) { ctx.skip(); return; }.

Proposed fix
- if (!etcdReachable || !app || !seed) {+ if (!etcdReachable || !app || !seed || !otlp) {
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if(!etcdReachable||!app||!seed){
ctx.skip();
return;
}
if(!etcdReachable||!app||!seed||!otlp){
ctx.skip();
return;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts` around lines 304 - 307,
Update the combined setup guard in the affected E2E test to also check that otlp
is available, skipping and returning when any shared setup value—including
etcdReachable, app, seed, or otlp—is missing.

Source: Learnings

The admin listener keeps its read surface (lists/gets for all 8
resource kinds incl. the former apikeys spelling, models/status,
health, OpenAPI + Scalar, playground, livez/readyz); resources are
managed exclusively through the declarative paths — resources_file
(SIGHUP reload) or direct etcd writes.
BREAKING CHANGE: POST/PUT/DELETE on /admin/v1/* answer 405 with
Allow: GET (409 file-managed rejection included); the api-key rotate
route is gone (404) — rotate declaratively by writing the same
resource id with a new key_hash. The published OpenAPI documents no
write operations; write-only component schemas (ApiKeyRequest,
ApiKeyEntry, ApiKeyRotateResponse, DeleteResponse) are removed.
- router: every /admin/v1/* resource route serves get() only; rotate
routes, file-managed write guard, and RFC 9745 deprecation-header
middleware deleted; 43 write/rotate/uniqueness handler fns removed
- store: ConfigStore is read-only (16 put_*/delete_* methods and
StoreError::ReadOnly removed); EtcdConfigStore keeps reads only;
FileManagedStore::new(snapshot) drops the path param; InMemoryStore
keeps #[cfg(test)] inherent writes for unit-test seeding
- openapi: 24 write ops + rotate path removed from the base document;
no-op deprecation-marker pass deleted; unreachable component
schemas pruned; new gate pins GET-only /admin/v1/* and zero
deprecated marks
- tests: write-path suites deleted; read tests seed via InMemoryStore;
new 405/Allow + rotate-404 contract tests; etcd integration tests
seed via direct etcd writes (the declarative front door) and pin
that refused writes never touch etcd
- e2e: AdminClient write helpers removed (SeedClient is the write
front door); file-resource-source pins the new read-only contract;
apikey-lifecycle rotate coverage became a declarative secret-swap
test; obsolete write-path cases deleted
- docs: README, config.example.yaml, crate module docs updated
Second-auditor findings on the removal PR, all test/doc-level (no
runtime changes):
- rotate-404 tests now GET the rotate URIs too — POST-only 404 could
not distinguish a deleted route from the old handler's unknown-id
404; GET answers 405 on a surviving POST-only route
- e2e: deleting a key's etcd entry revokes an in-use bearer
(fail-closed, unknown-token 401, other keys unaffected) — the
deletion branch had lost its only end-to-end proof
- etcd integration: a2a_agents round-trip + loader coverage (7 -> 8
kinds)
- apikeys read test pins the full PublicApiKey projection
(allowed_tools/disabled/expires_at), not just the id
- OpenAPI descriptions stop claiming ids are generated by the Admin
API and revisions increment on update/rotate
- stale write-path narrative removed: config.example.yaml (dead
docs/api-admin.md link), schemas/README, dump-schema comment,
apikeys_handlers module doc, aisix-etcd provider doc, README RBAC
row + e2e counts
- dead code: AdminError::{BadRequest,Conflict,Schema} variants and
the aisix-mcp/uuid dependencies left over from the write path
Per-comment review triage:
- 7 handler module docs rewritten from CRUD-era text to the surviving
read-only contract; stale rotate/CRUD section comments in the
aisix-admin test module removed
- OpenAPI 'Caller API Keys' tag no longer advertises key rotation
- main.rs: unused match binding -> Some(_); canonical product name;
dropped a comment referencing the removed write-rejection path
- etcd integration: refused-writes test now covers PUT and DELETE
(405 + Allow), not just POST
- sdk-compat e2e: readiness gate switched from the SDK chat path (the
behavior under test) to an independent authenticated GET /v1/models
probe, per the harness gate rules
Cold-audit closeout:
- removed_resource_writes_answer_405_with_allow_get now generates the
FULL matrix (9 route spellings x POST/PUT/DELETE) instead of a
sampled subset — a partial revert (e.g. PUT re-added on one {id}
route) previously passed the whole suite
- last stale write-path narrative: store.rs module doc (read-only
trait), aisix-core schema.rs/models docs (declarative writers, not
'Admin API ... 400'), e2e smoke/seed/app/forward-compat headers no
longer cite deleted characterization or held-back write cases,
openapi.rs base-doc comment names a surviving schema
Round-two auditor findings:
- OpenAPI: the two remaining Entry revision descriptions (McpServer,
A2aAgent) stop describing create/update lifecycle; a regression
assertion now rejects write-lifecycle prose on any documented
revision field
- schemas/README + schema.rs + dump-schema: scope the strict-contract
claim to the in-repo writers (aisix validate, file source) — the
control plane validates its own API schema, a raw direct etcd put
gets no synchronous validation (lenient read only), and
unknown-field rejection applies only where a resource closes fields;
the previous rewrite overclaimed all three
- deletion-revocation e2e: propagation barrier is now a fresh key
seeded after the delete (later etcd revision), so the revocation
assertion is a real assertion instead of a gate poll; a regression
fails the assert, not a 30s timeout
- apikeys projection test seeds rate_limit + allowed_agents and pins
the list entry's projection too; module doc describes PublicApiKey
as an explicit allowlist (not 'minus nothing')
- lifecycle prose: secret-swap invalidates 'as soon as the write
propagates' (not 'immediately'); README e2e counts scoped to
scenario files (183/496)
@moonming
moonmingforce-pushed the feat/remove-admin-write-path branch from 74b0db5 to f43eab2CompareAugust 11, 2026 01:59
@moonming
moonming merged commit b61d270 into mainAug 11, 2026
30 of 32 checks passed
@moonming
moonming deleted the feat/remove-admin-write-path branch August 11, 2026 02:20
Sign up for freeto 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.

2 participants

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(admin)!: remove the Admin API resource write path by moonming · Pull Request #915 · api7/aisix · GitHub
Skip to content

feat(admin)!: remove the Admin API resource write path - #915

Merged
moonming merged 5 commits into
mainfrom
feat/remove-admin-write-path
Aug 11, 2026
Merged

feat(admin)!: remove the Admin API resource write path#915
moonming merged 5 commits into
mainfrom
feat/remove-admin-write-path

Conversation

@moonming

@moonmingmoonming commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Removes the Admin API resource write path. The admin listener (:3001) keeps its read surface — lists/gets for all 8 resource kinds (including the former apikeys spelling), /admin/v1/models/status, /admin/v1/health, OpenAPI + Scalar UI, playground, /livez, /readyz — but resources are now managed exclusively through the declarative paths: a resources_file (resources.yaml, reloaded on SIGHUP) or direct etcd writes.

This is the final step of the deprecation announced in v0.4.0 (RFC 9745 Deprecation headers) and executes the removal scheduled for v0.5.0+ after #848 lifted the write-path-exclusive validations into the canonical schemas so the declarative paths enforce them.

⚠️ Breaking changes

BeforeAfter
POST /admin/v1/<kind>, PUT/DELETE /admin/v1/<kind>/{id} (deprecated, functional)405 with Allow: GET
POST /admin/v1/api_keys/{id}/rotate (and apikeys spelling)404 — the route is gone
File mode: writes rejected with 409 naming the resources file405, same as every other mode
Rotate returned a fresh plaintext keyNo DP-side plaintext rotation. Rotate declaratively: write the same resource id with a new key_hash — the old plaintext stops authenticating as soon as the write propagates (pinned in apikey-lifecycle-e2e)

What to update:

  • Scripts that created/updated/deleted resources via :3001 → write resources.yaml (validate offline with aisix validate --resources <file>, reload with SIGHUP) or write entity-value JSON to etcd at {prefix}/{kind}/{id}.
  • Scripts that rotated caller keys via /rotate → hash the new secret client-side and update the resource's key_hash.
  • The published OpenAPI (/admin/openapi.json) no longer documents write operations; the write-only component schemas (ApiKeyRequest, ApiKeyEntry, ApiKeyRotateResponse, DeleteResponse) are gone from components.schemas.

What changed

Router / handlers — every /admin/v1/* resource route serves get(...) only; both rotate routes deleted; the file-managed write guard and the RFC 9745 deprecation-header middleware deleted (nothing left to mark). 43 write/rotate/uniqueness handler fns removed across the 8 handler modules.

Store layerConfigStore is now a read-only trait (16 put_*/delete_* methods removed, StoreError::ReadOnly gone). EtcdConfigStore keeps only reads (module doc rewritten: resources reach etcd through the declarative paths). FileManagedStore::new(snapshot) drops the path parameter — read-only by construction. InMemoryStore keeps #[cfg(test)] inherent write methods used by unit-test seeding.

OpenAPI — 24 write operations and the rotate path removed from the base document; the no-op write-deprecation marker pass deleted; unreachable component schemas pruned via a reachability walk from paths. New gate test pins that the published reference documents zero non-GET operations under /admin/v1/ and zero deprecated marks anywhere.

Tests — write-path unit tests (CRUD flows, rotate atomicity, write-auth, write-validation) deleted; read tests re-seeded through InMemoryStore; new contract tests pin 405 + Allow: GET on every collection/:id route (auth'd and unauthenticated) and 404 on both rotate spellings. The etcd integration test now seeds via direct etcd_client puts — the path operators actually use.

e2eAdminClient write helpers removed (reads stay; SeedClient is the write front door). file-resource-source-e2e pins the new file-mode contract (reads serve the file, writes 405, rotate 404, unauthenticated write 405 with no file-path leak). apikey-lifecycle-e2e's rotate coverage became a declarative secret-swap test (old plaintext dies immediately, id unchanged). Deleted: seed-vs-admin-characterization-e2e (its own comment scheduled retirement once the seed migration completed), apikey-budget-e2e (tested write-path 400s), the sdk-compat deprecation-header test, and forward-compat's admin strict-write test.

Docs — README, config.example.yaml, crate module docs updated to the read-only story.

Non-goals / follow-ups (filed internally)

  • 3 OpenAPI-parsing validations (mcp validate_spec, HeaderName typing, duplicate tool names) still lack declarative-path enforcement — blocked on a dependency-direction extraction, tracked internally.
  • etcd watch keeps last-good on an invalid PUT while resync drops the row — policy decision tracked internally.

Verification

  • cargo fmt --check, cargo clippy --workspace --all-targets -- -D warnings, cargo test --workspace — all green.
  • Real binary, file mode: boots with resources.yaml, GET list serves the file, POST /admin/v1/models → 405 + Allow: GET, rotate → 404, /admin/openapi.json documents no admin writes.
  • Local e2e stack run before push.

Summary by CodeRabbit

  • Changes
    • The Admin API is now read-only for managed resources.
    • Resource listing and detail views remain available through GET requests.
    • Creation, updates, deletion, and API-key rotation through the Admin API are no longer supported.
    • Unsupported write requests return 405 responses, while rotation routes return 404.
    • Manage resources through resources_file reloads or direct etcd writes.
    • Updated configuration, validation guidance, and end-to-end coverage to reflect the read-only administration model.

CopilotAI balanced review requested due to automatic review settings August 10, 2026 05:09
@coderabbitai

coderabbitaiBot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9acb9ae1-7d4d-42ce-83f0-61de46c03b1a

📥 Commits

Reviewing files that changed from the base of the PR and between f9c10cc and f43eab2.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (25)
  • README.md
  • config.example.yaml
  • crates/aisix-admin/src/a2a_agents_handlers.rs
  • crates/aisix-admin/src/apikeys_handlers.rs
  • crates/aisix-admin/src/cache_policies_handlers.rs
  • crates/aisix-admin/src/guardrails_handlers.rs
  • crates/aisix-admin/src/lib.rs
  • crates/aisix-admin/src/mcp_servers_handlers.rs
  • crates/aisix-admin/src/models_handlers.rs
  • crates/aisix-admin/src/observability_exporters_handlers.rs
  • crates/aisix-admin/src/openapi.rs
  • crates/aisix-admin/src/provider_keys_handlers.rs
  • crates/aisix-admin/src/store.rs
  • crates/aisix-admin/tests/etcd_integration.rs
  • crates/aisix-core/src/bin/dump-schema.rs
  • crates/aisix-core/src/models/mod.rs
  • crates/aisix-core/src/models/schema.rs
  • crates/aisix-server/src/main.rs
  • schemas/README.md
  • tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts
  • tests/e2e/src/cases/config-forward-compat-e2e.test.ts
  • tests/e2e/src/cases/openai-sdk-compat.test.ts
  • tests/e2e/src/cases/smoke.test.ts
  • tests/e2e/src/harness/app.ts
  • tests/e2e/src/harness/seed.ts
💤 Files with no reviewable changes (1)
  • tests/e2e/src/cases/smoke.test.ts
🚧 Files skipped from review as they are similar to previous changes (12)
  • crates/aisix-server/src/main.rs
  • config.example.yaml
  • crates/aisix-admin/src/provider_keys_handlers.rs
  • crates/aisix-admin/src/models_handlers.rs
  • schemas/README.md
  • crates/aisix-core/src/bin/dump-schema.rs
  • tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts
  • tests/e2e/src/cases/config-forward-compat-e2e.test.ts
  • crates/aisix-admin/src/apikeys_handlers.rs
  • README.md
  • crates/aisix-admin/tests/etcd_integration.rs
  • crates/aisix-admin/src/lib.rs

📝 Walkthrough

Walkthrough

The admin listener now exposes read-only resource routes. Resource writes use resources_file reloads, direct etcd writes, or declarative seed helpers. Stores, server wiring, tests, and documentation were updated.

Changes

Read-only admin resource surface

Layer / File(s)Summary
Read-only store contracts
crates/aisix-admin/src/store.rs, crates/aisix-admin/src/etcd_store.rs, crates/aisix-admin/src/file_store.rs, crates/aisix-admin/src/error.rs, crates/aisix-admin/src/state.rs
Store traits and implementations now support resource reads only.
GET-only admin routes
crates/aisix-admin/src/lib.rs, crates/aisix-admin/src/*_handlers.rs
Create, update, delete, and API-key rotation routes were removed.
Read-only server wiring
crates/aisix-server/src/main.rs
The server constructs read-only file-backed admin state and removes the file-managed write guard.
Direct etcd setup and integration coverage
crates/aisix-admin/tests/etcd_integration.rs, crates/aisix-admin/src/etcd_store.rs
Tests seed canonical documents directly in etcd and verify read-only behavior.
Declarative E2E flows and documentation
tests/e2e/src/cases/*, tests/e2e/src/harness/*, README.md, config.example.yaml, schemas/README.md
E2E setup and documentation now use declarative resource-management paths.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
participant EtcdClient
participant AdminAPI
participant ConfigLoader
EtcdClient->>EtcdClient: write canonical resource document
AdminAPI->>EtcdClient: list/get resource
EtcdClient-->>AdminAPI: resource document
ConfigLoader->>EtcdClient: load canonical resource documents
EtcdClient-->>ConfigLoader: configuration data
Loading

Possibly related PRs

  • api7/aisix#792: Related through direct etcd seeding and readiness migration.
  • api7/aisix#800: Related through removal of Admin API writes and E2E harness changes.
  • api7/aisix#848: Related through A2A and MCP admin-handler changes.

Suggested reviewers:jarvis9443


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error, 1 warning)

Check nameStatusExplanationResolution
Security Check❌ ErrorCategory 1: admin GETs serialize ProviderKey.api_key, McpServer.secret, and A2aAgent.secret; PublicApiKey also returns key_hash without redaction.Return redacted DTOs for every secret-bearing resource, omit key_hash and credential fields, and replace backend-detail error responses with generic messages.
E2e Test Quality Review⚠️ WarningE2E quality issue: config-forward-compat-e2e.test.ts test 3 deletes yellowKeyId created only by test 1, with no ordering declaration, creating a hidden test dependency.Make each test self-contained by seeding and cleaning its own rows, or explicitly declare and document sequential execution; also keep route-matrix coverage at the process level if all aliases are contractual.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly and concisely identifies the main breaking change: removing the Admin API resource write path.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/remove-admin-write-path

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/aisix-admin/tests/etcd_integration.rs (1)

383-489: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Add the missing a2a_agents canonical document.

The PR retains eight resource kinds, but writes contains seven entries. It omits a2a_agents. Add a valid A2A agent document, assert stats.accepted == 8, and assert snap.a2a_agents.len() == 1.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/aisix-admin/tests/etcd_integration.rs` around lines 383 - 489, Extend
the writes array with a valid a2a_agents canonical document using the existing
seed flow, then update the accepted-entry assertion from 7 to 8. Add a
corresponding snap.a2a_agents length assertion expecting exactly one loaded
agent, leaving the other resource assertions unchanged.
🧹 Nitpick comments (1)
crates/aisix-admin/src/lib.rs (1)

1131-1198: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Read coverage now spans only two of eight resource kinds. Deleting the write handlers also deleted each handler module's test module. The replacement read tests in lib.rs seed only models and api_keys. The other six kinds — provider_keys, guardrails, cache_policies, observability_exporters, mcp_servers, a2a_agents — have 405 write-refusal coverage but no test proving that GET serves a seeded entry. These handler bodies are hand-written per module, not macro-generated, so a wrong store call or a wrong response shape in one of the six would pass CI.

  • crates/aisix-admin/src/lib.rs#L1131-L1198: extend build_seedable_state with seed helpers for the remaining six kinds, then add list and get-by-id assertions for each, mirroring list_models_returns_seeded_entries and get_model_serves_seeded_entry.
  • crates/aisix-admin/src/a2a_agents_handlers.rs#L12: add a test that list_a2a_agents and get_a2a_agent return a seeded A2aAgent, or confirm the new lib.rs tests cover this module.
  • crates/aisix-admin/src/mcp_servers_handlers.rs#L9: add the same list/get coverage for McpServer, or confirm the new lib.rs tests cover this module.

Note that InMemoryStore currently exposes only put_model and put_apikey as #[cfg(test)] helpers, so seeding the other six kinds requires adding matching helpers in crates/aisix-admin/src/store.rs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/aisix-admin/src/lib.rs` around lines 1131 - 1198, Extend
crates/aisix-admin/src/store.rs with cfg(test) put helpers for provider_keys,
guardrails, cache_policies, observability_exporters, mcp_servers, and
a2a_agents, then update build_seedable_state and the tests in
crates/aisix-admin/src/lib.rs#L1131-L1198 to seed each kind and assert both list
and get-by-id responses, mirroring the model tests. Ensure
crates/aisix-admin/src/a2a_agents_handlers.rs#L12 and
crates/aisix-admin/src/mcp_servers_handlers.rs#L9 are covered by these lib.rs
tests; no direct handler changes are required unless coverage cannot exercise
their list/get functions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/aisix-admin/src/lib.rs`:
- Around line 1200-1230: Remove the stale rotation-coverage comment immediately
before openapi_apikey_schema_excludes_max_budget_usd, and delete the empty
CachePolicy CRUD and Health endpoint section comments. Preserve the
guardrail_payload function, the ObservabilityExporter CRUD comment, and all
surrounding tests and formatting.
In `@crates/aisix-admin/tests/etcd_integration.rs`:
- Around line 317-354: Extend the resource-route assertions in the integration
test to issue both PUT and DELETE requests to /admin/v1/models, asserting each
returns METHOD_NOT_ALLOWED and includes GET in the Allow header. Keep the
existing POST check and final etcd emptiness assertion so all refused write
methods verify that no data is written.
In `@crates/aisix-server/src/main.rs`:
- Around line 898-902: Update the file-source match arm in the admin_store
initialization to bind the second tuple element as Some(_) instead of
Some(path), preserving the existing condition and FileManagedStore construction.
In `@tests/e2e/src/cases/file-resource-source-e2e.test.ts`:
- Around line 179-213: Update the rejected-write assertions in the e2e test to
require the exact Allow header value "GET" instead of merely containing GET,
covering both the authenticated POST/DELETE responses and the unauthenticated
POST response. Preserve the existing status and response-body assertions.
In `@tests/e2e/src/cases/openai-sdk-compat.test.ts`:
- Around line 54-84: After seeding the API key in the test setup, add an
independent readiness poll using the seeded caller credentials against
authenticated GET /v1/models, continuing until it returns 200. Remove the
client.chat.completions.create-based propagation gate and keep that call
exclusively for the SDK chat behavior under test.
---
Outside diff comments:
In `@crates/aisix-admin/tests/etcd_integration.rs`:
- Around line 383-489: Extend the writes array with a valid a2a_agents canonical
document using the existing seed flow, then update the accepted-entry assertion
from 7 to 8. Add a corresponding snap.a2a_agents length assertion expecting
exactly one loaded agent, leaving the other resource assertions unchanged.
---
Nitpick comments:
In `@crates/aisix-admin/src/lib.rs`:
- Around line 1131-1198: Extend crates/aisix-admin/src/store.rs with cfg(test)
put helpers for provider_keys, guardrails, cache_policies,
observability_exporters, mcp_servers, and a2a_agents, then update
build_seedable_state and the tests in crates/aisix-admin/src/lib.rs#L1131-L1198
to seed each kind and assert both list and get-by-id responses, mirroring the
model tests. Ensure crates/aisix-admin/src/a2a_agents_handlers.rs#L12 and
crates/aisix-admin/src/mcp_servers_handlers.rs#L9 are covered by these lib.rs
tests; no direct handler changes are required unless coverage cannot exercise
their list/get functions.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f3cea19a-3b55-4ad2-b83e-bc29037270c5

📥 Commits

Reviewing files that changed from the base of the PR and between b77f1a6 and 152c57d.

📒 Files selected for processing (26)
  • README.md
  • config.example.yaml
  • crates/aisix-admin/src/a2a_agents_handlers.rs
  • crates/aisix-admin/src/apikeys_handlers.rs
  • crates/aisix-admin/src/cache_policies_handlers.rs
  • crates/aisix-admin/src/error.rs
  • crates/aisix-admin/src/etcd_store.rs
  • crates/aisix-admin/src/file_store.rs
  • crates/aisix-admin/src/guardrails_handlers.rs
  • crates/aisix-admin/src/lib.rs
  • crates/aisix-admin/src/mcp_servers_handlers.rs
  • crates/aisix-admin/src/models_handlers.rs
  • crates/aisix-admin/src/observability_exporters_handlers.rs
  • crates/aisix-admin/src/openapi.rs
  • crates/aisix-admin/src/provider_keys_handlers.rs
  • crates/aisix-admin/src/state.rs
  • crates/aisix-admin/src/store.rs
  • crates/aisix-admin/tests/etcd_integration.rs
  • crates/aisix-server/src/main.rs
  • tests/e2e/src/cases/apikey-budget-e2e.test.ts
  • tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts
  • tests/e2e/src/cases/config-forward-compat-e2e.test.ts
  • tests/e2e/src/cases/file-resource-source-e2e.test.ts
  • tests/e2e/src/cases/openai-sdk-compat.test.ts
  • tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts
  • tests/e2e/src/harness/admin.ts
💤 Files with no reviewable changes (8)
  • tests/e2e/src/cases/apikey-budget-e2e.test.ts
  • tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts
  • crates/aisix-admin/src/state.rs
  • crates/aisix-admin/src/observability_exporters_handlers.rs
  • crates/aisix-admin/src/cache_policies_handlers.rs
  • crates/aisix-admin/src/models_handlers.rs
  • crates/aisix-admin/src/guardrails_handlers.rs
  • crates/aisix-admin/src/provider_keys_handlers.rs

Comment threadcrates/aisix-admin/src/lib.rs Outdated
Comment threadcrates/aisix-admin/tests/etcd_integration.rs Outdated
Comment threadcrates/aisix-server/src/main.rs
Comment on lines +179 to +213
expect(postRes.status).toBe(405);
expect(postRes.headers.get("allow")).toContain("GET");
await postRes.text();

// The refused write did not change the resource set.
const relist = await fetch(`${app.adminUrl}/admin/v1/models`, { headers: auth });
expect(((await relist.json()) as unknown[]).length).toBe(2);

// DELETE and rotate are covered by the same guard.
const delRes = await fetch(`${app.adminUrl}/admin/v1/models/any-id`, {
method: "DELETE",
headers: auth,
});
expect(delRes.status).toBe(409);
expect(delRes.status).toBe(405);
expect(delRes.headers.get("allow")).toContain("GET");
await delRes.text();

// The rotate route was removed outright (it was POST-only), so the
// path no longer exists at all.
const rotateRes = await fetch(`${app.adminUrl}/admin/v1/api_keys/any-id/rotate`, {
method: "POST",
headers: auth,
});
expect(rotateRes.status).toBe(409);
expect(rotateRes.status).toBe(404);
await rotateRes.text();

// Auth ordering: an UNAUTHENTICATED write still gets 401, and the
// 401 body must not leak the resources-file path (that detail is
// only for authenticated admins).
// Method routing answers before auth: an unauthenticated write gets
// the same 405 — there is no write endpoint left to protect, and
// the 405 body carries no resources-file detail to leak.
const unauthed = await fetch(`${app.adminUrl}/admin/v1/models`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ display_name: "nope" }),
});
expect(unauthed.status).toBe(401);
const unauthedBody = (await unauthed.json()) as { error_msg: string };
expect(unauthedBody.error_msg).not.toContain(app.resourcesPath!);
expect(unauthed.status).toBe(405);
expect((await unauthed.text())).not.toContain(app.resourcesPath!);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert the exact Allow header value.

Use toBe("GET") for each rejected write. Add the same assertion for the unauthenticated request. toContain("GET") also accepts an invalid value such as GET, POST.

Based on PR objectives, rejected resource writes must return 405 with Allow: GET.

Proposed test update
- expect(postRes.headers.get("allow")).toContain("GET");+ expect(postRes.headers.get("allow")).toBe("GET");
...
- expect(delRes.headers.get("allow")).toContain("GET");+ expect(delRes.headers.get("allow")).toBe("GET");
...
expect(unauthed.status).toBe(405);
+ expect(unauthed.headers.get("allow")).toBe("GET");
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
expect(postRes.status).toBe(405);
expect(postRes.headers.get("allow")).toContain("GET");
awaitpostRes.text();
// The refused write did not change the resource set.
constrelist=awaitfetch(`${app.adminUrl}/admin/v1/models`,{headers: auth});
expect(((awaitrelist.json())asunknown[]).length).toBe(2);
// DELETE and rotate are covered by the same guard.
constdelRes=awaitfetch(`${app.adminUrl}/admin/v1/models/any-id`,{
method: "DELETE",
headers: auth,
});
expect(delRes.status).toBe(409);
expect(delRes.status).toBe(405);
expect(delRes.headers.get("allow")).toContain("GET");
awaitdelRes.text();
// The rotate route was removed outright (it was POST-only), so the
// path no longer exists at all.
constrotateRes=awaitfetch(`${app.adminUrl}/admin/v1/api_keys/any-id/rotate`,{
method: "POST",
headers: auth,
});
expect(rotateRes.status).toBe(409);
expect(rotateRes.status).toBe(404);
awaitrotateRes.text();
// Auth ordering: an UNAUTHENTICATED write still gets 401, and the
// 401 body must not leak the resources-file path (that detail is
// only for authenticated admins).
// Method routing answers before auth: an unauthenticated write gets
// the same 405 — there is no write endpoint left to protect, and
// the 405 body carries no resources-file detail to leak.
constunauthed=awaitfetch(`${app.adminUrl}/admin/v1/models`,{
method: "POST",
headers: {"content-type": "application/json"},
body: JSON.stringify({display_name: "nope"}),
});
expect(unauthed.status).toBe(401);
constunauthedBody=(awaitunauthed.json())as{error_msg: string};
expect(unauthedBody.error_msg).not.toContain(app.resourcesPath!);
expect(unauthed.status).toBe(405);
expect((awaitunauthed.text())).not.toContain(app.resourcesPath!);
expect(postRes.status).toBe(405);
expect(postRes.headers.get("allow")).toBe("GET");
awaitpostRes.text();
// The refused write did not change the resource set.
constrelist=awaitfetch(`${app.adminUrl}/admin/v1/models`,{headers: auth});
expect(((awaitrelist.json())asunknown[]).length).toBe(2);
constdelRes=awaitfetch(`${app.adminUrl}/admin/v1/models/any-id`,{
method: "DELETE",
headers: auth,
});
expect(delRes.status).toBe(405);
expect(delRes.headers.get("allow")).toBe("GET");
awaitdelRes.text();
// The rotate route was removed outright (it was POST-only), so the
// path no longer exists at all.
constrotateRes=awaitfetch(`${app.adminUrl}/admin/v1/api_keys/any-id/rotate`,{
method: "POST",
headers: auth,
});
expect(rotateRes.status).toBe(404);
awaitrotateRes.text();
// Method routing answers before auth: an unauthenticated write gets
// the same 405 — there is no write endpoint left to protect, and
// the 405 body carries no resources-file detail to leak.
constunauthed=awaitfetch(`${app.adminUrl}/admin/v1/models`,{
method: "POST",
headers: {"content-type": "application/json"},
body: JSON.stringify({display_name: "nope"}),
});
expect(unauthed.status).toBe(405);
expect(unauthed.headers.get("allow")).toBe("GET");
expect((awaitunauthed.text())).not.toContain(app.resourcesPath!);
🧰 Tools
🪛 ast-grep (0.45.0)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { execFile } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/e2e/src/cases/file-resource-source-e2e.test.ts` around lines 179 - 213,
Update the rejected-write assertions in the e2e test to require the exact Allow
header value "GET" instead of merely containing GET, covering both the
authenticated POST/DELETE responses and the unauthenticated POST response.
Preserve the existing status and response-body assertions.

Comment threadtests/e2e/src/cases/openai-sdk-compat.test.ts

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Removes Admin API resource writes, leaving read-only resource endpoints and moving management to declarative file or etcd paths.

Changes:

  • Removes write handlers, routes, store operations, rotation, and deprecation middleware.
  • Updates OpenAPI, documentation, and tests for the read-only contract.
  • Migrates test setup to direct etcd seeding.

Reviewed changes

Copilot reviewed 25 out of 26 changed files in this pull request and generated 12 comments.

Show a summary per file
FileDescription
README.mdDocuments the read-only Admin API.
config.example.yamlClarifies declarative resource management.
crates/aisix-server/src/main.rsWires read-only admin stores.
crates/aisix-admin/src/lib.rsRemoves write routes and middleware.
crates/aisix-admin/src/openapi.rsRemoves write operations and schemas.
crates/aisix-admin/src/store.rsMakes ConfigStore read-only.
crates/aisix-admin/src/state.rsRemoves file-write guard state.
crates/aisix-admin/src/error.rsRemoves write-related errors.
crates/aisix-admin/src/file_store.rsRetains snapshot reads only.
crates/aisix-admin/src/etcd_store.rsRetains etcd reads only.
crates/aisix-admin/src/models_handlers.rsRemoves model writes.
crates/aisix-admin/src/apikeys_handlers.rsRemoves API-key writes and rotation.
crates/aisix-admin/src/provider_keys_handlers.rsRemoves provider-key writes.
crates/aisix-admin/src/guardrails_handlers.rsRemoves guardrail writes.
crates/aisix-admin/src/cache_policies_handlers.rsRemoves cache-policy writes.
crates/aisix-admin/src/observability_exporters_handlers.rsRemoves exporter writes.
crates/aisix-admin/src/mcp_servers_handlers.rsRemoves MCP-server writes.
crates/aisix-admin/src/a2a_agents_handlers.rsRemoves A2A-agent writes.
crates/aisix-admin/tests/etcd_integration.rsTests direct-etcd writes and admin reads.
tests/e2e/src/harness/admin.tsRemoves Admin API write helpers.
tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.tsRemoves obsolete path comparison tests.
tests/e2e/src/cases/openai-sdk-compat.test.tsMigrates setup to etcd seeding.
tests/e2e/src/cases/file-resource-source-e2e.test.tsTests file-mode read-only behavior.
tests/e2e/src/cases/config-forward-compat-e2e.test.tsRemoves Admin write validation coverage.
tests/e2e/src/cases/apikey-lifecycle-e2e.test.tsReplaces rotation with declarative secret swapping.
tests/e2e/src/cases/apikey-budget-e2e.test.tsRemoves obsolete write-path validation test.
Suppressed comments (1)

crates/aisix-admin/src/mcp_servers_handlers.rs:9

  • This removal also eliminates the only production call to aisix_mcp::validate_spec. The file and etcd loaders only run the core JSON Schema, which does not reject specs with zero generatable operations or colliding sanitized tool names, so the newly exclusive declarative paths accept configurations the former write API rejected. Add the semantic validation to both loaders before removing this path.
use aisix_core::McpServer;

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment threadcrates/aisix-server/src/main.rs Outdated
use serde::{Deserialize, Serialize};
use serde_json::Value;
use uuid::Uuid;
use serde::Serialize;
Comment threadcrates/aisix-server/src/main.rs Outdated
Comment on lines 9 to 10
//! replaces the `key` field with a freshly-generated `sk-*` value and
//! bumps the revision, invalidating the old credential.
Comment on lines 8 to 9
//!
//! ids are UUID v4s generated on POST; PUT preserves the existing id.
@@ -38,7 +38,7 @@ const OPENAPI_JSON_BASE: &str = r##"{
"info": {
"title": "AISIX Admin API",
"version": "dev",
"description": "The AISIX Admin API configures an open-source AISIX gateway at runtime. Use it when you operate the gateway directly and need to create or update models, caller API keys, provider credentials, guardrails, cache policies, and observability exporters.\n\nThe write endpoints (POST, PUT, DELETE) are deprecated in favor of declarative configuration: load resources from a `resources_file` (`resources.yaml`) or write them to etcd directly. Write endpoints remain functional, and every mutating response carries a `Deprecation` header (RFC 9745) plus a `Link` header with `rel=\"deprecation\"` pointing at the migration documentation. Read endpoints are not deprecated.\n\nGateways connected to AISIX Cloud do not expose this listener. Configure them through AISIX Cloud."
"description": "The AISIX Admin API is the read-only operational surface of an open-source AISIX gateway: list and inspect the loaded models, caller API keys, provider credentials, guardrails, MCP servers, A2A agents, cache policies, and observability exporters, check per-model upstream health, and drive the playground.\n\nResource write endpoints were removed in favor of declarative configuration: declare resources in a `resources_file` (`resources.yaml`) and reload with SIGHUP, or write them to etcd directly. See the resources file reference at https://docs.api7.ai/ai-gateway/reference/resources-file.\n\nGateways connected to AISIX Cloud do not expose this listener. Configure them through AISIX Cloud."
Comment on lines 4 to 5
//! validate against the JSON schema, reject duplicate names (409),
//! generate a uuid v4 on POST, bump revision on PUT.
Comment on lines 4 to 5
//! validate against the JSON schema, reject duplicate names (409),
//! generate a uuid v4 on POST, bump revision on PUT.
Comment on lines 5 to 6
//! PUT. Additionally rejects a name containing the reserved tool-namespace
//! separator `__`, since the name prefixes the server's tools.
Comment on lines 8 to 9
//! every configuration path rejects an incomplete credential set; the checks
//! below are defense in depth.
@moonmingmoonming self-assigned this Aug 10, 2026
@moonming

Copy link
Copy Markdown
CollaboratorAuthor

Review triage — every inline comment dispositioned; fixes landed in f9c10cc and e34694b.

Fixed

  • 7 handler module docs rewritten to the read-only contract; stale rotate/CRUD section comments removed (e34694b)
  • OpenAPI Caller API Keys tag no longer says "and key rotation"; path/Entry descriptions no longer claim ids are generated by the Admin API or that update/rotate bumps revisions (f9c10cc, e34694b)
  • main.rs: unused Some(path) binding → Some(_); canonical product name; removed a comment referencing write rejections (e34694b)
  • etcd integration: refused-writes test now covers PUT and DELETE with Allow assertions, and the rotate-absence checks GET the rotate URIs too — POST-only 404 couldn't distinguish a deleted route from the old handler's unknown-id 404 (f9c10cc, e34694b)
  • sdk-compat e2e: readiness gate switched from the SDK chat path to an independent authenticated GET /v1/models probe per the harness gate rules (e34694b)
  • config.example.yaml: dead docs/api-admin.md link and Admin-API management claims replaced with the declarative sources (f9c10cc)

Not adopted, with reasons

  • Assert Allow with toBe("GET"): the real binary answers Allow: GET,HEAD (axum's MethodRouter advertises HEAD alongside GET), so an exact "GET" match fails against actual behavior. toContain("GET") plus the 405 status is the contract; the 405 itself already proves no write method is routed.
  • Duplicate key_hash uniqueness on the direct-etcd path (assert_unique_key removal): real gap, but pre-existing — the admin-side check never guarded direct etcd writes, which existed before this PR and are used by the control plane (which enforces uniqueness org-side) and the file source (which has its own per-kind identity check). Filed internally for loader-side conflict detection with /status/config visibility; tracked as a follow-up rather than blocking the removal.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts (1)

276-301: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Use an independent readiness check for key setup.

seedKey gates propagation with a POST /v1/chat/completions request. The rotation and deletion flows also use the chat authorization path for their assertions. A chat failure can stop the test before it checks the key transition.

Seed the caller keys, then verify readiness with GET /v1/models and require 200. Keep chat requests for the actual rotation and revocation assertions.

As per coding guidelines, E2E readiness gates must use an independent condition, and caller API keys must be checked with GET /v1/models returning 200.

Also applies to: 311-312

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts` around lines 276 - 301,
Update the key setup readiness flow around seedKey and the related
rotation/deletion cases to use an independent GET /v1/models request requiring
HTTP 200, rather than POST /v1/chat/completions. Keep chat requests in the
secret-swap and revocation assertions only, so readiness failures cannot mask
key-transition checks.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/aisix-core/src/bin/dump-schema.rs`:
- Around line 44-47: Update the comment near the STRICT shape documentation to
qualify unknown-field rejection as applying only where the resource schema is
closed. Preserve the existing distinction between declarative write contracts
and lenient etcd reads, while acknowledging resource-specific exceptions such as
open fields and custom guardrail validation documented in the schema guidance.
In `@tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts`:
- Around line 304-307: Update the combined setup guard in the affected E2E test
to also check that otlp is available, skipping and returning when any shared
setup value—including etcdReachable, app, seed, or otlp—is missing.
---
Outside diff comments:
In `@tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts`:
- Around line 276-301: Update the key setup readiness flow around seedKey and
the related rotation/deletion cases to use an independent GET /v1/models request
requiring HTTP 200, rather than POST /v1/chat/completions. Keep chat requests in
the secret-swap and revocation assertions only, so readiness failures cannot
mask key-transition checks.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: aa61ea23-b8c7-4503-b1c7-e50f6378ee7d

📥 Commits

Reviewing files that changed from the base of the PR and between 152c57d and f9c10cc.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (12)
  • README.md
  • config.example.yaml
  • crates/aisix-admin/Cargo.toml
  • crates/aisix-admin/src/apikeys_handlers.rs
  • crates/aisix-admin/src/error.rs
  • crates/aisix-admin/src/lib.rs
  • crates/aisix-admin/src/openapi.rs
  • crates/aisix-admin/tests/etcd_integration.rs
  • crates/aisix-core/src/bin/dump-schema.rs
  • crates/aisix-etcd/src/provider.rs
  • schemas/README.md
  • tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts
💤 Files with no reviewable changes (2)
  • crates/aisix-admin/Cargo.toml
  • crates/aisix-admin/src/error.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • config.example.yaml
  • README.md
  • crates/aisix-admin/src/apikeys_handlers.rs
  • crates/aisix-admin/src/lib.rs

Comment threadcrates/aisix-core/src/bin/dump-schema.rs
Comment on lines +304 to 307
if (!etcdReachable || !app || !seed) {
ctx.skip();
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Include otlp in the setup guard.

The guard omits otlp. Add it to the combined setup check so the test does not run with incomplete shared setup.

Based on learnings, E2E cases must preserve if (!etcdReachable || !app || !seed || !otlp) { ctx.skip(); return; }.

Proposed fix
- if (!etcdReachable || !app || !seed) {+ if (!etcdReachable || !app || !seed || !otlp) {
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if(!etcdReachable||!app||!seed){
ctx.skip();
return;
}
if(!etcdReachable||!app||!seed||!otlp){
ctx.skip();
return;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts` around lines 304 - 307,
Update the combined setup guard in the affected E2E test to also check that otlp
is available, skipping and returning when any shared setup value—including
etcdReachable, app, seed, or otlp—is missing.

Source: Learnings

The admin listener keeps its read surface (lists/gets for all 8
resource kinds incl. the former apikeys spelling, models/status,
health, OpenAPI + Scalar, playground, livez/readyz); resources are
managed exclusively through the declarative paths — resources_file
(SIGHUP reload) or direct etcd writes.
BREAKING CHANGE: POST/PUT/DELETE on /admin/v1/* answer 405 with
Allow: GET (409 file-managed rejection included); the api-key rotate
route is gone (404) — rotate declaratively by writing the same
resource id with a new key_hash. The published OpenAPI documents no
write operations; write-only component schemas (ApiKeyRequest,
ApiKeyEntry, ApiKeyRotateResponse, DeleteResponse) are removed.
- router: every /admin/v1/* resource route serves get() only; rotate
routes, file-managed write guard, and RFC 9745 deprecation-header
middleware deleted; 43 write/rotate/uniqueness handler fns removed
- store: ConfigStore is read-only (16 put_*/delete_* methods and
StoreError::ReadOnly removed); EtcdConfigStore keeps reads only;
FileManagedStore::new(snapshot) drops the path param; InMemoryStore
keeps #[cfg(test)] inherent writes for unit-test seeding
- openapi: 24 write ops + rotate path removed from the base document;
no-op deprecation-marker pass deleted; unreachable component
schemas pruned; new gate pins GET-only /admin/v1/* and zero
deprecated marks
- tests: write-path suites deleted; read tests seed via InMemoryStore;
new 405/Allow + rotate-404 contract tests; etcd integration tests
seed via direct etcd writes (the declarative front door) and pin
that refused writes never touch etcd
- e2e: AdminClient write helpers removed (SeedClient is the write
front door); file-resource-source pins the new read-only contract;
apikey-lifecycle rotate coverage became a declarative secret-swap
test; obsolete write-path cases deleted
- docs: README, config.example.yaml, crate module docs updated
Second-auditor findings on the removal PR, all test/doc-level (no
runtime changes):
- rotate-404 tests now GET the rotate URIs too — POST-only 404 could
not distinguish a deleted route from the old handler's unknown-id
404; GET answers 405 on a surviving POST-only route
- e2e: deleting a key's etcd entry revokes an in-use bearer
(fail-closed, unknown-token 401, other keys unaffected) — the
deletion branch had lost its only end-to-end proof
- etcd integration: a2a_agents round-trip + loader coverage (7 -> 8
kinds)
- apikeys read test pins the full PublicApiKey projection
(allowed_tools/disabled/expires_at), not just the id
- OpenAPI descriptions stop claiming ids are generated by the Admin
API and revisions increment on update/rotate
- stale write-path narrative removed: config.example.yaml (dead
docs/api-admin.md link), schemas/README, dump-schema comment,
apikeys_handlers module doc, aisix-etcd provider doc, README RBAC
row + e2e counts
- dead code: AdminError::{BadRequest,Conflict,Schema} variants and
the aisix-mcp/uuid dependencies left over from the write path
Per-comment review triage:
- 7 handler module docs rewritten from CRUD-era text to the surviving
read-only contract; stale rotate/CRUD section comments in the
aisix-admin test module removed
- OpenAPI 'Caller API Keys' tag no longer advertises key rotation
- main.rs: unused match binding -> Some(_); canonical product name;
dropped a comment referencing the removed write-rejection path
- etcd integration: refused-writes test now covers PUT and DELETE
(405 + Allow), not just POST
- sdk-compat e2e: readiness gate switched from the SDK chat path (the
behavior under test) to an independent authenticated GET /v1/models
probe, per the harness gate rules
Cold-audit closeout:
- removed_resource_writes_answer_405_with_allow_get now generates the
FULL matrix (9 route spellings x POST/PUT/DELETE) instead of a
sampled subset — a partial revert (e.g. PUT re-added on one {id}
route) previously passed the whole suite
- last stale write-path narrative: store.rs module doc (read-only
trait), aisix-core schema.rs/models docs (declarative writers, not
'Admin API ... 400'), e2e smoke/seed/app/forward-compat headers no
longer cite deleted characterization or held-back write cases,
openapi.rs base-doc comment names a surviving schema
Round-two auditor findings:
- OpenAPI: the two remaining Entry revision descriptions (McpServer,
A2aAgent) stop describing create/update lifecycle; a regression
assertion now rejects write-lifecycle prose on any documented
revision field
- schemas/README + schema.rs + dump-schema: scope the strict-contract
claim to the in-repo writers (aisix validate, file source) — the
control plane validates its own API schema, a raw direct etcd put
gets no synchronous validation (lenient read only), and
unknown-field rejection applies only where a resource closes fields;
the previous rewrite overclaimed all three
- deletion-revocation e2e: propagation barrier is now a fresh key
seeded after the delete (later etcd revision), so the revocation
assertion is a real assertion instead of a gate poll; a regression
fails the assert, not a 30s timeout
- apikeys projection test seeds rate_limit + allowed_agents and pins
the list entry's projection too; module doc describes PublicApiKey
as an explicit allowlist (not 'minus nothing')
- lifecycle prose: secret-swap invalidates 'as soon as the write
propagates' (not 'immediately'); README e2e counts scoped to
scenario files (183/496)
@moonming
moonmingforce-pushed the feat/remove-admin-write-path branch from 74b0db5 to f43eab2CompareAugust 11, 2026 01:59
@moonming
moonming merged commit b61d270 into mainAug 11, 2026
30 of 32 checks passed
@moonming
moonming deleted the feat/remove-admin-write-path branch August 11, 2026 02:20
Sign up for freeto 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.

2 participants

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(admin)!: remove the Admin API resource write path by moonming · Pull Request #915 · api7/aisix · GitHub
Skip to content

feat(admin)!: remove the Admin API resource write path - #915

Merged
moonming merged 5 commits into
mainfrom
feat/remove-admin-write-path
Aug 11, 2026
Merged

feat(admin)!: remove the Admin API resource write path#915
moonming merged 5 commits into
mainfrom
feat/remove-admin-write-path

Conversation

@moonming

@moonmingmoonming commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Removes the Admin API resource write path. The admin listener (:3001) keeps its read surface — lists/gets for all 8 resource kinds (including the former apikeys spelling), /admin/v1/models/status, /admin/v1/health, OpenAPI + Scalar UI, playground, /livez, /readyz — but resources are now managed exclusively through the declarative paths: a resources_file (resources.yaml, reloaded on SIGHUP) or direct etcd writes.

This is the final step of the deprecation announced in v0.4.0 (RFC 9745 Deprecation headers) and executes the removal scheduled for v0.5.0+ after #848 lifted the write-path-exclusive validations into the canonical schemas so the declarative paths enforce them.

⚠️ Breaking changes

BeforeAfter
POST /admin/v1/<kind>, PUT/DELETE /admin/v1/<kind>/{id} (deprecated, functional)405 with Allow: GET
POST /admin/v1/api_keys/{id}/rotate (and apikeys spelling)404 — the route is gone
File mode: writes rejected with 409 naming the resources file405, same as every other mode
Rotate returned a fresh plaintext keyNo DP-side plaintext rotation. Rotate declaratively: write the same resource id with a new key_hash — the old plaintext stops authenticating as soon as the write propagates (pinned in apikey-lifecycle-e2e)

What to update:

  • Scripts that created/updated/deleted resources via :3001 → write resources.yaml (validate offline with aisix validate --resources <file>, reload with SIGHUP) or write entity-value JSON to etcd at {prefix}/{kind}/{id}.
  • Scripts that rotated caller keys via /rotate → hash the new secret client-side and update the resource's key_hash.
  • The published OpenAPI (/admin/openapi.json) no longer documents write operations; the write-only component schemas (ApiKeyRequest, ApiKeyEntry, ApiKeyRotateResponse, DeleteResponse) are gone from components.schemas.

What changed

Router / handlers — every /admin/v1/* resource route serves get(...) only; both rotate routes deleted; the file-managed write guard and the RFC 9745 deprecation-header middleware deleted (nothing left to mark). 43 write/rotate/uniqueness handler fns removed across the 8 handler modules.

Store layerConfigStore is now a read-only trait (16 put_*/delete_* methods removed, StoreError::ReadOnly gone). EtcdConfigStore keeps only reads (module doc rewritten: resources reach etcd through the declarative paths). FileManagedStore::new(snapshot) drops the path parameter — read-only by construction. InMemoryStore keeps #[cfg(test)] inherent write methods used by unit-test seeding.

OpenAPI — 24 write operations and the rotate path removed from the base document; the no-op write-deprecation marker pass deleted; unreachable component schemas pruned via a reachability walk from paths. New gate test pins that the published reference documents zero non-GET operations under /admin/v1/ and zero deprecated marks anywhere.

Tests — write-path unit tests (CRUD flows, rotate atomicity, write-auth, write-validation) deleted; read tests re-seeded through InMemoryStore; new contract tests pin 405 + Allow: GET on every collection/:id route (auth'd and unauthenticated) and 404 on both rotate spellings. The etcd integration test now seeds via direct etcd_client puts — the path operators actually use.

e2eAdminClient write helpers removed (reads stay; SeedClient is the write front door). file-resource-source-e2e pins the new file-mode contract (reads serve the file, writes 405, rotate 404, unauthenticated write 405 with no file-path leak). apikey-lifecycle-e2e's rotate coverage became a declarative secret-swap test (old plaintext dies immediately, id unchanged). Deleted: seed-vs-admin-characterization-e2e (its own comment scheduled retirement once the seed migration completed), apikey-budget-e2e (tested write-path 400s), the sdk-compat deprecation-header test, and forward-compat's admin strict-write test.

Docs — README, config.example.yaml, crate module docs updated to the read-only story.

Non-goals / follow-ups (filed internally)

  • 3 OpenAPI-parsing validations (mcp validate_spec, HeaderName typing, duplicate tool names) still lack declarative-path enforcement — blocked on a dependency-direction extraction, tracked internally.
  • etcd watch keeps last-good on an invalid PUT while resync drops the row — policy decision tracked internally.

Verification

  • cargo fmt --check, cargo clippy --workspace --all-targets -- -D warnings, cargo test --workspace — all green.
  • Real binary, file mode: boots with resources.yaml, GET list serves the file, POST /admin/v1/models → 405 + Allow: GET, rotate → 404, /admin/openapi.json documents no admin writes.
  • Local e2e stack run before push.

Summary by CodeRabbit

  • Changes
    • The Admin API is now read-only for managed resources.
    • Resource listing and detail views remain available through GET requests.
    • Creation, updates, deletion, and API-key rotation through the Admin API are no longer supported.
    • Unsupported write requests return 405 responses, while rotation routes return 404.
    • Manage resources through resources_file reloads or direct etcd writes.
    • Updated configuration, validation guidance, and end-to-end coverage to reflect the read-only administration model.

CopilotAI balanced review requested due to automatic review settings August 10, 2026 05:09
@coderabbitai

coderabbitaiBot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9acb9ae1-7d4d-42ce-83f0-61de46c03b1a

📥 Commits

Reviewing files that changed from the base of the PR and between f9c10cc and f43eab2.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (25)
  • README.md
  • config.example.yaml
  • crates/aisix-admin/src/a2a_agents_handlers.rs
  • crates/aisix-admin/src/apikeys_handlers.rs
  • crates/aisix-admin/src/cache_policies_handlers.rs
  • crates/aisix-admin/src/guardrails_handlers.rs
  • crates/aisix-admin/src/lib.rs
  • crates/aisix-admin/src/mcp_servers_handlers.rs
  • crates/aisix-admin/src/models_handlers.rs
  • crates/aisix-admin/src/observability_exporters_handlers.rs
  • crates/aisix-admin/src/openapi.rs
  • crates/aisix-admin/src/provider_keys_handlers.rs
  • crates/aisix-admin/src/store.rs
  • crates/aisix-admin/tests/etcd_integration.rs
  • crates/aisix-core/src/bin/dump-schema.rs
  • crates/aisix-core/src/models/mod.rs
  • crates/aisix-core/src/models/schema.rs
  • crates/aisix-server/src/main.rs
  • schemas/README.md
  • tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts
  • tests/e2e/src/cases/config-forward-compat-e2e.test.ts
  • tests/e2e/src/cases/openai-sdk-compat.test.ts
  • tests/e2e/src/cases/smoke.test.ts
  • tests/e2e/src/harness/app.ts
  • tests/e2e/src/harness/seed.ts
💤 Files with no reviewable changes (1)
  • tests/e2e/src/cases/smoke.test.ts
🚧 Files skipped from review as they are similar to previous changes (12)
  • crates/aisix-server/src/main.rs
  • config.example.yaml
  • crates/aisix-admin/src/provider_keys_handlers.rs
  • crates/aisix-admin/src/models_handlers.rs
  • schemas/README.md
  • crates/aisix-core/src/bin/dump-schema.rs
  • tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts
  • tests/e2e/src/cases/config-forward-compat-e2e.test.ts
  • crates/aisix-admin/src/apikeys_handlers.rs
  • README.md
  • crates/aisix-admin/tests/etcd_integration.rs
  • crates/aisix-admin/src/lib.rs

📝 Walkthrough

Walkthrough

The admin listener now exposes read-only resource routes. Resource writes use resources_file reloads, direct etcd writes, or declarative seed helpers. Stores, server wiring, tests, and documentation were updated.

Changes

Read-only admin resource surface

Layer / File(s)Summary
Read-only store contracts
crates/aisix-admin/src/store.rs, crates/aisix-admin/src/etcd_store.rs, crates/aisix-admin/src/file_store.rs, crates/aisix-admin/src/error.rs, crates/aisix-admin/src/state.rs
Store traits and implementations now support resource reads only.
GET-only admin routes
crates/aisix-admin/src/lib.rs, crates/aisix-admin/src/*_handlers.rs
Create, update, delete, and API-key rotation routes were removed.
Read-only server wiring
crates/aisix-server/src/main.rs
The server constructs read-only file-backed admin state and removes the file-managed write guard.
Direct etcd setup and integration coverage
crates/aisix-admin/tests/etcd_integration.rs, crates/aisix-admin/src/etcd_store.rs
Tests seed canonical documents directly in etcd and verify read-only behavior.
Declarative E2E flows and documentation
tests/e2e/src/cases/*, tests/e2e/src/harness/*, README.md, config.example.yaml, schemas/README.md
E2E setup and documentation now use declarative resource-management paths.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
participant EtcdClient
participant AdminAPI
participant ConfigLoader
EtcdClient->>EtcdClient: write canonical resource document
AdminAPI->>EtcdClient: list/get resource
EtcdClient-->>AdminAPI: resource document
ConfigLoader->>EtcdClient: load canonical resource documents
EtcdClient-->>ConfigLoader: configuration data
Loading

Possibly related PRs

  • api7/aisix#792: Related through direct etcd seeding and readiness migration.
  • api7/aisix#800: Related through removal of Admin API writes and E2E harness changes.
  • api7/aisix#848: Related through A2A and MCP admin-handler changes.

Suggested reviewers:jarvis9443


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error, 1 warning)

Check nameStatusExplanationResolution
Security Check❌ ErrorCategory 1: admin GETs serialize ProviderKey.api_key, McpServer.secret, and A2aAgent.secret; PublicApiKey also returns key_hash without redaction.Return redacted DTOs for every secret-bearing resource, omit key_hash and credential fields, and replace backend-detail error responses with generic messages.
E2e Test Quality Review⚠️ WarningE2E quality issue: config-forward-compat-e2e.test.ts test 3 deletes yellowKeyId created only by test 1, with no ordering declaration, creating a hidden test dependency.Make each test self-contained by seeding and cleaning its own rows, or explicitly declare and document sequential execution; also keep route-matrix coverage at the process level if all aliases are contractual.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly and concisely identifies the main breaking change: removing the Admin API resource write path.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/remove-admin-write-path

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/aisix-admin/tests/etcd_integration.rs (1)

383-489: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Add the missing a2a_agents canonical document.

The PR retains eight resource kinds, but writes contains seven entries. It omits a2a_agents. Add a valid A2A agent document, assert stats.accepted == 8, and assert snap.a2a_agents.len() == 1.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/aisix-admin/tests/etcd_integration.rs` around lines 383 - 489, Extend
the writes array with a valid a2a_agents canonical document using the existing
seed flow, then update the accepted-entry assertion from 7 to 8. Add a
corresponding snap.a2a_agents length assertion expecting exactly one loaded
agent, leaving the other resource assertions unchanged.
🧹 Nitpick comments (1)
crates/aisix-admin/src/lib.rs (1)

1131-1198: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Read coverage now spans only two of eight resource kinds. Deleting the write handlers also deleted each handler module's test module. The replacement read tests in lib.rs seed only models and api_keys. The other six kinds — provider_keys, guardrails, cache_policies, observability_exporters, mcp_servers, a2a_agents — have 405 write-refusal coverage but no test proving that GET serves a seeded entry. These handler bodies are hand-written per module, not macro-generated, so a wrong store call or a wrong response shape in one of the six would pass CI.

  • crates/aisix-admin/src/lib.rs#L1131-L1198: extend build_seedable_state with seed helpers for the remaining six kinds, then add list and get-by-id assertions for each, mirroring list_models_returns_seeded_entries and get_model_serves_seeded_entry.
  • crates/aisix-admin/src/a2a_agents_handlers.rs#L12: add a test that list_a2a_agents and get_a2a_agent return a seeded A2aAgent, or confirm the new lib.rs tests cover this module.
  • crates/aisix-admin/src/mcp_servers_handlers.rs#L9: add the same list/get coverage for McpServer, or confirm the new lib.rs tests cover this module.

Note that InMemoryStore currently exposes only put_model and put_apikey as #[cfg(test)] helpers, so seeding the other six kinds requires adding matching helpers in crates/aisix-admin/src/store.rs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/aisix-admin/src/lib.rs` around lines 1131 - 1198, Extend
crates/aisix-admin/src/store.rs with cfg(test) put helpers for provider_keys,
guardrails, cache_policies, observability_exporters, mcp_servers, and
a2a_agents, then update build_seedable_state and the tests in
crates/aisix-admin/src/lib.rs#L1131-L1198 to seed each kind and assert both list
and get-by-id responses, mirroring the model tests. Ensure
crates/aisix-admin/src/a2a_agents_handlers.rs#L12 and
crates/aisix-admin/src/mcp_servers_handlers.rs#L9 are covered by these lib.rs
tests; no direct handler changes are required unless coverage cannot exercise
their list/get functions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/aisix-admin/src/lib.rs`:
- Around line 1200-1230: Remove the stale rotation-coverage comment immediately
before openapi_apikey_schema_excludes_max_budget_usd, and delete the empty
CachePolicy CRUD and Health endpoint section comments. Preserve the
guardrail_payload function, the ObservabilityExporter CRUD comment, and all
surrounding tests and formatting.
In `@crates/aisix-admin/tests/etcd_integration.rs`:
- Around line 317-354: Extend the resource-route assertions in the integration
test to issue both PUT and DELETE requests to /admin/v1/models, asserting each
returns METHOD_NOT_ALLOWED and includes GET in the Allow header. Keep the
existing POST check and final etcd emptiness assertion so all refused write
methods verify that no data is written.
In `@crates/aisix-server/src/main.rs`:
- Around line 898-902: Update the file-source match arm in the admin_store
initialization to bind the second tuple element as Some(_) instead of
Some(path), preserving the existing condition and FileManagedStore construction.
In `@tests/e2e/src/cases/file-resource-source-e2e.test.ts`:
- Around line 179-213: Update the rejected-write assertions in the e2e test to
require the exact Allow header value "GET" instead of merely containing GET,
covering both the authenticated POST/DELETE responses and the unauthenticated
POST response. Preserve the existing status and response-body assertions.
In `@tests/e2e/src/cases/openai-sdk-compat.test.ts`:
- Around line 54-84: After seeding the API key in the test setup, add an
independent readiness poll using the seeded caller credentials against
authenticated GET /v1/models, continuing until it returns 200. Remove the
client.chat.completions.create-based propagation gate and keep that call
exclusively for the SDK chat behavior under test.
---
Outside diff comments:
In `@crates/aisix-admin/tests/etcd_integration.rs`:
- Around line 383-489: Extend the writes array with a valid a2a_agents canonical
document using the existing seed flow, then update the accepted-entry assertion
from 7 to 8. Add a corresponding snap.a2a_agents length assertion expecting
exactly one loaded agent, leaving the other resource assertions unchanged.
---
Nitpick comments:
In `@crates/aisix-admin/src/lib.rs`:
- Around line 1131-1198: Extend crates/aisix-admin/src/store.rs with cfg(test)
put helpers for provider_keys, guardrails, cache_policies,
observability_exporters, mcp_servers, and a2a_agents, then update
build_seedable_state and the tests in crates/aisix-admin/src/lib.rs#L1131-L1198
to seed each kind and assert both list and get-by-id responses, mirroring the
model tests. Ensure crates/aisix-admin/src/a2a_agents_handlers.rs#L12 and
crates/aisix-admin/src/mcp_servers_handlers.rs#L9 are covered by these lib.rs
tests; no direct handler changes are required unless coverage cannot exercise
their list/get functions.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f3cea19a-3b55-4ad2-b83e-bc29037270c5

📥 Commits

Reviewing files that changed from the base of the PR and between b77f1a6 and 152c57d.

📒 Files selected for processing (26)
  • README.md
  • config.example.yaml
  • crates/aisix-admin/src/a2a_agents_handlers.rs
  • crates/aisix-admin/src/apikeys_handlers.rs
  • crates/aisix-admin/src/cache_policies_handlers.rs
  • crates/aisix-admin/src/error.rs
  • crates/aisix-admin/src/etcd_store.rs
  • crates/aisix-admin/src/file_store.rs
  • crates/aisix-admin/src/guardrails_handlers.rs
  • crates/aisix-admin/src/lib.rs
  • crates/aisix-admin/src/mcp_servers_handlers.rs
  • crates/aisix-admin/src/models_handlers.rs
  • crates/aisix-admin/src/observability_exporters_handlers.rs
  • crates/aisix-admin/src/openapi.rs
  • crates/aisix-admin/src/provider_keys_handlers.rs
  • crates/aisix-admin/src/state.rs
  • crates/aisix-admin/src/store.rs
  • crates/aisix-admin/tests/etcd_integration.rs
  • crates/aisix-server/src/main.rs
  • tests/e2e/src/cases/apikey-budget-e2e.test.ts
  • tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts
  • tests/e2e/src/cases/config-forward-compat-e2e.test.ts
  • tests/e2e/src/cases/file-resource-source-e2e.test.ts
  • tests/e2e/src/cases/openai-sdk-compat.test.ts
  • tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts
  • tests/e2e/src/harness/admin.ts
💤 Files with no reviewable changes (8)
  • tests/e2e/src/cases/apikey-budget-e2e.test.ts
  • tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts
  • crates/aisix-admin/src/state.rs
  • crates/aisix-admin/src/observability_exporters_handlers.rs
  • crates/aisix-admin/src/cache_policies_handlers.rs
  • crates/aisix-admin/src/models_handlers.rs
  • crates/aisix-admin/src/guardrails_handlers.rs
  • crates/aisix-admin/src/provider_keys_handlers.rs

Comment threadcrates/aisix-admin/src/lib.rs Outdated
Comment threadcrates/aisix-admin/tests/etcd_integration.rs Outdated
Comment threadcrates/aisix-server/src/main.rs
Comment on lines +179 to +213
expect(postRes.status).toBe(405);
expect(postRes.headers.get("allow")).toContain("GET");
await postRes.text();

// The refused write did not change the resource set.
const relist = await fetch(`${app.adminUrl}/admin/v1/models`, { headers: auth });
expect(((await relist.json()) as unknown[]).length).toBe(2);

// DELETE and rotate are covered by the same guard.
const delRes = await fetch(`${app.adminUrl}/admin/v1/models/any-id`, {
method: "DELETE",
headers: auth,
});
expect(delRes.status).toBe(409);
expect(delRes.status).toBe(405);
expect(delRes.headers.get("allow")).toContain("GET");
await delRes.text();

// The rotate route was removed outright (it was POST-only), so the
// path no longer exists at all.
const rotateRes = await fetch(`${app.adminUrl}/admin/v1/api_keys/any-id/rotate`, {
method: "POST",
headers: auth,
});
expect(rotateRes.status).toBe(409);
expect(rotateRes.status).toBe(404);
await rotateRes.text();

// Auth ordering: an UNAUTHENTICATED write still gets 401, and the
// 401 body must not leak the resources-file path (that detail is
// only for authenticated admins).
// Method routing answers before auth: an unauthenticated write gets
// the same 405 — there is no write endpoint left to protect, and
// the 405 body carries no resources-file detail to leak.
const unauthed = await fetch(`${app.adminUrl}/admin/v1/models`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ display_name: "nope" }),
});
expect(unauthed.status).toBe(401);
const unauthedBody = (await unauthed.json()) as { error_msg: string };
expect(unauthedBody.error_msg).not.toContain(app.resourcesPath!);
expect(unauthed.status).toBe(405);
expect((await unauthed.text())).not.toContain(app.resourcesPath!);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert the exact Allow header value.

Use toBe("GET") for each rejected write. Add the same assertion for the unauthenticated request. toContain("GET") also accepts an invalid value such as GET, POST.

Based on PR objectives, rejected resource writes must return 405 with Allow: GET.

Proposed test update
- expect(postRes.headers.get("allow")).toContain("GET");+ expect(postRes.headers.get("allow")).toBe("GET");
...
- expect(delRes.headers.get("allow")).toContain("GET");+ expect(delRes.headers.get("allow")).toBe("GET");
...
expect(unauthed.status).toBe(405);
+ expect(unauthed.headers.get("allow")).toBe("GET");
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
expect(postRes.status).toBe(405);
expect(postRes.headers.get("allow")).toContain("GET");
awaitpostRes.text();
// The refused write did not change the resource set.
constrelist=awaitfetch(`${app.adminUrl}/admin/v1/models`,{headers: auth});
expect(((awaitrelist.json())asunknown[]).length).toBe(2);
// DELETE and rotate are covered by the same guard.
constdelRes=awaitfetch(`${app.adminUrl}/admin/v1/models/any-id`,{
method: "DELETE",
headers: auth,
});
expect(delRes.status).toBe(409);
expect(delRes.status).toBe(405);
expect(delRes.headers.get("allow")).toContain("GET");
awaitdelRes.text();
// The rotate route was removed outright (it was POST-only), so the
// path no longer exists at all.
constrotateRes=awaitfetch(`${app.adminUrl}/admin/v1/api_keys/any-id/rotate`,{
method: "POST",
headers: auth,
});
expect(rotateRes.status).toBe(409);
expect(rotateRes.status).toBe(404);
awaitrotateRes.text();
// Auth ordering: an UNAUTHENTICATED write still gets 401, and the
// 401 body must not leak the resources-file path (that detail is
// only for authenticated admins).
// Method routing answers before auth: an unauthenticated write gets
// the same 405 — there is no write endpoint left to protect, and
// the 405 body carries no resources-file detail to leak.
constunauthed=awaitfetch(`${app.adminUrl}/admin/v1/models`,{
method: "POST",
headers: {"content-type": "application/json"},
body: JSON.stringify({display_name: "nope"}),
});
expect(unauthed.status).toBe(401);
constunauthedBody=(awaitunauthed.json())as{error_msg: string};
expect(unauthedBody.error_msg).not.toContain(app.resourcesPath!);
expect(unauthed.status).toBe(405);
expect((awaitunauthed.text())).not.toContain(app.resourcesPath!);
expect(postRes.status).toBe(405);
expect(postRes.headers.get("allow")).toBe("GET");
awaitpostRes.text();
// The refused write did not change the resource set.
constrelist=awaitfetch(`${app.adminUrl}/admin/v1/models`,{headers: auth});
expect(((awaitrelist.json())asunknown[]).length).toBe(2);
constdelRes=awaitfetch(`${app.adminUrl}/admin/v1/models/any-id`,{
method: "DELETE",
headers: auth,
});
expect(delRes.status).toBe(405);
expect(delRes.headers.get("allow")).toBe("GET");
awaitdelRes.text();
// The rotate route was removed outright (it was POST-only), so the
// path no longer exists at all.
constrotateRes=awaitfetch(`${app.adminUrl}/admin/v1/api_keys/any-id/rotate`,{
method: "POST",
headers: auth,
});
expect(rotateRes.status).toBe(404);
awaitrotateRes.text();
// Method routing answers before auth: an unauthenticated write gets
// the same 405 — there is no write endpoint left to protect, and
// the 405 body carries no resources-file detail to leak.
constunauthed=awaitfetch(`${app.adminUrl}/admin/v1/models`,{
method: "POST",
headers: {"content-type": "application/json"},
body: JSON.stringify({display_name: "nope"}),
});
expect(unauthed.status).toBe(405);
expect(unauthed.headers.get("allow")).toBe("GET");
expect((awaitunauthed.text())).not.toContain(app.resourcesPath!);
🧰 Tools
🪛 ast-grep (0.45.0)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { execFile } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/e2e/src/cases/file-resource-source-e2e.test.ts` around lines 179 - 213,
Update the rejected-write assertions in the e2e test to require the exact Allow
header value "GET" instead of merely containing GET, covering both the
authenticated POST/DELETE responses and the unauthenticated POST response.
Preserve the existing status and response-body assertions.

Comment threadtests/e2e/src/cases/openai-sdk-compat.test.ts

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Removes Admin API resource writes, leaving read-only resource endpoints and moving management to declarative file or etcd paths.

Changes:

  • Removes write handlers, routes, store operations, rotation, and deprecation middleware.
  • Updates OpenAPI, documentation, and tests for the read-only contract.
  • Migrates test setup to direct etcd seeding.

Reviewed changes

Copilot reviewed 25 out of 26 changed files in this pull request and generated 12 comments.

Show a summary per file
FileDescription
README.mdDocuments the read-only Admin API.
config.example.yamlClarifies declarative resource management.
crates/aisix-server/src/main.rsWires read-only admin stores.
crates/aisix-admin/src/lib.rsRemoves write routes and middleware.
crates/aisix-admin/src/openapi.rsRemoves write operations and schemas.
crates/aisix-admin/src/store.rsMakes ConfigStore read-only.
crates/aisix-admin/src/state.rsRemoves file-write guard state.
crates/aisix-admin/src/error.rsRemoves write-related errors.
crates/aisix-admin/src/file_store.rsRetains snapshot reads only.
crates/aisix-admin/src/etcd_store.rsRetains etcd reads only.
crates/aisix-admin/src/models_handlers.rsRemoves model writes.
crates/aisix-admin/src/apikeys_handlers.rsRemoves API-key writes and rotation.
crates/aisix-admin/src/provider_keys_handlers.rsRemoves provider-key writes.
crates/aisix-admin/src/guardrails_handlers.rsRemoves guardrail writes.
crates/aisix-admin/src/cache_policies_handlers.rsRemoves cache-policy writes.
crates/aisix-admin/src/observability_exporters_handlers.rsRemoves exporter writes.
crates/aisix-admin/src/mcp_servers_handlers.rsRemoves MCP-server writes.
crates/aisix-admin/src/a2a_agents_handlers.rsRemoves A2A-agent writes.
crates/aisix-admin/tests/etcd_integration.rsTests direct-etcd writes and admin reads.
tests/e2e/src/harness/admin.tsRemoves Admin API write helpers.
tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.tsRemoves obsolete path comparison tests.
tests/e2e/src/cases/openai-sdk-compat.test.tsMigrates setup to etcd seeding.
tests/e2e/src/cases/file-resource-source-e2e.test.tsTests file-mode read-only behavior.
tests/e2e/src/cases/config-forward-compat-e2e.test.tsRemoves Admin write validation coverage.
tests/e2e/src/cases/apikey-lifecycle-e2e.test.tsReplaces rotation with declarative secret swapping.
tests/e2e/src/cases/apikey-budget-e2e.test.tsRemoves obsolete write-path validation test.
Suppressed comments (1)

crates/aisix-admin/src/mcp_servers_handlers.rs:9

  • This removal also eliminates the only production call to aisix_mcp::validate_spec. The file and etcd loaders only run the core JSON Schema, which does not reject specs with zero generatable operations or colliding sanitized tool names, so the newly exclusive declarative paths accept configurations the former write API rejected. Add the semantic validation to both loaders before removing this path.
use aisix_core::McpServer;

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment threadcrates/aisix-server/src/main.rs Outdated
use serde::{Deserialize, Serialize};
use serde_json::Value;
use uuid::Uuid;
use serde::Serialize;
Comment threadcrates/aisix-server/src/main.rs Outdated
Comment on lines 9 to 10
//! replaces the `key` field with a freshly-generated `sk-*` value and
//! bumps the revision, invalidating the old credential.
Comment on lines 8 to 9
//!
//! ids are UUID v4s generated on POST; PUT preserves the existing id.
@@ -38,7 +38,7 @@ const OPENAPI_JSON_BASE: &str = r##"{
"info": {
"title": "AISIX Admin API",
"version": "dev",
"description": "The AISIX Admin API configures an open-source AISIX gateway at runtime. Use it when you operate the gateway directly and need to create or update models, caller API keys, provider credentials, guardrails, cache policies, and observability exporters.\n\nThe write endpoints (POST, PUT, DELETE) are deprecated in favor of declarative configuration: load resources from a `resources_file` (`resources.yaml`) or write them to etcd directly. Write endpoints remain functional, and every mutating response carries a `Deprecation` header (RFC 9745) plus a `Link` header with `rel=\"deprecation\"` pointing at the migration documentation. Read endpoints are not deprecated.\n\nGateways connected to AISIX Cloud do not expose this listener. Configure them through AISIX Cloud."
"description": "The AISIX Admin API is the read-only operational surface of an open-source AISIX gateway: list and inspect the loaded models, caller API keys, provider credentials, guardrails, MCP servers, A2A agents, cache policies, and observability exporters, check per-model upstream health, and drive the playground.\n\nResource write endpoints were removed in favor of declarative configuration: declare resources in a `resources_file` (`resources.yaml`) and reload with SIGHUP, or write them to etcd directly. See the resources file reference at https://docs.api7.ai/ai-gateway/reference/resources-file.\n\nGateways connected to AISIX Cloud do not expose this listener. Configure them through AISIX Cloud."
Comment on lines 4 to 5
//! validate against the JSON schema, reject duplicate names (409),
//! generate a uuid v4 on POST, bump revision on PUT.
Comment on lines 4 to 5
//! validate against the JSON schema, reject duplicate names (409),
//! generate a uuid v4 on POST, bump revision on PUT.
Comment on lines 5 to 6
//! PUT. Additionally rejects a name containing the reserved tool-namespace
//! separator `__`, since the name prefixes the server's tools.
Comment on lines 8 to 9
//! every configuration path rejects an incomplete credential set; the checks
//! below are defense in depth.
@moonmingmoonming self-assigned this Aug 10, 2026
@moonming

Copy link
Copy Markdown
CollaboratorAuthor

Review triage — every inline comment dispositioned; fixes landed in f9c10cc and e34694b.

Fixed

  • 7 handler module docs rewritten to the read-only contract; stale rotate/CRUD section comments removed (e34694b)
  • OpenAPI Caller API Keys tag no longer says "and key rotation"; path/Entry descriptions no longer claim ids are generated by the Admin API or that update/rotate bumps revisions (f9c10cc, e34694b)
  • main.rs: unused Some(path) binding → Some(_); canonical product name; removed a comment referencing write rejections (e34694b)
  • etcd integration: refused-writes test now covers PUT and DELETE with Allow assertions, and the rotate-absence checks GET the rotate URIs too — POST-only 404 couldn't distinguish a deleted route from the old handler's unknown-id 404 (f9c10cc, e34694b)
  • sdk-compat e2e: readiness gate switched from the SDK chat path to an independent authenticated GET /v1/models probe per the harness gate rules (e34694b)
  • config.example.yaml: dead docs/api-admin.md link and Admin-API management claims replaced with the declarative sources (f9c10cc)

Not adopted, with reasons

  • Assert Allow with toBe("GET"): the real binary answers Allow: GET,HEAD (axum's MethodRouter advertises HEAD alongside GET), so an exact "GET" match fails against actual behavior. toContain("GET") plus the 405 status is the contract; the 405 itself already proves no write method is routed.
  • Duplicate key_hash uniqueness on the direct-etcd path (assert_unique_key removal): real gap, but pre-existing — the admin-side check never guarded direct etcd writes, which existed before this PR and are used by the control plane (which enforces uniqueness org-side) and the file source (which has its own per-kind identity check). Filed internally for loader-side conflict detection with /status/config visibility; tracked as a follow-up rather than blocking the removal.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts (1)

276-301: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Use an independent readiness check for key setup.

seedKey gates propagation with a POST /v1/chat/completions request. The rotation and deletion flows also use the chat authorization path for their assertions. A chat failure can stop the test before it checks the key transition.

Seed the caller keys, then verify readiness with GET /v1/models and require 200. Keep chat requests for the actual rotation and revocation assertions.

As per coding guidelines, E2E readiness gates must use an independent condition, and caller API keys must be checked with GET /v1/models returning 200.

Also applies to: 311-312

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts` around lines 276 - 301,
Update the key setup readiness flow around seedKey and the related
rotation/deletion cases to use an independent GET /v1/models request requiring
HTTP 200, rather than POST /v1/chat/completions. Keep chat requests in the
secret-swap and revocation assertions only, so readiness failures cannot mask
key-transition checks.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/aisix-core/src/bin/dump-schema.rs`:
- Around line 44-47: Update the comment near the STRICT shape documentation to
qualify unknown-field rejection as applying only where the resource schema is
closed. Preserve the existing distinction between declarative write contracts
and lenient etcd reads, while acknowledging resource-specific exceptions such as
open fields and custom guardrail validation documented in the schema guidance.
In `@tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts`:
- Around line 304-307: Update the combined setup guard in the affected E2E test
to also check that otlp is available, skipping and returning when any shared
setup value—including etcdReachable, app, seed, or otlp—is missing.
---
Outside diff comments:
In `@tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts`:
- Around line 276-301: Update the key setup readiness flow around seedKey and
the related rotation/deletion cases to use an independent GET /v1/models request
requiring HTTP 200, rather than POST /v1/chat/completions. Keep chat requests in
the secret-swap and revocation assertions only, so readiness failures cannot
mask key-transition checks.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: aa61ea23-b8c7-4503-b1c7-e50f6378ee7d

📥 Commits

Reviewing files that changed from the base of the PR and between 152c57d and f9c10cc.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (12)
  • README.md
  • config.example.yaml
  • crates/aisix-admin/Cargo.toml
  • crates/aisix-admin/src/apikeys_handlers.rs
  • crates/aisix-admin/src/error.rs
  • crates/aisix-admin/src/lib.rs
  • crates/aisix-admin/src/openapi.rs
  • crates/aisix-admin/tests/etcd_integration.rs
  • crates/aisix-core/src/bin/dump-schema.rs
  • crates/aisix-etcd/src/provider.rs
  • schemas/README.md
  • tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts
💤 Files with no reviewable changes (2)
  • crates/aisix-admin/Cargo.toml
  • crates/aisix-admin/src/error.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • config.example.yaml
  • README.md
  • crates/aisix-admin/src/apikeys_handlers.rs
  • crates/aisix-admin/src/lib.rs

Comment threadcrates/aisix-core/src/bin/dump-schema.rs
Comment on lines +304 to 307
if (!etcdReachable || !app || !seed) {
ctx.skip();
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Include otlp in the setup guard.

The guard omits otlp. Add it to the combined setup check so the test does not run with incomplete shared setup.

Based on learnings, E2E cases must preserve if (!etcdReachable || !app || !seed || !otlp) { ctx.skip(); return; }.

Proposed fix
- if (!etcdReachable || !app || !seed) {+ if (!etcdReachable || !app || !seed || !otlp) {
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if(!etcdReachable||!app||!seed){
ctx.skip();
return;
}
if(!etcdReachable||!app||!seed||!otlp){
ctx.skip();
return;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts` around lines 304 - 307,
Update the combined setup guard in the affected E2E test to also check that otlp
is available, skipping and returning when any shared setup value—including
etcdReachable, app, seed, or otlp—is missing.

Source: Learnings

The admin listener keeps its read surface (lists/gets for all 8
resource kinds incl. the former apikeys spelling, models/status,
health, OpenAPI + Scalar, playground, livez/readyz); resources are
managed exclusively through the declarative paths — resources_file
(SIGHUP reload) or direct etcd writes.
BREAKING CHANGE: POST/PUT/DELETE on /admin/v1/* answer 405 with
Allow: GET (409 file-managed rejection included); the api-key rotate
route is gone (404) — rotate declaratively by writing the same
resource id with a new key_hash. The published OpenAPI documents no
write operations; write-only component schemas (ApiKeyRequest,
ApiKeyEntry, ApiKeyRotateResponse, DeleteResponse) are removed.
- router: every /admin/v1/* resource route serves get() only; rotate
routes, file-managed write guard, and RFC 9745 deprecation-header
middleware deleted; 43 write/rotate/uniqueness handler fns removed
- store: ConfigStore is read-only (16 put_*/delete_* methods and
StoreError::ReadOnly removed); EtcdConfigStore keeps reads only;
FileManagedStore::new(snapshot) drops the path param; InMemoryStore
keeps #[cfg(test)] inherent writes for unit-test seeding
- openapi: 24 write ops + rotate path removed from the base document;
no-op deprecation-marker pass deleted; unreachable component
schemas pruned; new gate pins GET-only /admin/v1/* and zero
deprecated marks
- tests: write-path suites deleted; read tests seed via InMemoryStore;
new 405/Allow + rotate-404 contract tests; etcd integration tests
seed via direct etcd writes (the declarative front door) and pin
that refused writes never touch etcd
- e2e: AdminClient write helpers removed (SeedClient is the write
front door); file-resource-source pins the new read-only contract;
apikey-lifecycle rotate coverage became a declarative secret-swap
test; obsolete write-path cases deleted
- docs: README, config.example.yaml, crate module docs updated
Second-auditor findings on the removal PR, all test/doc-level (no
runtime changes):
- rotate-404 tests now GET the rotate URIs too — POST-only 404 could
not distinguish a deleted route from the old handler's unknown-id
404; GET answers 405 on a surviving POST-only route
- e2e: deleting a key's etcd entry revokes an in-use bearer
(fail-closed, unknown-token 401, other keys unaffected) — the
deletion branch had lost its only end-to-end proof
- etcd integration: a2a_agents round-trip + loader coverage (7 -> 8
kinds)
- apikeys read test pins the full PublicApiKey projection
(allowed_tools/disabled/expires_at), not just the id
- OpenAPI descriptions stop claiming ids are generated by the Admin
API and revisions increment on update/rotate
- stale write-path narrative removed: config.example.yaml (dead
docs/api-admin.md link), schemas/README, dump-schema comment,
apikeys_handlers module doc, aisix-etcd provider doc, README RBAC
row + e2e counts
- dead code: AdminError::{BadRequest,Conflict,Schema} variants and
the aisix-mcp/uuid dependencies left over from the write path
Per-comment review triage:
- 7 handler module docs rewritten from CRUD-era text to the surviving
read-only contract; stale rotate/CRUD section comments in the
aisix-admin test module removed
- OpenAPI 'Caller API Keys' tag no longer advertises key rotation
- main.rs: unused match binding -> Some(_); canonical product name;
dropped a comment referencing the removed write-rejection path
- etcd integration: refused-writes test now covers PUT and DELETE
(405 + Allow), not just POST
- sdk-compat e2e: readiness gate switched from the SDK chat path (the
behavior under test) to an independent authenticated GET /v1/models
probe, per the harness gate rules
Cold-audit closeout:
- removed_resource_writes_answer_405_with_allow_get now generates the
FULL matrix (9 route spellings x POST/PUT/DELETE) instead of a
sampled subset — a partial revert (e.g. PUT re-added on one {id}
route) previously passed the whole suite
- last stale write-path narrative: store.rs module doc (read-only
trait), aisix-core schema.rs/models docs (declarative writers, not
'Admin API ... 400'), e2e smoke/seed/app/forward-compat headers no
longer cite deleted characterization or held-back write cases,
openapi.rs base-doc comment names a surviving schema
Round-two auditor findings:
- OpenAPI: the two remaining Entry revision descriptions (McpServer,
A2aAgent) stop describing create/update lifecycle; a regression
assertion now rejects write-lifecycle prose on any documented
revision field
- schemas/README + schema.rs + dump-schema: scope the strict-contract
claim to the in-repo writers (aisix validate, file source) — the
control plane validates its own API schema, a raw direct etcd put
gets no synchronous validation (lenient read only), and
unknown-field rejection applies only where a resource closes fields;
the previous rewrite overclaimed all three
- deletion-revocation e2e: propagation barrier is now a fresh key
seeded after the delete (later etcd revision), so the revocation
assertion is a real assertion instead of a gate poll; a regression
fails the assert, not a 30s timeout
- apikeys projection test seeds rate_limit + allowed_agents and pins
the list entry's projection too; module doc describes PublicApiKey
as an explicit allowlist (not 'minus nothing')
- lifecycle prose: secret-swap invalidates 'as soon as the write
propagates' (not 'immediately'); README e2e counts scoped to
scenario files (183/496)
@moonming
moonmingforce-pushed the feat/remove-admin-write-path branch from 74b0db5 to f43eab2CompareAugust 11, 2026 01:59
@moonming
moonming merged commit b61d270 into mainAug 11, 2026
30 of 32 checks passed
@moonming
moonming deleted the feat/remove-admin-write-path branch August 11, 2026 02:20
Sign up for freeto 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.

2 participants

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' feat(admin)!: remove the Admin API resource write path by moonming · Pull Request #915 · api7/aisix · GitHub
Skip to content

feat(admin)!: remove the Admin API resource write path - #915

Merged
moonming merged 5 commits into
mainfrom
feat/remove-admin-write-path
Aug 11, 2026
Merged

feat(admin)!: remove the Admin API resource write path#915
moonming merged 5 commits into
mainfrom
feat/remove-admin-write-path

Conversation

@moonming

@moonmingmoonming commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Removes the Admin API resource write path. The admin listener (:3001) keeps its read surface — lists/gets for all 8 resource kinds (including the former apikeys spelling), /admin/v1/models/status, /admin/v1/health, OpenAPI + Scalar UI, playground, /livez, /readyz — but resources are now managed exclusively through the declarative paths: a resources_file (resources.yaml, reloaded on SIGHUP) or direct etcd writes.

This is the final step of the deprecation announced in v0.4.0 (RFC 9745 Deprecation headers) and executes the removal scheduled for v0.5.0+ after #848 lifted the write-path-exclusive validations into the canonical schemas so the declarative paths enforce them.

⚠️ Breaking changes

BeforeAfter
POST /admin/v1/<kind>, PUT/DELETE /admin/v1/<kind>/{id} (deprecated, functional)405 with Allow: GET
POST /admin/v1/api_keys/{id}/rotate (and apikeys spelling)404 — the route is gone
File mode: writes rejected with 409 naming the resources file405, same as every other mode
Rotate returned a fresh plaintext keyNo DP-side plaintext rotation. Rotate declaratively: write the same resource id with a new key_hash — the old plaintext stops authenticating as soon as the write propagates (pinned in apikey-lifecycle-e2e)

What to update:

  • Scripts that created/updated/deleted resources via :3001 → write resources.yaml (validate offline with aisix validate --resources <file>, reload with SIGHUP) or write entity-value JSON to etcd at {prefix}/{kind}/{id}.
  • Scripts that rotated caller keys via /rotate → hash the new secret client-side and update the resource's key_hash.
  • The published OpenAPI (/admin/openapi.json) no longer documents write operations; the write-only component schemas (ApiKeyRequest, ApiKeyEntry, ApiKeyRotateResponse, DeleteResponse) are gone from components.schemas.

What changed

Router / handlers — every /admin/v1/* resource route serves get(...) only; both rotate routes deleted; the file-managed write guard and the RFC 9745 deprecation-header middleware deleted (nothing left to mark). 43 write/rotate/uniqueness handler fns removed across the 8 handler modules.

Store layerConfigStore is now a read-only trait (16 put_*/delete_* methods removed, StoreError::ReadOnly gone). EtcdConfigStore keeps only reads (module doc rewritten: resources reach etcd through the declarative paths). FileManagedStore::new(snapshot) drops the path parameter — read-only by construction. InMemoryStore keeps #[cfg(test)] inherent write methods used by unit-test seeding.

OpenAPI — 24 write operations and the rotate path removed from the base document; the no-op write-deprecation marker pass deleted; unreachable component schemas pruned via a reachability walk from paths. New gate test pins that the published reference documents zero non-GET operations under /admin/v1/ and zero deprecated marks anywhere.

Tests — write-path unit tests (CRUD flows, rotate atomicity, write-auth, write-validation) deleted; read tests re-seeded through InMemoryStore; new contract tests pin 405 + Allow: GET on every collection/:id route (auth'd and unauthenticated) and 404 on both rotate spellings. The etcd integration test now seeds via direct etcd_client puts — the path operators actually use.

e2eAdminClient write helpers removed (reads stay; SeedClient is the write front door). file-resource-source-e2e pins the new file-mode contract (reads serve the file, writes 405, rotate 404, unauthenticated write 405 with no file-path leak). apikey-lifecycle-e2e's rotate coverage became a declarative secret-swap test (old plaintext dies immediately, id unchanged). Deleted: seed-vs-admin-characterization-e2e (its own comment scheduled retirement once the seed migration completed), apikey-budget-e2e (tested write-path 400s), the sdk-compat deprecation-header test, and forward-compat's admin strict-write test.

Docs — README, config.example.yaml, crate module docs updated to the read-only story.

Non-goals / follow-ups (filed internally)

  • 3 OpenAPI-parsing validations (mcp validate_spec, HeaderName typing, duplicate tool names) still lack declarative-path enforcement — blocked on a dependency-direction extraction, tracked internally.
  • etcd watch keeps last-good on an invalid PUT while resync drops the row — policy decision tracked internally.

Verification

  • cargo fmt --check, cargo clippy --workspace --all-targets -- -D warnings, cargo test --workspace — all green.
  • Real binary, file mode: boots with resources.yaml, GET list serves the file, POST /admin/v1/models → 405 + Allow: GET, rotate → 404, /admin/openapi.json documents no admin writes.
  • Local e2e stack run before push.

Summary by CodeRabbit

  • Changes
    • The Admin API is now read-only for managed resources.
    • Resource listing and detail views remain available through GET requests.
    • Creation, updates, deletion, and API-key rotation through the Admin API are no longer supported.
    • Unsupported write requests return 405 responses, while rotation routes return 404.
    • Manage resources through resources_file reloads or direct etcd writes.
    • Updated configuration, validation guidance, and end-to-end coverage to reflect the read-only administration model.

CopilotAI balanced review requested due to automatic review settings August 10, 2026 05:09
@coderabbitai

coderabbitaiBot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9acb9ae1-7d4d-42ce-83f0-61de46c03b1a

📥 Commits

Reviewing files that changed from the base of the PR and between f9c10cc and f43eab2.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (25)
  • README.md
  • config.example.yaml
  • crates/aisix-admin/src/a2a_agents_handlers.rs
  • crates/aisix-admin/src/apikeys_handlers.rs
  • crates/aisix-admin/src/cache_policies_handlers.rs
  • crates/aisix-admin/src/guardrails_handlers.rs
  • crates/aisix-admin/src/lib.rs
  • crates/aisix-admin/src/mcp_servers_handlers.rs
  • crates/aisix-admin/src/models_handlers.rs
  • crates/aisix-admin/src/observability_exporters_handlers.rs
  • crates/aisix-admin/src/openapi.rs
  • crates/aisix-admin/src/provider_keys_handlers.rs
  • crates/aisix-admin/src/store.rs
  • crates/aisix-admin/tests/etcd_integration.rs
  • crates/aisix-core/src/bin/dump-schema.rs
  • crates/aisix-core/src/models/mod.rs
  • crates/aisix-core/src/models/schema.rs
  • crates/aisix-server/src/main.rs
  • schemas/README.md
  • tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts
  • tests/e2e/src/cases/config-forward-compat-e2e.test.ts
  • tests/e2e/src/cases/openai-sdk-compat.test.ts
  • tests/e2e/src/cases/smoke.test.ts
  • tests/e2e/src/harness/app.ts
  • tests/e2e/src/harness/seed.ts
💤 Files with no reviewable changes (1)
  • tests/e2e/src/cases/smoke.test.ts
🚧 Files skipped from review as they are similar to previous changes (12)
  • crates/aisix-server/src/main.rs
  • config.example.yaml
  • crates/aisix-admin/src/provider_keys_handlers.rs
  • crates/aisix-admin/src/models_handlers.rs
  • schemas/README.md
  • crates/aisix-core/src/bin/dump-schema.rs
  • tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts
  • tests/e2e/src/cases/config-forward-compat-e2e.test.ts
  • crates/aisix-admin/src/apikeys_handlers.rs
  • README.md
  • crates/aisix-admin/tests/etcd_integration.rs
  • crates/aisix-admin/src/lib.rs

📝 Walkthrough

Walkthrough

The admin listener now exposes read-only resource routes. Resource writes use resources_file reloads, direct etcd writes, or declarative seed helpers. Stores, server wiring, tests, and documentation were updated.

Changes

Read-only admin resource surface

Layer / File(s)Summary
Read-only store contracts
crates/aisix-admin/src/store.rs, crates/aisix-admin/src/etcd_store.rs, crates/aisix-admin/src/file_store.rs, crates/aisix-admin/src/error.rs, crates/aisix-admin/src/state.rs
Store traits and implementations now support resource reads only.
GET-only admin routes
crates/aisix-admin/src/lib.rs, crates/aisix-admin/src/*_handlers.rs
Create, update, delete, and API-key rotation routes were removed.
Read-only server wiring
crates/aisix-server/src/main.rs
The server constructs read-only file-backed admin state and removes the file-managed write guard.
Direct etcd setup and integration coverage
crates/aisix-admin/tests/etcd_integration.rs, crates/aisix-admin/src/etcd_store.rs
Tests seed canonical documents directly in etcd and verify read-only behavior.
Declarative E2E flows and documentation
tests/e2e/src/cases/*, tests/e2e/src/harness/*, README.md, config.example.yaml, schemas/README.md
E2E setup and documentation now use declarative resource-management paths.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
participant EtcdClient
participant AdminAPI
participant ConfigLoader
EtcdClient->>EtcdClient: write canonical resource document
AdminAPI->>EtcdClient: list/get resource
EtcdClient-->>AdminAPI: resource document
ConfigLoader->>EtcdClient: load canonical resource documents
EtcdClient-->>ConfigLoader: configuration data
Loading

Possibly related PRs

  • api7/aisix#792: Related through direct etcd seeding and readiness migration.
  • api7/aisix#800: Related through removal of Admin API writes and E2E harness changes.
  • api7/aisix#848: Related through A2A and MCP admin-handler changes.

Suggested reviewers:jarvis9443


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error, 1 warning)

Check nameStatusExplanationResolution
Security Check❌ ErrorCategory 1: admin GETs serialize ProviderKey.api_key, McpServer.secret, and A2aAgent.secret; PublicApiKey also returns key_hash without redaction.Return redacted DTOs for every secret-bearing resource, omit key_hash and credential fields, and replace backend-detail error responses with generic messages.
E2e Test Quality Review⚠️ WarningE2E quality issue: config-forward-compat-e2e.test.ts test 3 deletes yellowKeyId created only by test 1, with no ordering declaration, creating a hidden test dependency.Make each test self-contained by seeding and cleaning its own rows, or explicitly declare and document sequential execution; also keep route-matrix coverage at the process level if all aliases are contractual.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly and concisely identifies the main breaking change: removing the Admin API resource write path.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/remove-admin-write-path

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/aisix-admin/tests/etcd_integration.rs (1)

383-489: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Add the missing a2a_agents canonical document.

The PR retains eight resource kinds, but writes contains seven entries. It omits a2a_agents. Add a valid A2A agent document, assert stats.accepted == 8, and assert snap.a2a_agents.len() == 1.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/aisix-admin/tests/etcd_integration.rs` around lines 383 - 489, Extend
the writes array with a valid a2a_agents canonical document using the existing
seed flow, then update the accepted-entry assertion from 7 to 8. Add a
corresponding snap.a2a_agents length assertion expecting exactly one loaded
agent, leaving the other resource assertions unchanged.
🧹 Nitpick comments (1)
crates/aisix-admin/src/lib.rs (1)

1131-1198: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Read coverage now spans only two of eight resource kinds. Deleting the write handlers also deleted each handler module's test module. The replacement read tests in lib.rs seed only models and api_keys. The other six kinds — provider_keys, guardrails, cache_policies, observability_exporters, mcp_servers, a2a_agents — have 405 write-refusal coverage but no test proving that GET serves a seeded entry. These handler bodies are hand-written per module, not macro-generated, so a wrong store call or a wrong response shape in one of the six would pass CI.

  • crates/aisix-admin/src/lib.rs#L1131-L1198: extend build_seedable_state with seed helpers for the remaining six kinds, then add list and get-by-id assertions for each, mirroring list_models_returns_seeded_entries and get_model_serves_seeded_entry.
  • crates/aisix-admin/src/a2a_agents_handlers.rs#L12: add a test that list_a2a_agents and get_a2a_agent return a seeded A2aAgent, or confirm the new lib.rs tests cover this module.
  • crates/aisix-admin/src/mcp_servers_handlers.rs#L9: add the same list/get coverage for McpServer, or confirm the new lib.rs tests cover this module.

Note that InMemoryStore currently exposes only put_model and put_apikey as #[cfg(test)] helpers, so seeding the other six kinds requires adding matching helpers in crates/aisix-admin/src/store.rs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/aisix-admin/src/lib.rs` around lines 1131 - 1198, Extend
crates/aisix-admin/src/store.rs with cfg(test) put helpers for provider_keys,
guardrails, cache_policies, observability_exporters, mcp_servers, and
a2a_agents, then update build_seedable_state and the tests in
crates/aisix-admin/src/lib.rs#L1131-L1198 to seed each kind and assert both list
and get-by-id responses, mirroring the model tests. Ensure
crates/aisix-admin/src/a2a_agents_handlers.rs#L12 and
crates/aisix-admin/src/mcp_servers_handlers.rs#L9 are covered by these lib.rs
tests; no direct handler changes are required unless coverage cannot exercise
their list/get functions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/aisix-admin/src/lib.rs`:
- Around line 1200-1230: Remove the stale rotation-coverage comment immediately
before openapi_apikey_schema_excludes_max_budget_usd, and delete the empty
CachePolicy CRUD and Health endpoint section comments. Preserve the
guardrail_payload function, the ObservabilityExporter CRUD comment, and all
surrounding tests and formatting.
In `@crates/aisix-admin/tests/etcd_integration.rs`:
- Around line 317-354: Extend the resource-route assertions in the integration
test to issue both PUT and DELETE requests to /admin/v1/models, asserting each
returns METHOD_NOT_ALLOWED and includes GET in the Allow header. Keep the
existing POST check and final etcd emptiness assertion so all refused write
methods verify that no data is written.
In `@crates/aisix-server/src/main.rs`:
- Around line 898-902: Update the file-source match arm in the admin_store
initialization to bind the second tuple element as Some(_) instead of
Some(path), preserving the existing condition and FileManagedStore construction.
In `@tests/e2e/src/cases/file-resource-source-e2e.test.ts`:
- Around line 179-213: Update the rejected-write assertions in the e2e test to
require the exact Allow header value "GET" instead of merely containing GET,
covering both the authenticated POST/DELETE responses and the unauthenticated
POST response. Preserve the existing status and response-body assertions.
In `@tests/e2e/src/cases/openai-sdk-compat.test.ts`:
- Around line 54-84: After seeding the API key in the test setup, add an
independent readiness poll using the seeded caller credentials against
authenticated GET /v1/models, continuing until it returns 200. Remove the
client.chat.completions.create-based propagation gate and keep that call
exclusively for the SDK chat behavior under test.
---
Outside diff comments:
In `@crates/aisix-admin/tests/etcd_integration.rs`:
- Around line 383-489: Extend the writes array with a valid a2a_agents canonical
document using the existing seed flow, then update the accepted-entry assertion
from 7 to 8. Add a corresponding snap.a2a_agents length assertion expecting
exactly one loaded agent, leaving the other resource assertions unchanged.
---
Nitpick comments:
In `@crates/aisix-admin/src/lib.rs`:
- Around line 1131-1198: Extend crates/aisix-admin/src/store.rs with cfg(test)
put helpers for provider_keys, guardrails, cache_policies,
observability_exporters, mcp_servers, and a2a_agents, then update
build_seedable_state and the tests in crates/aisix-admin/src/lib.rs#L1131-L1198
to seed each kind and assert both list and get-by-id responses, mirroring the
model tests. Ensure crates/aisix-admin/src/a2a_agents_handlers.rs#L12 and
crates/aisix-admin/src/mcp_servers_handlers.rs#L9 are covered by these lib.rs
tests; no direct handler changes are required unless coverage cannot exercise
their list/get functions.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f3cea19a-3b55-4ad2-b83e-bc29037270c5

📥 Commits

Reviewing files that changed from the base of the PR and between b77f1a6 and 152c57d.

📒 Files selected for processing (26)
  • README.md
  • config.example.yaml
  • crates/aisix-admin/src/a2a_agents_handlers.rs
  • crates/aisix-admin/src/apikeys_handlers.rs
  • crates/aisix-admin/src/cache_policies_handlers.rs
  • crates/aisix-admin/src/error.rs
  • crates/aisix-admin/src/etcd_store.rs
  • crates/aisix-admin/src/file_store.rs
  • crates/aisix-admin/src/guardrails_handlers.rs
  • crates/aisix-admin/src/lib.rs
  • crates/aisix-admin/src/mcp_servers_handlers.rs
  • crates/aisix-admin/src/models_handlers.rs
  • crates/aisix-admin/src/observability_exporters_handlers.rs
  • crates/aisix-admin/src/openapi.rs
  • crates/aisix-admin/src/provider_keys_handlers.rs
  • crates/aisix-admin/src/state.rs
  • crates/aisix-admin/src/store.rs
  • crates/aisix-admin/tests/etcd_integration.rs
  • crates/aisix-server/src/main.rs
  • tests/e2e/src/cases/apikey-budget-e2e.test.ts
  • tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts
  • tests/e2e/src/cases/config-forward-compat-e2e.test.ts
  • tests/e2e/src/cases/file-resource-source-e2e.test.ts
  • tests/e2e/src/cases/openai-sdk-compat.test.ts
  • tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts
  • tests/e2e/src/harness/admin.ts
💤 Files with no reviewable changes (8)
  • tests/e2e/src/cases/apikey-budget-e2e.test.ts
  • tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts
  • crates/aisix-admin/src/state.rs
  • crates/aisix-admin/src/observability_exporters_handlers.rs
  • crates/aisix-admin/src/cache_policies_handlers.rs
  • crates/aisix-admin/src/models_handlers.rs
  • crates/aisix-admin/src/guardrails_handlers.rs
  • crates/aisix-admin/src/provider_keys_handlers.rs

Comment threadcrates/aisix-admin/src/lib.rs Outdated
Comment threadcrates/aisix-admin/tests/etcd_integration.rs Outdated
Comment threadcrates/aisix-server/src/main.rs
Comment on lines +179 to +213
expect(postRes.status).toBe(405);
expect(postRes.headers.get("allow")).toContain("GET");
await postRes.text();

// The refused write did not change the resource set.
const relist = await fetch(`${app.adminUrl}/admin/v1/models`, { headers: auth });
expect(((await relist.json()) as unknown[]).length).toBe(2);

// DELETE and rotate are covered by the same guard.
const delRes = await fetch(`${app.adminUrl}/admin/v1/models/any-id`, {
method: "DELETE",
headers: auth,
});
expect(delRes.status).toBe(409);
expect(delRes.status).toBe(405);
expect(delRes.headers.get("allow")).toContain("GET");
await delRes.text();

// The rotate route was removed outright (it was POST-only), so the
// path no longer exists at all.
const rotateRes = await fetch(`${app.adminUrl}/admin/v1/api_keys/any-id/rotate`, {
method: "POST",
headers: auth,
});
expect(rotateRes.status).toBe(409);
expect(rotateRes.status).toBe(404);
await rotateRes.text();

// Auth ordering: an UNAUTHENTICATED write still gets 401, and the
// 401 body must not leak the resources-file path (that detail is
// only for authenticated admins).
// Method routing answers before auth: an unauthenticated write gets
// the same 405 — there is no write endpoint left to protect, and
// the 405 body carries no resources-file detail to leak.
const unauthed = await fetch(`${app.adminUrl}/admin/v1/models`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ display_name: "nope" }),
});
expect(unauthed.status).toBe(401);
const unauthedBody = (await unauthed.json()) as { error_msg: string };
expect(unauthedBody.error_msg).not.toContain(app.resourcesPath!);
expect(unauthed.status).toBe(405);
expect((await unauthed.text())).not.toContain(app.resourcesPath!);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert the exact Allow header value.

Use toBe("GET") for each rejected write. Add the same assertion for the unauthenticated request. toContain("GET") also accepts an invalid value such as GET, POST.

Based on PR objectives, rejected resource writes must return 405 with Allow: GET.

Proposed test update
- expect(postRes.headers.get("allow")).toContain("GET");+ expect(postRes.headers.get("allow")).toBe("GET");
...
- expect(delRes.headers.get("allow")).toContain("GET");+ expect(delRes.headers.get("allow")).toBe("GET");
...
expect(unauthed.status).toBe(405);
+ expect(unauthed.headers.get("allow")).toBe("GET");
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
expect(postRes.status).toBe(405);
expect(postRes.headers.get("allow")).toContain("GET");
awaitpostRes.text();
// The refused write did not change the resource set.
constrelist=awaitfetch(`${app.adminUrl}/admin/v1/models`,{headers: auth});
expect(((awaitrelist.json())asunknown[]).length).toBe(2);
// DELETE and rotate are covered by the same guard.
constdelRes=awaitfetch(`${app.adminUrl}/admin/v1/models/any-id`,{
method: "DELETE",
headers: auth,
});
expect(delRes.status).toBe(409);
expect(delRes.status).toBe(405);
expect(delRes.headers.get("allow")).toContain("GET");
awaitdelRes.text();
// The rotate route was removed outright (it was POST-only), so the
// path no longer exists at all.
constrotateRes=awaitfetch(`${app.adminUrl}/admin/v1/api_keys/any-id/rotate`,{
method: "POST",
headers: auth,
});
expect(rotateRes.status).toBe(409);
expect(rotateRes.status).toBe(404);
awaitrotateRes.text();
// Auth ordering: an UNAUTHENTICATED write still gets 401, and the
// 401 body must not leak the resources-file path (that detail is
// only for authenticated admins).
// Method routing answers before auth: an unauthenticated write gets
// the same 405 — there is no write endpoint left to protect, and
// the 405 body carries no resources-file detail to leak.
constunauthed=awaitfetch(`${app.adminUrl}/admin/v1/models`,{
method: "POST",
headers: {"content-type": "application/json"},
body: JSON.stringify({display_name: "nope"}),
});
expect(unauthed.status).toBe(401);
constunauthedBody=(awaitunauthed.json())as{error_msg: string};
expect(unauthedBody.error_msg).not.toContain(app.resourcesPath!);
expect(unauthed.status).toBe(405);
expect((awaitunauthed.text())).not.toContain(app.resourcesPath!);
expect(postRes.status).toBe(405);
expect(postRes.headers.get("allow")).toBe("GET");
awaitpostRes.text();
// The refused write did not change the resource set.
constrelist=awaitfetch(`${app.adminUrl}/admin/v1/models`,{headers: auth});
expect(((awaitrelist.json())asunknown[]).length).toBe(2);
constdelRes=awaitfetch(`${app.adminUrl}/admin/v1/models/any-id`,{
method: "DELETE",
headers: auth,
});
expect(delRes.status).toBe(405);
expect(delRes.headers.get("allow")).toBe("GET");
awaitdelRes.text();
// The rotate route was removed outright (it was POST-only), so the
// path no longer exists at all.
constrotateRes=awaitfetch(`${app.adminUrl}/admin/v1/api_keys/any-id/rotate`,{
method: "POST",
headers: auth,
});
expect(rotateRes.status).toBe(404);
awaitrotateRes.text();
// Method routing answers before auth: an unauthenticated write gets
// the same 405 — there is no write endpoint left to protect, and
// the 405 body carries no resources-file detail to leak.
constunauthed=awaitfetch(`${app.adminUrl}/admin/v1/models`,{
method: "POST",
headers: {"content-type": "application/json"},
body: JSON.stringify({display_name: "nope"}),
});
expect(unauthed.status).toBe(405);
expect(unauthed.headers.get("allow")).toBe("GET");
expect((awaitunauthed.text())).not.toContain(app.resourcesPath!);
🧰 Tools
🪛 ast-grep (0.45.0)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { execFile } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/e2e/src/cases/file-resource-source-e2e.test.ts` around lines 179 - 213,
Update the rejected-write assertions in the e2e test to require the exact Allow
header value "GET" instead of merely containing GET, covering both the
authenticated POST/DELETE responses and the unauthenticated POST response.
Preserve the existing status and response-body assertions.

Comment threadtests/e2e/src/cases/openai-sdk-compat.test.ts

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Removes Admin API resource writes, leaving read-only resource endpoints and moving management to declarative file or etcd paths.

Changes:

  • Removes write handlers, routes, store operations, rotation, and deprecation middleware.
  • Updates OpenAPI, documentation, and tests for the read-only contract.
  • Migrates test setup to direct etcd seeding.

Reviewed changes

Copilot reviewed 25 out of 26 changed files in this pull request and generated 12 comments.

Show a summary per file
FileDescription
README.mdDocuments the read-only Admin API.
config.example.yamlClarifies declarative resource management.
crates/aisix-server/src/main.rsWires read-only admin stores.
crates/aisix-admin/src/lib.rsRemoves write routes and middleware.
crates/aisix-admin/src/openapi.rsRemoves write operations and schemas.
crates/aisix-admin/src/store.rsMakes ConfigStore read-only.
crates/aisix-admin/src/state.rsRemoves file-write guard state.
crates/aisix-admin/src/error.rsRemoves write-related errors.
crates/aisix-admin/src/file_store.rsRetains snapshot reads only.
crates/aisix-admin/src/etcd_store.rsRetains etcd reads only.
crates/aisix-admin/src/models_handlers.rsRemoves model writes.
crates/aisix-admin/src/apikeys_handlers.rsRemoves API-key writes and rotation.
crates/aisix-admin/src/provider_keys_handlers.rsRemoves provider-key writes.
crates/aisix-admin/src/guardrails_handlers.rsRemoves guardrail writes.
crates/aisix-admin/src/cache_policies_handlers.rsRemoves cache-policy writes.
crates/aisix-admin/src/observability_exporters_handlers.rsRemoves exporter writes.
crates/aisix-admin/src/mcp_servers_handlers.rsRemoves MCP-server writes.
crates/aisix-admin/src/a2a_agents_handlers.rsRemoves A2A-agent writes.
crates/aisix-admin/tests/etcd_integration.rsTests direct-etcd writes and admin reads.
tests/e2e/src/harness/admin.tsRemoves Admin API write helpers.
tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.tsRemoves obsolete path comparison tests.
tests/e2e/src/cases/openai-sdk-compat.test.tsMigrates setup to etcd seeding.
tests/e2e/src/cases/file-resource-source-e2e.test.tsTests file-mode read-only behavior.
tests/e2e/src/cases/config-forward-compat-e2e.test.tsRemoves Admin write validation coverage.
tests/e2e/src/cases/apikey-lifecycle-e2e.test.tsReplaces rotation with declarative secret swapping.
tests/e2e/src/cases/apikey-budget-e2e.test.tsRemoves obsolete write-path validation test.
Suppressed comments (1)

crates/aisix-admin/src/mcp_servers_handlers.rs:9

  • This removal also eliminates the only production call to aisix_mcp::validate_spec. The file and etcd loaders only run the core JSON Schema, which does not reject specs with zero generatable operations or colliding sanitized tool names, so the newly exclusive declarative paths accept configurations the former write API rejected. Add the semantic validation to both loaders before removing this path.
use aisix_core::McpServer;

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment threadcrates/aisix-server/src/main.rs Outdated
use serde::{Deserialize, Serialize};
use serde_json::Value;
use uuid::Uuid;
use serde::Serialize;
Comment threadcrates/aisix-server/src/main.rs Outdated
Comment on lines 9 to 10
//! replaces the `key` field with a freshly-generated `sk-*` value and
//! bumps the revision, invalidating the old credential.
Comment on lines 8 to 9
//!
//! ids are UUID v4s generated on POST; PUT preserves the existing id.
@@ -38,7 +38,7 @@ const OPENAPI_JSON_BASE: &str = r##"{
"info": {
"title": "AISIX Admin API",
"version": "dev",
"description": "The AISIX Admin API configures an open-source AISIX gateway at runtime. Use it when you operate the gateway directly and need to create or update models, caller API keys, provider credentials, guardrails, cache policies, and observability exporters.\n\nThe write endpoints (POST, PUT, DELETE) are deprecated in favor of declarative configuration: load resources from a `resources_file` (`resources.yaml`) or write them to etcd directly. Write endpoints remain functional, and every mutating response carries a `Deprecation` header (RFC 9745) plus a `Link` header with `rel=\"deprecation\"` pointing at the migration documentation. Read endpoints are not deprecated.\n\nGateways connected to AISIX Cloud do not expose this listener. Configure them through AISIX Cloud."
"description": "The AISIX Admin API is the read-only operational surface of an open-source AISIX gateway: list and inspect the loaded models, caller API keys, provider credentials, guardrails, MCP servers, A2A agents, cache policies, and observability exporters, check per-model upstream health, and drive the playground.\n\nResource write endpoints were removed in favor of declarative configuration: declare resources in a `resources_file` (`resources.yaml`) and reload with SIGHUP, or write them to etcd directly. See the resources file reference at https://docs.api7.ai/ai-gateway/reference/resources-file.\n\nGateways connected to AISIX Cloud do not expose this listener. Configure them through AISIX Cloud."
Comment on lines 4 to 5
//! validate against the JSON schema, reject duplicate names (409),
//! generate a uuid v4 on POST, bump revision on PUT.
Comment on lines 4 to 5
//! validate against the JSON schema, reject duplicate names (409),
//! generate a uuid v4 on POST, bump revision on PUT.
Comment on lines 5 to 6
//! PUT. Additionally rejects a name containing the reserved tool-namespace
//! separator `__`, since the name prefixes the server's tools.
Comment on lines 8 to 9
//! every configuration path rejects an incomplete credential set; the checks
//! below are defense in depth.
@moonmingmoonming self-assigned this Aug 10, 2026
@moonming

Copy link
Copy Markdown
CollaboratorAuthor

Review triage — every inline comment dispositioned; fixes landed in f9c10cc and e34694b.

Fixed

  • 7 handler module docs rewritten to the read-only contract; stale rotate/CRUD section comments removed (e34694b)
  • OpenAPI Caller API Keys tag no longer says "and key rotation"; path/Entry descriptions no longer claim ids are generated by the Admin API or that update/rotate bumps revisions (f9c10cc, e34694b)
  • main.rs: unused Some(path) binding → Some(_); canonical product name; removed a comment referencing write rejections (e34694b)
  • etcd integration: refused-writes test now covers PUT and DELETE with Allow assertions, and the rotate-absence checks GET the rotate URIs too — POST-only 404 couldn't distinguish a deleted route from the old handler's unknown-id 404 (f9c10cc, e34694b)
  • sdk-compat e2e: readiness gate switched from the SDK chat path to an independent authenticated GET /v1/models probe per the harness gate rules (e34694b)
  • config.example.yaml: dead docs/api-admin.md link and Admin-API management claims replaced with the declarative sources (f9c10cc)

Not adopted, with reasons

  • Assert Allow with toBe("GET"): the real binary answers Allow: GET,HEAD (axum's MethodRouter advertises HEAD alongside GET), so an exact "GET" match fails against actual behavior. toContain("GET") plus the 405 status is the contract; the 405 itself already proves no write method is routed.
  • Duplicate key_hash uniqueness on the direct-etcd path (assert_unique_key removal): real gap, but pre-existing — the admin-side check never guarded direct etcd writes, which existed before this PR and are used by the control plane (which enforces uniqueness org-side) and the file source (which has its own per-kind identity check). Filed internally for loader-side conflict detection with /status/config visibility; tracked as a follow-up rather than blocking the removal.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts (1)

276-301: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Use an independent readiness check for key setup.

seedKey gates propagation with a POST /v1/chat/completions request. The rotation and deletion flows also use the chat authorization path for their assertions. A chat failure can stop the test before it checks the key transition.

Seed the caller keys, then verify readiness with GET /v1/models and require 200. Keep chat requests for the actual rotation and revocation assertions.

As per coding guidelines, E2E readiness gates must use an independent condition, and caller API keys must be checked with GET /v1/models returning 200.

Also applies to: 311-312

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts` around lines 276 - 301,
Update the key setup readiness flow around seedKey and the related
rotation/deletion cases to use an independent GET /v1/models request requiring
HTTP 200, rather than POST /v1/chat/completions. Keep chat requests in the
secret-swap and revocation assertions only, so readiness failures cannot mask
key-transition checks.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/aisix-core/src/bin/dump-schema.rs`:
- Around line 44-47: Update the comment near the STRICT shape documentation to
qualify unknown-field rejection as applying only where the resource schema is
closed. Preserve the existing distinction between declarative write contracts
and lenient etcd reads, while acknowledging resource-specific exceptions such as
open fields and custom guardrail validation documented in the schema guidance.
In `@tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts`:
- Around line 304-307: Update the combined setup guard in the affected E2E test
to also check that otlp is available, skipping and returning when any shared
setup value—including etcdReachable, app, seed, or otlp—is missing.
---
Outside diff comments:
In `@tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts`:
- Around line 276-301: Update the key setup readiness flow around seedKey and
the related rotation/deletion cases to use an independent GET /v1/models request
requiring HTTP 200, rather than POST /v1/chat/completions. Keep chat requests in
the secret-swap and revocation assertions only, so readiness failures cannot
mask key-transition checks.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: aa61ea23-b8c7-4503-b1c7-e50f6378ee7d

📥 Commits

Reviewing files that changed from the base of the PR and between 152c57d and f9c10cc.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (12)
  • README.md
  • config.example.yaml
  • crates/aisix-admin/Cargo.toml
  • crates/aisix-admin/src/apikeys_handlers.rs
  • crates/aisix-admin/src/error.rs
  • crates/aisix-admin/src/lib.rs
  • crates/aisix-admin/src/openapi.rs
  • crates/aisix-admin/tests/etcd_integration.rs
  • crates/aisix-core/src/bin/dump-schema.rs
  • crates/aisix-etcd/src/provider.rs
  • schemas/README.md
  • tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts
💤 Files with no reviewable changes (2)
  • crates/aisix-admin/Cargo.toml
  • crates/aisix-admin/src/error.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • config.example.yaml
  • README.md
  • crates/aisix-admin/src/apikeys_handlers.rs
  • crates/aisix-admin/src/lib.rs

Comment threadcrates/aisix-core/src/bin/dump-schema.rs
Comment on lines +304 to 307
if (!etcdReachable || !app || !seed) {
ctx.skip();
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Include otlp in the setup guard.

The guard omits otlp. Add it to the combined setup check so the test does not run with incomplete shared setup.

Based on learnings, E2E cases must preserve if (!etcdReachable || !app || !seed || !otlp) { ctx.skip(); return; }.

Proposed fix
- if (!etcdReachable || !app || !seed) {+ if (!etcdReachable || !app || !seed || !otlp) {
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if(!etcdReachable||!app||!seed){
ctx.skip();
return;
}
if(!etcdReachable||!app||!seed||!otlp){
ctx.skip();
return;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts` around lines 304 - 307,
Update the combined setup guard in the affected E2E test to also check that otlp
is available, skipping and returning when any shared setup value—including
etcdReachable, app, seed, or otlp—is missing.

Source: Learnings

The admin listener keeps its read surface (lists/gets for all 8
resource kinds incl. the former apikeys spelling, models/status,
health, OpenAPI + Scalar, playground, livez/readyz); resources are
managed exclusively through the declarative paths — resources_file
(SIGHUP reload) or direct etcd writes.
BREAKING CHANGE: POST/PUT/DELETE on /admin/v1/* answer 405 with
Allow: GET (409 file-managed rejection included); the api-key rotate
route is gone (404) — rotate declaratively by writing the same
resource id with a new key_hash. The published OpenAPI documents no
write operations; write-only component schemas (ApiKeyRequest,
ApiKeyEntry, ApiKeyRotateResponse, DeleteResponse) are removed.
- router: every /admin/v1/* resource route serves get() only; rotate
routes, file-managed write guard, and RFC 9745 deprecation-header
middleware deleted; 43 write/rotate/uniqueness handler fns removed
- store: ConfigStore is read-only (16 put_*/delete_* methods and
StoreError::ReadOnly removed); EtcdConfigStore keeps reads only;
FileManagedStore::new(snapshot) drops the path param; InMemoryStore
keeps #[cfg(test)] inherent writes for unit-test seeding
- openapi: 24 write ops + rotate path removed from the base document;
no-op deprecation-marker pass deleted; unreachable component
schemas pruned; new gate pins GET-only /admin/v1/* and zero
deprecated marks
- tests: write-path suites deleted; read tests seed via InMemoryStore;
new 405/Allow + rotate-404 contract tests; etcd integration tests
seed via direct etcd writes (the declarative front door) and pin
that refused writes never touch etcd
- e2e: AdminClient write helpers removed (SeedClient is the write
front door); file-resource-source pins the new read-only contract;
apikey-lifecycle rotate coverage became a declarative secret-swap
test; obsolete write-path cases deleted
- docs: README, config.example.yaml, crate module docs updated
Second-auditor findings on the removal PR, all test/doc-level (no
runtime changes):
- rotate-404 tests now GET the rotate URIs too — POST-only 404 could
not distinguish a deleted route from the old handler's unknown-id
404; GET answers 405 on a surviving POST-only route
- e2e: deleting a key's etcd entry revokes an in-use bearer
(fail-closed, unknown-token 401, other keys unaffected) — the
deletion branch had lost its only end-to-end proof
- etcd integration: a2a_agents round-trip + loader coverage (7 -> 8
kinds)
- apikeys read test pins the full PublicApiKey projection
(allowed_tools/disabled/expires_at), not just the id
- OpenAPI descriptions stop claiming ids are generated by the Admin
API and revisions increment on update/rotate
- stale write-path narrative removed: config.example.yaml (dead
docs/api-admin.md link), schemas/README, dump-schema comment,
apikeys_handlers module doc, aisix-etcd provider doc, README RBAC
row + e2e counts
- dead code: AdminError::{BadRequest,Conflict,Schema} variants and
the aisix-mcp/uuid dependencies left over from the write path
Per-comment review triage:
- 7 handler module docs rewritten from CRUD-era text to the surviving
read-only contract; stale rotate/CRUD section comments in the
aisix-admin test module removed
- OpenAPI 'Caller API Keys' tag no longer advertises key rotation
- main.rs: unused match binding -> Some(_); canonical product name;
dropped a comment referencing the removed write-rejection path
- etcd integration: refused-writes test now covers PUT and DELETE
(405 + Allow), not just POST
- sdk-compat e2e: readiness gate switched from the SDK chat path (the
behavior under test) to an independent authenticated GET /v1/models
probe, per the harness gate rules
Cold-audit closeout:
- removed_resource_writes_answer_405_with_allow_get now generates the
FULL matrix (9 route spellings x POST/PUT/DELETE) instead of a
sampled subset — a partial revert (e.g. PUT re-added on one {id}
route) previously passed the whole suite
- last stale write-path narrative: store.rs module doc (read-only
trait), aisix-core schema.rs/models docs (declarative writers, not
'Admin API ... 400'), e2e smoke/seed/app/forward-compat headers no
longer cite deleted characterization or held-back write cases,
openapi.rs base-doc comment names a surviving schema
Round-two auditor findings:
- OpenAPI: the two remaining Entry revision descriptions (McpServer,
A2aAgent) stop describing create/update lifecycle; a regression
assertion now rejects write-lifecycle prose on any documented
revision field
- schemas/README + schema.rs + dump-schema: scope the strict-contract
claim to the in-repo writers (aisix validate, file source) — the
control plane validates its own API schema, a raw direct etcd put
gets no synchronous validation (lenient read only), and
unknown-field rejection applies only where a resource closes fields;
the previous rewrite overclaimed all three
- deletion-revocation e2e: propagation barrier is now a fresh key
seeded after the delete (later etcd revision), so the revocation
assertion is a real assertion instead of a gate poll; a regression
fails the assert, not a 30s timeout
- apikeys projection test seeds rate_limit + allowed_agents and pins
the list entry's projection too; module doc describes PublicApiKey
as an explicit allowlist (not 'minus nothing')
- lifecycle prose: secret-swap invalidates 'as soon as the write
propagates' (not 'immediately'); README e2e counts scoped to
scenario files (183/496)
@moonming
moonmingforce-pushed the feat/remove-admin-write-path branch from 74b0db5 to f43eab2CompareAugust 11, 2026 01:59
@moonming
moonming merged commit b61d270 into mainAug 11, 2026
30 of 32 checks passed
@moonming
moonming deleted the feat/remove-admin-write-path branch August 11, 2026 02:20
Sign up for freeto 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.

2 participants

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(admin)!: remove the Admin API resource write path by moonming · Pull Request #915 · api7/aisix · GitHub
Skip to content

feat(admin)!: remove the Admin API resource write path - #915

Merged
moonming merged 5 commits into
mainfrom
feat/remove-admin-write-path
Aug 11, 2026
Merged

feat(admin)!: remove the Admin API resource write path#915
moonming merged 5 commits into
mainfrom
feat/remove-admin-write-path

Conversation

@moonming

@moonmingmoonming commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Removes the Admin API resource write path. The admin listener (:3001) keeps its read surface — lists/gets for all 8 resource kinds (including the former apikeys spelling), /admin/v1/models/status, /admin/v1/health, OpenAPI + Scalar UI, playground, /livez, /readyz — but resources are now managed exclusively through the declarative paths: a resources_file (resources.yaml, reloaded on SIGHUP) or direct etcd writes.

This is the final step of the deprecation announced in v0.4.0 (RFC 9745 Deprecation headers) and executes the removal scheduled for v0.5.0+ after #848 lifted the write-path-exclusive validations into the canonical schemas so the declarative paths enforce them.

⚠️ Breaking changes

BeforeAfter
POST /admin/v1/<kind>, PUT/DELETE /admin/v1/<kind>/{id} (deprecated, functional)405 with Allow: GET
POST /admin/v1/api_keys/{id}/rotate (and apikeys spelling)404 — the route is gone
File mode: writes rejected with 409 naming the resources file405, same as every other mode
Rotate returned a fresh plaintext keyNo DP-side plaintext rotation. Rotate declaratively: write the same resource id with a new key_hash — the old plaintext stops authenticating as soon as the write propagates (pinned in apikey-lifecycle-e2e)

What to update:

  • Scripts that created/updated/deleted resources via :3001 → write resources.yaml (validate offline with aisix validate --resources <file>, reload with SIGHUP) or write entity-value JSON to etcd at {prefix}/{kind}/{id}.
  • Scripts that rotated caller keys via /rotate → hash the new secret client-side and update the resource's key_hash.
  • The published OpenAPI (/admin/openapi.json) no longer documents write operations; the write-only component schemas (ApiKeyRequest, ApiKeyEntry, ApiKeyRotateResponse, DeleteResponse) are gone from components.schemas.

What changed

Router / handlers — every /admin/v1/* resource route serves get(...) only; both rotate routes deleted; the file-managed write guard and the RFC 9745 deprecation-header middleware deleted (nothing left to mark). 43 write/rotate/uniqueness handler fns removed across the 8 handler modules.

Store layerConfigStore is now a read-only trait (16 put_*/delete_* methods removed, StoreError::ReadOnly gone). EtcdConfigStore keeps only reads (module doc rewritten: resources reach etcd through the declarative paths). FileManagedStore::new(snapshot) drops the path parameter — read-only by construction. InMemoryStore keeps #[cfg(test)] inherent write methods used by unit-test seeding.

OpenAPI — 24 write operations and the rotate path removed from the base document; the no-op write-deprecation marker pass deleted; unreachable component schemas pruned via a reachability walk from paths. New gate test pins that the published reference documents zero non-GET operations under /admin/v1/ and zero deprecated marks anywhere.

Tests — write-path unit tests (CRUD flows, rotate atomicity, write-auth, write-validation) deleted; read tests re-seeded through InMemoryStore; new contract tests pin 405 + Allow: GET on every collection/:id route (auth'd and unauthenticated) and 404 on both rotate spellings. The etcd integration test now seeds via direct etcd_client puts — the path operators actually use.

e2eAdminClient write helpers removed (reads stay; SeedClient is the write front door). file-resource-source-e2e pins the new file-mode contract (reads serve the file, writes 405, rotate 404, unauthenticated write 405 with no file-path leak). apikey-lifecycle-e2e's rotate coverage became a declarative secret-swap test (old plaintext dies immediately, id unchanged). Deleted: seed-vs-admin-characterization-e2e (its own comment scheduled retirement once the seed migration completed), apikey-budget-e2e (tested write-path 400s), the sdk-compat deprecation-header test, and forward-compat's admin strict-write test.

Docs — README, config.example.yaml, crate module docs updated to the read-only story.

Non-goals / follow-ups (filed internally)

  • 3 OpenAPI-parsing validations (mcp validate_spec, HeaderName typing, duplicate tool names) still lack declarative-path enforcement — blocked on a dependency-direction extraction, tracked internally.
  • etcd watch keeps last-good on an invalid PUT while resync drops the row — policy decision tracked internally.

Verification

  • cargo fmt --check, cargo clippy --workspace --all-targets -- -D warnings, cargo test --workspace — all green.
  • Real binary, file mode: boots with resources.yaml, GET list serves the file, POST /admin/v1/models → 405 + Allow: GET, rotate → 404, /admin/openapi.json documents no admin writes.
  • Local e2e stack run before push.

Summary by CodeRabbit

  • Changes
    • The Admin API is now read-only for managed resources.
    • Resource listing and detail views remain available through GET requests.
    • Creation, updates, deletion, and API-key rotation through the Admin API are no longer supported.
    • Unsupported write requests return 405 responses, while rotation routes return 404.
    • Manage resources through resources_file reloads or direct etcd writes.
    • Updated configuration, validation guidance, and end-to-end coverage to reflect the read-only administration model.

CopilotAI balanced review requested due to automatic review settings August 10, 2026 05:09
@coderabbitai

coderabbitaiBot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9acb9ae1-7d4d-42ce-83f0-61de46c03b1a

📥 Commits

Reviewing files that changed from the base of the PR and between f9c10cc and f43eab2.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (25)
  • README.md
  • config.example.yaml
  • crates/aisix-admin/src/a2a_agents_handlers.rs
  • crates/aisix-admin/src/apikeys_handlers.rs
  • crates/aisix-admin/src/cache_policies_handlers.rs
  • crates/aisix-admin/src/guardrails_handlers.rs
  • crates/aisix-admin/src/lib.rs
  • crates/aisix-admin/src/mcp_servers_handlers.rs
  • crates/aisix-admin/src/models_handlers.rs
  • crates/aisix-admin/src/observability_exporters_handlers.rs
  • crates/aisix-admin/src/openapi.rs
  • crates/aisix-admin/src/provider_keys_handlers.rs
  • crates/aisix-admin/src/store.rs
  • crates/aisix-admin/tests/etcd_integration.rs
  • crates/aisix-core/src/bin/dump-schema.rs
  • crates/aisix-core/src/models/mod.rs
  • crates/aisix-core/src/models/schema.rs
  • crates/aisix-server/src/main.rs
  • schemas/README.md
  • tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts
  • tests/e2e/src/cases/config-forward-compat-e2e.test.ts
  • tests/e2e/src/cases/openai-sdk-compat.test.ts
  • tests/e2e/src/cases/smoke.test.ts
  • tests/e2e/src/harness/app.ts
  • tests/e2e/src/harness/seed.ts
💤 Files with no reviewable changes (1)
  • tests/e2e/src/cases/smoke.test.ts
🚧 Files skipped from review as they are similar to previous changes (12)
  • crates/aisix-server/src/main.rs
  • config.example.yaml
  • crates/aisix-admin/src/provider_keys_handlers.rs
  • crates/aisix-admin/src/models_handlers.rs
  • schemas/README.md
  • crates/aisix-core/src/bin/dump-schema.rs
  • tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts
  • tests/e2e/src/cases/config-forward-compat-e2e.test.ts
  • crates/aisix-admin/src/apikeys_handlers.rs
  • README.md
  • crates/aisix-admin/tests/etcd_integration.rs
  • crates/aisix-admin/src/lib.rs

📝 Walkthrough

Walkthrough

The admin listener now exposes read-only resource routes. Resource writes use resources_file reloads, direct etcd writes, or declarative seed helpers. Stores, server wiring, tests, and documentation were updated.

Changes

Read-only admin resource surface

Layer / File(s)Summary
Read-only store contracts
crates/aisix-admin/src/store.rs, crates/aisix-admin/src/etcd_store.rs, crates/aisix-admin/src/file_store.rs, crates/aisix-admin/src/error.rs, crates/aisix-admin/src/state.rs
Store traits and implementations now support resource reads only.
GET-only admin routes
crates/aisix-admin/src/lib.rs, crates/aisix-admin/src/*_handlers.rs
Create, update, delete, and API-key rotation routes were removed.
Read-only server wiring
crates/aisix-server/src/main.rs
The server constructs read-only file-backed admin state and removes the file-managed write guard.
Direct etcd setup and integration coverage
crates/aisix-admin/tests/etcd_integration.rs, crates/aisix-admin/src/etcd_store.rs
Tests seed canonical documents directly in etcd and verify read-only behavior.
Declarative E2E flows and documentation
tests/e2e/src/cases/*, tests/e2e/src/harness/*, README.md, config.example.yaml, schemas/README.md
E2E setup and documentation now use declarative resource-management paths.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
participant EtcdClient
participant AdminAPI
participant ConfigLoader
EtcdClient->>EtcdClient: write canonical resource document
AdminAPI->>EtcdClient: list/get resource
EtcdClient-->>AdminAPI: resource document
ConfigLoader->>EtcdClient: load canonical resource documents
EtcdClient-->>ConfigLoader: configuration data
Loading

Possibly related PRs

  • api7/aisix#792: Related through direct etcd seeding and readiness migration.
  • api7/aisix#800: Related through removal of Admin API writes and E2E harness changes.
  • api7/aisix#848: Related through A2A and MCP admin-handler changes.

Suggested reviewers:jarvis9443


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error, 1 warning)

Check nameStatusExplanationResolution
Security Check❌ ErrorCategory 1: admin GETs serialize ProviderKey.api_key, McpServer.secret, and A2aAgent.secret; PublicApiKey also returns key_hash without redaction.Return redacted DTOs for every secret-bearing resource, omit key_hash and credential fields, and replace backend-detail error responses with generic messages.
E2e Test Quality Review⚠️ WarningE2E quality issue: config-forward-compat-e2e.test.ts test 3 deletes yellowKeyId created only by test 1, with no ordering declaration, creating a hidden test dependency.Make each test self-contained by seeding and cleaning its own rows, or explicitly declare and document sequential execution; also keep route-matrix coverage at the process level if all aliases are contractual.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly and concisely identifies the main breaking change: removing the Admin API resource write path.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/remove-admin-write-path

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/aisix-admin/tests/etcd_integration.rs (1)

383-489: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Add the missing a2a_agents canonical document.

The PR retains eight resource kinds, but writes contains seven entries. It omits a2a_agents. Add a valid A2A agent document, assert stats.accepted == 8, and assert snap.a2a_agents.len() == 1.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/aisix-admin/tests/etcd_integration.rs` around lines 383 - 489, Extend
the writes array with a valid a2a_agents canonical document using the existing
seed flow, then update the accepted-entry assertion from 7 to 8. Add a
corresponding snap.a2a_agents length assertion expecting exactly one loaded
agent, leaving the other resource assertions unchanged.
🧹 Nitpick comments (1)
crates/aisix-admin/src/lib.rs (1)

1131-1198: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Read coverage now spans only two of eight resource kinds. Deleting the write handlers also deleted each handler module's test module. The replacement read tests in lib.rs seed only models and api_keys. The other six kinds — provider_keys, guardrails, cache_policies, observability_exporters, mcp_servers, a2a_agents — have 405 write-refusal coverage but no test proving that GET serves a seeded entry. These handler bodies are hand-written per module, not macro-generated, so a wrong store call or a wrong response shape in one of the six would pass CI.

  • crates/aisix-admin/src/lib.rs#L1131-L1198: extend build_seedable_state with seed helpers for the remaining six kinds, then add list and get-by-id assertions for each, mirroring list_models_returns_seeded_entries and get_model_serves_seeded_entry.
  • crates/aisix-admin/src/a2a_agents_handlers.rs#L12: add a test that list_a2a_agents and get_a2a_agent return a seeded A2aAgent, or confirm the new lib.rs tests cover this module.
  • crates/aisix-admin/src/mcp_servers_handlers.rs#L9: add the same list/get coverage for McpServer, or confirm the new lib.rs tests cover this module.

Note that InMemoryStore currently exposes only put_model and put_apikey as #[cfg(test)] helpers, so seeding the other six kinds requires adding matching helpers in crates/aisix-admin/src/store.rs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/aisix-admin/src/lib.rs` around lines 1131 - 1198, Extend
crates/aisix-admin/src/store.rs with cfg(test) put helpers for provider_keys,
guardrails, cache_policies, observability_exporters, mcp_servers, and
a2a_agents, then update build_seedable_state and the tests in
crates/aisix-admin/src/lib.rs#L1131-L1198 to seed each kind and assert both list
and get-by-id responses, mirroring the model tests. Ensure
crates/aisix-admin/src/a2a_agents_handlers.rs#L12 and
crates/aisix-admin/src/mcp_servers_handlers.rs#L9 are covered by these lib.rs
tests; no direct handler changes are required unless coverage cannot exercise
their list/get functions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/aisix-admin/src/lib.rs`:
- Around line 1200-1230: Remove the stale rotation-coverage comment immediately
before openapi_apikey_schema_excludes_max_budget_usd, and delete the empty
CachePolicy CRUD and Health endpoint section comments. Preserve the
guardrail_payload function, the ObservabilityExporter CRUD comment, and all
surrounding tests and formatting.
In `@crates/aisix-admin/tests/etcd_integration.rs`:
- Around line 317-354: Extend the resource-route assertions in the integration
test to issue both PUT and DELETE requests to /admin/v1/models, asserting each
returns METHOD_NOT_ALLOWED and includes GET in the Allow header. Keep the
existing POST check and final etcd emptiness assertion so all refused write
methods verify that no data is written.
In `@crates/aisix-server/src/main.rs`:
- Around line 898-902: Update the file-source match arm in the admin_store
initialization to bind the second tuple element as Some(_) instead of
Some(path), preserving the existing condition and FileManagedStore construction.
In `@tests/e2e/src/cases/file-resource-source-e2e.test.ts`:
- Around line 179-213: Update the rejected-write assertions in the e2e test to
require the exact Allow header value "GET" instead of merely containing GET,
covering both the authenticated POST/DELETE responses and the unauthenticated
POST response. Preserve the existing status and response-body assertions.
In `@tests/e2e/src/cases/openai-sdk-compat.test.ts`:
- Around line 54-84: After seeding the API key in the test setup, add an
independent readiness poll using the seeded caller credentials against
authenticated GET /v1/models, continuing until it returns 200. Remove the
client.chat.completions.create-based propagation gate and keep that call
exclusively for the SDK chat behavior under test.
---
Outside diff comments:
In `@crates/aisix-admin/tests/etcd_integration.rs`:
- Around line 383-489: Extend the writes array with a valid a2a_agents canonical
document using the existing seed flow, then update the accepted-entry assertion
from 7 to 8. Add a corresponding snap.a2a_agents length assertion expecting
exactly one loaded agent, leaving the other resource assertions unchanged.
---
Nitpick comments:
In `@crates/aisix-admin/src/lib.rs`:
- Around line 1131-1198: Extend crates/aisix-admin/src/store.rs with cfg(test)
put helpers for provider_keys, guardrails, cache_policies,
observability_exporters, mcp_servers, and a2a_agents, then update
build_seedable_state and the tests in crates/aisix-admin/src/lib.rs#L1131-L1198
to seed each kind and assert both list and get-by-id responses, mirroring the
model tests. Ensure crates/aisix-admin/src/a2a_agents_handlers.rs#L12 and
crates/aisix-admin/src/mcp_servers_handlers.rs#L9 are covered by these lib.rs
tests; no direct handler changes are required unless coverage cannot exercise
their list/get functions.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f3cea19a-3b55-4ad2-b83e-bc29037270c5

📥 Commits

Reviewing files that changed from the base of the PR and between b77f1a6 and 152c57d.

📒 Files selected for processing (26)
  • README.md
  • config.example.yaml
  • crates/aisix-admin/src/a2a_agents_handlers.rs
  • crates/aisix-admin/src/apikeys_handlers.rs
  • crates/aisix-admin/src/cache_policies_handlers.rs
  • crates/aisix-admin/src/error.rs
  • crates/aisix-admin/src/etcd_store.rs
  • crates/aisix-admin/src/file_store.rs
  • crates/aisix-admin/src/guardrails_handlers.rs
  • crates/aisix-admin/src/lib.rs
  • crates/aisix-admin/src/mcp_servers_handlers.rs
  • crates/aisix-admin/src/models_handlers.rs
  • crates/aisix-admin/src/observability_exporters_handlers.rs
  • crates/aisix-admin/src/openapi.rs
  • crates/aisix-admin/src/provider_keys_handlers.rs
  • crates/aisix-admin/src/state.rs
  • crates/aisix-admin/src/store.rs
  • crates/aisix-admin/tests/etcd_integration.rs
  • crates/aisix-server/src/main.rs
  • tests/e2e/src/cases/apikey-budget-e2e.test.ts
  • tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts
  • tests/e2e/src/cases/config-forward-compat-e2e.test.ts
  • tests/e2e/src/cases/file-resource-source-e2e.test.ts
  • tests/e2e/src/cases/openai-sdk-compat.test.ts
  • tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts
  • tests/e2e/src/harness/admin.ts
💤 Files with no reviewable changes (8)
  • tests/e2e/src/cases/apikey-budget-e2e.test.ts
  • tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts
  • crates/aisix-admin/src/state.rs
  • crates/aisix-admin/src/observability_exporters_handlers.rs
  • crates/aisix-admin/src/cache_policies_handlers.rs
  • crates/aisix-admin/src/models_handlers.rs
  • crates/aisix-admin/src/guardrails_handlers.rs
  • crates/aisix-admin/src/provider_keys_handlers.rs

Comment threadcrates/aisix-admin/src/lib.rs Outdated
Comment threadcrates/aisix-admin/tests/etcd_integration.rs Outdated
Comment threadcrates/aisix-server/src/main.rs
Comment on lines +179 to +213
expect(postRes.status).toBe(405);
expect(postRes.headers.get("allow")).toContain("GET");
await postRes.text();

// The refused write did not change the resource set.
const relist = await fetch(`${app.adminUrl}/admin/v1/models`, { headers: auth });
expect(((await relist.json()) as unknown[]).length).toBe(2);

// DELETE and rotate are covered by the same guard.
const delRes = await fetch(`${app.adminUrl}/admin/v1/models/any-id`, {
method: "DELETE",
headers: auth,
});
expect(delRes.status).toBe(409);
expect(delRes.status).toBe(405);
expect(delRes.headers.get("allow")).toContain("GET");
await delRes.text();

// The rotate route was removed outright (it was POST-only), so the
// path no longer exists at all.
const rotateRes = await fetch(`${app.adminUrl}/admin/v1/api_keys/any-id/rotate`, {
method: "POST",
headers: auth,
});
expect(rotateRes.status).toBe(409);
expect(rotateRes.status).toBe(404);
await rotateRes.text();

// Auth ordering: an UNAUTHENTICATED write still gets 401, and the
// 401 body must not leak the resources-file path (that detail is
// only for authenticated admins).
// Method routing answers before auth: an unauthenticated write gets
// the same 405 — there is no write endpoint left to protect, and
// the 405 body carries no resources-file detail to leak.
const unauthed = await fetch(`${app.adminUrl}/admin/v1/models`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ display_name: "nope" }),
});
expect(unauthed.status).toBe(401);
const unauthedBody = (await unauthed.json()) as { error_msg: string };
expect(unauthedBody.error_msg).not.toContain(app.resourcesPath!);
expect(unauthed.status).toBe(405);
expect((await unauthed.text())).not.toContain(app.resourcesPath!);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert the exact Allow header value.

Use toBe("GET") for each rejected write. Add the same assertion for the unauthenticated request. toContain("GET") also accepts an invalid value such as GET, POST.

Based on PR objectives, rejected resource writes must return 405 with Allow: GET.

Proposed test update
- expect(postRes.headers.get("allow")).toContain("GET");+ expect(postRes.headers.get("allow")).toBe("GET");
...
- expect(delRes.headers.get("allow")).toContain("GET");+ expect(delRes.headers.get("allow")).toBe("GET");
...
expect(unauthed.status).toBe(405);
+ expect(unauthed.headers.get("allow")).toBe("GET");
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
expect(postRes.status).toBe(405);
expect(postRes.headers.get("allow")).toContain("GET");
awaitpostRes.text();
// The refused write did not change the resource set.
constrelist=awaitfetch(`${app.adminUrl}/admin/v1/models`,{headers: auth});
expect(((awaitrelist.json())asunknown[]).length).toBe(2);
// DELETE and rotate are covered by the same guard.
constdelRes=awaitfetch(`${app.adminUrl}/admin/v1/models/any-id`,{
method: "DELETE",
headers: auth,
});
expect(delRes.status).toBe(409);
expect(delRes.status).toBe(405);
expect(delRes.headers.get("allow")).toContain("GET");
awaitdelRes.text();
// The rotate route was removed outright (it was POST-only), so the
// path no longer exists at all.
constrotateRes=awaitfetch(`${app.adminUrl}/admin/v1/api_keys/any-id/rotate`,{
method: "POST",
headers: auth,
});
expect(rotateRes.status).toBe(409);
expect(rotateRes.status).toBe(404);
awaitrotateRes.text();
// Auth ordering: an UNAUTHENTICATED write still gets 401, and the
// 401 body must not leak the resources-file path (that detail is
// only for authenticated admins).
// Method routing answers before auth: an unauthenticated write gets
// the same 405 — there is no write endpoint left to protect, and
// the 405 body carries no resources-file detail to leak.
constunauthed=awaitfetch(`${app.adminUrl}/admin/v1/models`,{
method: "POST",
headers: {"content-type": "application/json"},
body: JSON.stringify({display_name: "nope"}),
});
expect(unauthed.status).toBe(401);
constunauthedBody=(awaitunauthed.json())as{error_msg: string};
expect(unauthedBody.error_msg).not.toContain(app.resourcesPath!);
expect(unauthed.status).toBe(405);
expect((awaitunauthed.text())).not.toContain(app.resourcesPath!);
expect(postRes.status).toBe(405);
expect(postRes.headers.get("allow")).toBe("GET");
awaitpostRes.text();
// The refused write did not change the resource set.
constrelist=awaitfetch(`${app.adminUrl}/admin/v1/models`,{headers: auth});
expect(((awaitrelist.json())asunknown[]).length).toBe(2);
constdelRes=awaitfetch(`${app.adminUrl}/admin/v1/models/any-id`,{
method: "DELETE",
headers: auth,
});
expect(delRes.status).toBe(405);
expect(delRes.headers.get("allow")).toBe("GET");
awaitdelRes.text();
// The rotate route was removed outright (it was POST-only), so the
// path no longer exists at all.
constrotateRes=awaitfetch(`${app.adminUrl}/admin/v1/api_keys/any-id/rotate`,{
method: "POST",
headers: auth,
});
expect(rotateRes.status).toBe(404);
awaitrotateRes.text();
// Method routing answers before auth: an unauthenticated write gets
// the same 405 — there is no write endpoint left to protect, and
// the 405 body carries no resources-file detail to leak.
constunauthed=awaitfetch(`${app.adminUrl}/admin/v1/models`,{
method: "POST",
headers: {"content-type": "application/json"},
body: JSON.stringify({display_name: "nope"}),
});
expect(unauthed.status).toBe(405);
expect(unauthed.headers.get("allow")).toBe("GET");
expect((awaitunauthed.text())).not.toContain(app.resourcesPath!);
🧰 Tools
🪛 ast-grep (0.45.0)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { execFile } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/e2e/src/cases/file-resource-source-e2e.test.ts` around lines 179 - 213,
Update the rejected-write assertions in the e2e test to require the exact Allow
header value "GET" instead of merely containing GET, covering both the
authenticated POST/DELETE responses and the unauthenticated POST response.
Preserve the existing status and response-body assertions.

Comment threadtests/e2e/src/cases/openai-sdk-compat.test.ts

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Removes Admin API resource writes, leaving read-only resource endpoints and moving management to declarative file or etcd paths.

Changes:

  • Removes write handlers, routes, store operations, rotation, and deprecation middleware.
  • Updates OpenAPI, documentation, and tests for the read-only contract.
  • Migrates test setup to direct etcd seeding.

Reviewed changes

Copilot reviewed 25 out of 26 changed files in this pull request and generated 12 comments.

Show a summary per file
FileDescription
README.mdDocuments the read-only Admin API.
config.example.yamlClarifies declarative resource management.
crates/aisix-server/src/main.rsWires read-only admin stores.
crates/aisix-admin/src/lib.rsRemoves write routes and middleware.
crates/aisix-admin/src/openapi.rsRemoves write operations and schemas.
crates/aisix-admin/src/store.rsMakes ConfigStore read-only.
crates/aisix-admin/src/state.rsRemoves file-write guard state.
crates/aisix-admin/src/error.rsRemoves write-related errors.
crates/aisix-admin/src/file_store.rsRetains snapshot reads only.
crates/aisix-admin/src/etcd_store.rsRetains etcd reads only.
crates/aisix-admin/src/models_handlers.rsRemoves model writes.
crates/aisix-admin/src/apikeys_handlers.rsRemoves API-key writes and rotation.
crates/aisix-admin/src/provider_keys_handlers.rsRemoves provider-key writes.
crates/aisix-admin/src/guardrails_handlers.rsRemoves guardrail writes.
crates/aisix-admin/src/cache_policies_handlers.rsRemoves cache-policy writes.
crates/aisix-admin/src/observability_exporters_handlers.rsRemoves exporter writes.
crates/aisix-admin/src/mcp_servers_handlers.rsRemoves MCP-server writes.
crates/aisix-admin/src/a2a_agents_handlers.rsRemoves A2A-agent writes.
crates/aisix-admin/tests/etcd_integration.rsTests direct-etcd writes and admin reads.
tests/e2e/src/harness/admin.tsRemoves Admin API write helpers.
tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.tsRemoves obsolete path comparison tests.
tests/e2e/src/cases/openai-sdk-compat.test.tsMigrates setup to etcd seeding.
tests/e2e/src/cases/file-resource-source-e2e.test.tsTests file-mode read-only behavior.
tests/e2e/src/cases/config-forward-compat-e2e.test.tsRemoves Admin write validation coverage.
tests/e2e/src/cases/apikey-lifecycle-e2e.test.tsReplaces rotation with declarative secret swapping.
tests/e2e/src/cases/apikey-budget-e2e.test.tsRemoves obsolete write-path validation test.
Suppressed comments (1)

crates/aisix-admin/src/mcp_servers_handlers.rs:9

  • This removal also eliminates the only production call to aisix_mcp::validate_spec. The file and etcd loaders only run the core JSON Schema, which does not reject specs with zero generatable operations or colliding sanitized tool names, so the newly exclusive declarative paths accept configurations the former write API rejected. Add the semantic validation to both loaders before removing this path.
use aisix_core::McpServer;

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment threadcrates/aisix-server/src/main.rs Outdated
use serde::{Deserialize, Serialize};
use serde_json::Value;
use uuid::Uuid;
use serde::Serialize;
Comment threadcrates/aisix-server/src/main.rs Outdated
Comment on lines 9 to 10
//! replaces the `key` field with a freshly-generated `sk-*` value and
//! bumps the revision, invalidating the old credential.
Comment on lines 8 to 9
//!
//! ids are UUID v4s generated on POST; PUT preserves the existing id.
@@ -38,7 +38,7 @@ const OPENAPI_JSON_BASE: &str = r##"{
"info": {
"title": "AISIX Admin API",
"version": "dev",
"description": "The AISIX Admin API configures an open-source AISIX gateway at runtime. Use it when you operate the gateway directly and need to create or update models, caller API keys, provider credentials, guardrails, cache policies, and observability exporters.\n\nThe write endpoints (POST, PUT, DELETE) are deprecated in favor of declarative configuration: load resources from a `resources_file` (`resources.yaml`) or write them to etcd directly. Write endpoints remain functional, and every mutating response carries a `Deprecation` header (RFC 9745) plus a `Link` header with `rel=\"deprecation\"` pointing at the migration documentation. Read endpoints are not deprecated.\n\nGateways connected to AISIX Cloud do not expose this listener. Configure them through AISIX Cloud."
"description": "The AISIX Admin API is the read-only operational surface of an open-source AISIX gateway: list and inspect the loaded models, caller API keys, provider credentials, guardrails, MCP servers, A2A agents, cache policies, and observability exporters, check per-model upstream health, and drive the playground.\n\nResource write endpoints were removed in favor of declarative configuration: declare resources in a `resources_file` (`resources.yaml`) and reload with SIGHUP, or write them to etcd directly. See the resources file reference at https://docs.api7.ai/ai-gateway/reference/resources-file.\n\nGateways connected to AISIX Cloud do not expose this listener. Configure them through AISIX Cloud."
Comment on lines 4 to 5
//! validate against the JSON schema, reject duplicate names (409),
//! generate a uuid v4 on POST, bump revision on PUT.
Comment on lines 4 to 5
//! validate against the JSON schema, reject duplicate names (409),
//! generate a uuid v4 on POST, bump revision on PUT.
Comment on lines 5 to 6
//! PUT. Additionally rejects a name containing the reserved tool-namespace
//! separator `__`, since the name prefixes the server's tools.
Comment on lines 8 to 9
//! every configuration path rejects an incomplete credential set; the checks
//! below are defense in depth.
@moonmingmoonming self-assigned this Aug 10, 2026
@moonming

Copy link
Copy Markdown
CollaboratorAuthor

Review triage — every inline comment dispositioned; fixes landed in f9c10cc and e34694b.

Fixed

  • 7 handler module docs rewritten to the read-only contract; stale rotate/CRUD section comments removed (e34694b)
  • OpenAPI Caller API Keys tag no longer says "and key rotation"; path/Entry descriptions no longer claim ids are generated by the Admin API or that update/rotate bumps revisions (f9c10cc, e34694b)
  • main.rs: unused Some(path) binding → Some(_); canonical product name; removed a comment referencing write rejections (e34694b)
  • etcd integration: refused-writes test now covers PUT and DELETE with Allow assertions, and the rotate-absence checks GET the rotate URIs too — POST-only 404 couldn't distinguish a deleted route from the old handler's unknown-id 404 (f9c10cc, e34694b)
  • sdk-compat e2e: readiness gate switched from the SDK chat path to an independent authenticated GET /v1/models probe per the harness gate rules (e34694b)
  • config.example.yaml: dead docs/api-admin.md link and Admin-API management claims replaced with the declarative sources (f9c10cc)

Not adopted, with reasons

  • Assert Allow with toBe("GET"): the real binary answers Allow: GET,HEAD (axum's MethodRouter advertises HEAD alongside GET), so an exact "GET" match fails against actual behavior. toContain("GET") plus the 405 status is the contract; the 405 itself already proves no write method is routed.
  • Duplicate key_hash uniqueness on the direct-etcd path (assert_unique_key removal): real gap, but pre-existing — the admin-side check never guarded direct etcd writes, which existed before this PR and are used by the control plane (which enforces uniqueness org-side) and the file source (which has its own per-kind identity check). Filed internally for loader-side conflict detection with /status/config visibility; tracked as a follow-up rather than blocking the removal.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts (1)

276-301: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Use an independent readiness check for key setup.

seedKey gates propagation with a POST /v1/chat/completions request. The rotation and deletion flows also use the chat authorization path for their assertions. A chat failure can stop the test before it checks the key transition.

Seed the caller keys, then verify readiness with GET /v1/models and require 200. Keep chat requests for the actual rotation and revocation assertions.

As per coding guidelines, E2E readiness gates must use an independent condition, and caller API keys must be checked with GET /v1/models returning 200.

Also applies to: 311-312

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts` around lines 276 - 301,
Update the key setup readiness flow around seedKey and the related
rotation/deletion cases to use an independent GET /v1/models request requiring
HTTP 200, rather than POST /v1/chat/completions. Keep chat requests in the
secret-swap and revocation assertions only, so readiness failures cannot mask
key-transition checks.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/aisix-core/src/bin/dump-schema.rs`:
- Around line 44-47: Update the comment near the STRICT shape documentation to
qualify unknown-field rejection as applying only where the resource schema is
closed. Preserve the existing distinction between declarative write contracts
and lenient etcd reads, while acknowledging resource-specific exceptions such as
open fields and custom guardrail validation documented in the schema guidance.
In `@tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts`:
- Around line 304-307: Update the combined setup guard in the affected E2E test
to also check that otlp is available, skipping and returning when any shared
setup value—including etcdReachable, app, seed, or otlp—is missing.
---
Outside diff comments:
In `@tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts`:
- Around line 276-301: Update the key setup readiness flow around seedKey and
the related rotation/deletion cases to use an independent GET /v1/models request
requiring HTTP 200, rather than POST /v1/chat/completions. Keep chat requests in
the secret-swap and revocation assertions only, so readiness failures cannot
mask key-transition checks.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: aa61ea23-b8c7-4503-b1c7-e50f6378ee7d

📥 Commits

Reviewing files that changed from the base of the PR and between 152c57d and f9c10cc.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (12)
  • README.md
  • config.example.yaml
  • crates/aisix-admin/Cargo.toml
  • crates/aisix-admin/src/apikeys_handlers.rs
  • crates/aisix-admin/src/error.rs
  • crates/aisix-admin/src/lib.rs
  • crates/aisix-admin/src/openapi.rs
  • crates/aisix-admin/tests/etcd_integration.rs
  • crates/aisix-core/src/bin/dump-schema.rs
  • crates/aisix-etcd/src/provider.rs
  • schemas/README.md
  • tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts
💤 Files with no reviewable changes (2)
  • crates/aisix-admin/Cargo.toml
  • crates/aisix-admin/src/error.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • config.example.yaml
  • README.md
  • crates/aisix-admin/src/apikeys_handlers.rs
  • crates/aisix-admin/src/lib.rs

Comment threadcrates/aisix-core/src/bin/dump-schema.rs
Comment on lines +304 to 307
if (!etcdReachable || !app || !seed) {
ctx.skip();
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Include otlp in the setup guard.

The guard omits otlp. Add it to the combined setup check so the test does not run with incomplete shared setup.

Based on learnings, E2E cases must preserve if (!etcdReachable || !app || !seed || !otlp) { ctx.skip(); return; }.

Proposed fix
- if (!etcdReachable || !app || !seed) {+ if (!etcdReachable || !app || !seed || !otlp) {
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if(!etcdReachable||!app||!seed){
ctx.skip();
return;
}
if(!etcdReachable||!app||!seed||!otlp){
ctx.skip();
return;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts` around lines 304 - 307,
Update the combined setup guard in the affected E2E test to also check that otlp
is available, skipping and returning when any shared setup value—including
etcdReachable, app, seed, or otlp—is missing.

Source: Learnings

The admin listener keeps its read surface (lists/gets for all 8
resource kinds incl. the former apikeys spelling, models/status,
health, OpenAPI + Scalar, playground, livez/readyz); resources are
managed exclusively through the declarative paths — resources_file
(SIGHUP reload) or direct etcd writes.
BREAKING CHANGE: POST/PUT/DELETE on /admin/v1/* answer 405 with
Allow: GET (409 file-managed rejection included); the api-key rotate
route is gone (404) — rotate declaratively by writing the same
resource id with a new key_hash. The published OpenAPI documents no
write operations; write-only component schemas (ApiKeyRequest,
ApiKeyEntry, ApiKeyRotateResponse, DeleteResponse) are removed.
- router: every /admin/v1/* resource route serves get() only; rotate
routes, file-managed write guard, and RFC 9745 deprecation-header
middleware deleted; 43 write/rotate/uniqueness handler fns removed
- store: ConfigStore is read-only (16 put_*/delete_* methods and
StoreError::ReadOnly removed); EtcdConfigStore keeps reads only;
FileManagedStore::new(snapshot) drops the path param; InMemoryStore
keeps #[cfg(test)] inherent writes for unit-test seeding
- openapi: 24 write ops + rotate path removed from the base document;
no-op deprecation-marker pass deleted; unreachable component
schemas pruned; new gate pins GET-only /admin/v1/* and zero
deprecated marks
- tests: write-path suites deleted; read tests seed via InMemoryStore;
new 405/Allow + rotate-404 contract tests; etcd integration tests
seed via direct etcd writes (the declarative front door) and pin
that refused writes never touch etcd
- e2e: AdminClient write helpers removed (SeedClient is the write
front door); file-resource-source pins the new read-only contract;
apikey-lifecycle rotate coverage became a declarative secret-swap
test; obsolete write-path cases deleted
- docs: README, config.example.yaml, crate module docs updated
Second-auditor findings on the removal PR, all test/doc-level (no
runtime changes):
- rotate-404 tests now GET the rotate URIs too — POST-only 404 could
not distinguish a deleted route from the old handler's unknown-id
404; GET answers 405 on a surviving POST-only route
- e2e: deleting a key's etcd entry revokes an in-use bearer
(fail-closed, unknown-token 401, other keys unaffected) — the
deletion branch had lost its only end-to-end proof
- etcd integration: a2a_agents round-trip + loader coverage (7 -> 8
kinds)
- apikeys read test pins the full PublicApiKey projection
(allowed_tools/disabled/expires_at), not just the id
- OpenAPI descriptions stop claiming ids are generated by the Admin
API and revisions increment on update/rotate
- stale write-path narrative removed: config.example.yaml (dead
docs/api-admin.md link), schemas/README, dump-schema comment,
apikeys_handlers module doc, aisix-etcd provider doc, README RBAC
row + e2e counts
- dead code: AdminError::{BadRequest,Conflict,Schema} variants and
the aisix-mcp/uuid dependencies left over from the write path
Per-comment review triage:
- 7 handler module docs rewritten from CRUD-era text to the surviving
read-only contract; stale rotate/CRUD section comments in the
aisix-admin test module removed
- OpenAPI 'Caller API Keys' tag no longer advertises key rotation
- main.rs: unused match binding -> Some(_); canonical product name;
dropped a comment referencing the removed write-rejection path
- etcd integration: refused-writes test now covers PUT and DELETE
(405 + Allow), not just POST
- sdk-compat e2e: readiness gate switched from the SDK chat path (the
behavior under test) to an independent authenticated GET /v1/models
probe, per the harness gate rules
Cold-audit closeout:
- removed_resource_writes_answer_405_with_allow_get now generates the
FULL matrix (9 route spellings x POST/PUT/DELETE) instead of a
sampled subset — a partial revert (e.g. PUT re-added on one {id}
route) previously passed the whole suite
- last stale write-path narrative: store.rs module doc (read-only
trait), aisix-core schema.rs/models docs (declarative writers, not
'Admin API ... 400'), e2e smoke/seed/app/forward-compat headers no
longer cite deleted characterization or held-back write cases,
openapi.rs base-doc comment names a surviving schema
Round-two auditor findings:
- OpenAPI: the two remaining Entry revision descriptions (McpServer,
A2aAgent) stop describing create/update lifecycle; a regression
assertion now rejects write-lifecycle prose on any documented
revision field
- schemas/README + schema.rs + dump-schema: scope the strict-contract
claim to the in-repo writers (aisix validate, file source) — the
control plane validates its own API schema, a raw direct etcd put
gets no synchronous validation (lenient read only), and
unknown-field rejection applies only where a resource closes fields;
the previous rewrite overclaimed all three
- deletion-revocation e2e: propagation barrier is now a fresh key
seeded after the delete (later etcd revision), so the revocation
assertion is a real assertion instead of a gate poll; a regression
fails the assert, not a 30s timeout
- apikeys projection test seeds rate_limit + allowed_agents and pins
the list entry's projection too; module doc describes PublicApiKey
as an explicit allowlist (not 'minus nothing')
- lifecycle prose: secret-swap invalidates 'as soon as the write
propagates' (not 'immediately'); README e2e counts scoped to
scenario files (183/496)
@moonming
moonmingforce-pushed the feat/remove-admin-write-path branch from 74b0db5 to f43eab2CompareAugust 11, 2026 01:59
@moonming
moonming merged commit b61d270 into mainAug 11, 2026
30 of 32 checks passed
@moonming
moonming deleted the feat/remove-admin-write-path branch August 11, 2026 02:20
Sign up for freeto 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.

2 participants

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(admin)!: remove the Admin API resource write path by moonming · Pull Request #915 · api7/aisix · GitHub
Skip to content

feat(admin)!: remove the Admin API resource write path - #915

Merged
moonming merged 5 commits into
mainfrom
feat/remove-admin-write-path
Aug 11, 2026
Merged

feat(admin)!: remove the Admin API resource write path#915
moonming merged 5 commits into
mainfrom
feat/remove-admin-write-path

Conversation

@moonming

@moonmingmoonming commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Removes the Admin API resource write path. The admin listener (:3001) keeps its read surface — lists/gets for all 8 resource kinds (including the former apikeys spelling), /admin/v1/models/status, /admin/v1/health, OpenAPI + Scalar UI, playground, /livez, /readyz — but resources are now managed exclusively through the declarative paths: a resources_file (resources.yaml, reloaded on SIGHUP) or direct etcd writes.

This is the final step of the deprecation announced in v0.4.0 (RFC 9745 Deprecation headers) and executes the removal scheduled for v0.5.0+ after #848 lifted the write-path-exclusive validations into the canonical schemas so the declarative paths enforce them.

⚠️ Breaking changes

BeforeAfter
POST /admin/v1/<kind>, PUT/DELETE /admin/v1/<kind>/{id} (deprecated, functional)405 with Allow: GET
POST /admin/v1/api_keys/{id}/rotate (and apikeys spelling)404 — the route is gone
File mode: writes rejected with 409 naming the resources file405, same as every other mode
Rotate returned a fresh plaintext keyNo DP-side plaintext rotation. Rotate declaratively: write the same resource id with a new key_hash — the old plaintext stops authenticating as soon as the write propagates (pinned in apikey-lifecycle-e2e)

What to update:

  • Scripts that created/updated/deleted resources via :3001 → write resources.yaml (validate offline with aisix validate --resources <file>, reload with SIGHUP) or write entity-value JSON to etcd at {prefix}/{kind}/{id}.
  • Scripts that rotated caller keys via /rotate → hash the new secret client-side and update the resource's key_hash.
  • The published OpenAPI (/admin/openapi.json) no longer documents write operations; the write-only component schemas (ApiKeyRequest, ApiKeyEntry, ApiKeyRotateResponse, DeleteResponse) are gone from components.schemas.

What changed

Router / handlers — every /admin/v1/* resource route serves get(...) only; both rotate routes deleted; the file-managed write guard and the RFC 9745 deprecation-header middleware deleted (nothing left to mark). 43 write/rotate/uniqueness handler fns removed across the 8 handler modules.

Store layerConfigStore is now a read-only trait (16 put_*/delete_* methods removed, StoreError::ReadOnly gone). EtcdConfigStore keeps only reads (module doc rewritten: resources reach etcd through the declarative paths). FileManagedStore::new(snapshot) drops the path parameter — read-only by construction. InMemoryStore keeps #[cfg(test)] inherent write methods used by unit-test seeding.

OpenAPI — 24 write operations and the rotate path removed from the base document; the no-op write-deprecation marker pass deleted; unreachable component schemas pruned via a reachability walk from paths. New gate test pins that the published reference documents zero non-GET operations under /admin/v1/ and zero deprecated marks anywhere.

Tests — write-path unit tests (CRUD flows, rotate atomicity, write-auth, write-validation) deleted; read tests re-seeded through InMemoryStore; new contract tests pin 405 + Allow: GET on every collection/:id route (auth'd and unauthenticated) and 404 on both rotate spellings. The etcd integration test now seeds via direct etcd_client puts — the path operators actually use.

e2eAdminClient write helpers removed (reads stay; SeedClient is the write front door). file-resource-source-e2e pins the new file-mode contract (reads serve the file, writes 405, rotate 404, unauthenticated write 405 with no file-path leak). apikey-lifecycle-e2e's rotate coverage became a declarative secret-swap test (old plaintext dies immediately, id unchanged). Deleted: seed-vs-admin-characterization-e2e (its own comment scheduled retirement once the seed migration completed), apikey-budget-e2e (tested write-path 400s), the sdk-compat deprecation-header test, and forward-compat's admin strict-write test.

Docs — README, config.example.yaml, crate module docs updated to the read-only story.

Non-goals / follow-ups (filed internally)

  • 3 OpenAPI-parsing validations (mcp validate_spec, HeaderName typing, duplicate tool names) still lack declarative-path enforcement — blocked on a dependency-direction extraction, tracked internally.
  • etcd watch keeps last-good on an invalid PUT while resync drops the row — policy decision tracked internally.

Verification

  • cargo fmt --check, cargo clippy --workspace --all-targets -- -D warnings, cargo test --workspace — all green.
  • Real binary, file mode: boots with resources.yaml, GET list serves the file, POST /admin/v1/models → 405 + Allow: GET, rotate → 404, /admin/openapi.json documents no admin writes.
  • Local e2e stack run before push.

Summary by CodeRabbit

  • Changes
    • The Admin API is now read-only for managed resources.
    • Resource listing and detail views remain available through GET requests.
    • Creation, updates, deletion, and API-key rotation through the Admin API are no longer supported.
    • Unsupported write requests return 405 responses, while rotation routes return 404.
    • Manage resources through resources_file reloads or direct etcd writes.
    • Updated configuration, validation guidance, and end-to-end coverage to reflect the read-only administration model.

CopilotAI balanced review requested due to automatic review settings August 10, 2026 05:09
@coderabbitai

coderabbitaiBot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9acb9ae1-7d4d-42ce-83f0-61de46c03b1a

📥 Commits

Reviewing files that changed from the base of the PR and between f9c10cc and f43eab2.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (25)
  • README.md
  • config.example.yaml
  • crates/aisix-admin/src/a2a_agents_handlers.rs
  • crates/aisix-admin/src/apikeys_handlers.rs
  • crates/aisix-admin/src/cache_policies_handlers.rs
  • crates/aisix-admin/src/guardrails_handlers.rs
  • crates/aisix-admin/src/lib.rs
  • crates/aisix-admin/src/mcp_servers_handlers.rs
  • crates/aisix-admin/src/models_handlers.rs
  • crates/aisix-admin/src/observability_exporters_handlers.rs
  • crates/aisix-admin/src/openapi.rs
  • crates/aisix-admin/src/provider_keys_handlers.rs
  • crates/aisix-admin/src/store.rs
  • crates/aisix-admin/tests/etcd_integration.rs
  • crates/aisix-core/src/bin/dump-schema.rs
  • crates/aisix-core/src/models/mod.rs
  • crates/aisix-core/src/models/schema.rs
  • crates/aisix-server/src/main.rs
  • schemas/README.md
  • tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts
  • tests/e2e/src/cases/config-forward-compat-e2e.test.ts
  • tests/e2e/src/cases/openai-sdk-compat.test.ts
  • tests/e2e/src/cases/smoke.test.ts
  • tests/e2e/src/harness/app.ts
  • tests/e2e/src/harness/seed.ts
💤 Files with no reviewable changes (1)
  • tests/e2e/src/cases/smoke.test.ts
🚧 Files skipped from review as they are similar to previous changes (12)
  • crates/aisix-server/src/main.rs
  • config.example.yaml
  • crates/aisix-admin/src/provider_keys_handlers.rs
  • crates/aisix-admin/src/models_handlers.rs
  • schemas/README.md
  • crates/aisix-core/src/bin/dump-schema.rs
  • tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts
  • tests/e2e/src/cases/config-forward-compat-e2e.test.ts
  • crates/aisix-admin/src/apikeys_handlers.rs
  • README.md
  • crates/aisix-admin/tests/etcd_integration.rs
  • crates/aisix-admin/src/lib.rs

📝 Walkthrough

Walkthrough

The admin listener now exposes read-only resource routes. Resource writes use resources_file reloads, direct etcd writes, or declarative seed helpers. Stores, server wiring, tests, and documentation were updated.

Changes

Read-only admin resource surface

Layer / File(s)Summary
Read-only store contracts
crates/aisix-admin/src/store.rs, crates/aisix-admin/src/etcd_store.rs, crates/aisix-admin/src/file_store.rs, crates/aisix-admin/src/error.rs, crates/aisix-admin/src/state.rs
Store traits and implementations now support resource reads only.
GET-only admin routes
crates/aisix-admin/src/lib.rs, crates/aisix-admin/src/*_handlers.rs
Create, update, delete, and API-key rotation routes were removed.
Read-only server wiring
crates/aisix-server/src/main.rs
The server constructs read-only file-backed admin state and removes the file-managed write guard.
Direct etcd setup and integration coverage
crates/aisix-admin/tests/etcd_integration.rs, crates/aisix-admin/src/etcd_store.rs
Tests seed canonical documents directly in etcd and verify read-only behavior.
Declarative E2E flows and documentation
tests/e2e/src/cases/*, tests/e2e/src/harness/*, README.md, config.example.yaml, schemas/README.md
E2E setup and documentation now use declarative resource-management paths.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
participant EtcdClient
participant AdminAPI
participant ConfigLoader
EtcdClient->>EtcdClient: write canonical resource document
AdminAPI->>EtcdClient: list/get resource
EtcdClient-->>AdminAPI: resource document
ConfigLoader->>EtcdClient: load canonical resource documents
EtcdClient-->>ConfigLoader: configuration data
Loading

Possibly related PRs

  • api7/aisix#792: Related through direct etcd seeding and readiness migration.
  • api7/aisix#800: Related through removal of Admin API writes and E2E harness changes.
  • api7/aisix#848: Related through A2A and MCP admin-handler changes.

Suggested reviewers:jarvis9443


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error, 1 warning)

Check nameStatusExplanationResolution
Security Check❌ ErrorCategory 1: admin GETs serialize ProviderKey.api_key, McpServer.secret, and A2aAgent.secret; PublicApiKey also returns key_hash without redaction.Return redacted DTOs for every secret-bearing resource, omit key_hash and credential fields, and replace backend-detail error responses with generic messages.
E2e Test Quality Review⚠️ WarningE2E quality issue: config-forward-compat-e2e.test.ts test 3 deletes yellowKeyId created only by test 1, with no ordering declaration, creating a hidden test dependency.Make each test self-contained by seeding and cleaning its own rows, or explicitly declare and document sequential execution; also keep route-matrix coverage at the process level if all aliases are contractual.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly and concisely identifies the main breaking change: removing the Admin API resource write path.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/remove-admin-write-path

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/aisix-admin/tests/etcd_integration.rs (1)

383-489: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Add the missing a2a_agents canonical document.

The PR retains eight resource kinds, but writes contains seven entries. It omits a2a_agents. Add a valid A2A agent document, assert stats.accepted == 8, and assert snap.a2a_agents.len() == 1.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/aisix-admin/tests/etcd_integration.rs` around lines 383 - 489, Extend
the writes array with a valid a2a_agents canonical document using the existing
seed flow, then update the accepted-entry assertion from 7 to 8. Add a
corresponding snap.a2a_agents length assertion expecting exactly one loaded
agent, leaving the other resource assertions unchanged.
🧹 Nitpick comments (1)
crates/aisix-admin/src/lib.rs (1)

1131-1198: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Read coverage now spans only two of eight resource kinds. Deleting the write handlers also deleted each handler module's test module. The replacement read tests in lib.rs seed only models and api_keys. The other six kinds — provider_keys, guardrails, cache_policies, observability_exporters, mcp_servers, a2a_agents — have 405 write-refusal coverage but no test proving that GET serves a seeded entry. These handler bodies are hand-written per module, not macro-generated, so a wrong store call or a wrong response shape in one of the six would pass CI.

  • crates/aisix-admin/src/lib.rs#L1131-L1198: extend build_seedable_state with seed helpers for the remaining six kinds, then add list and get-by-id assertions for each, mirroring list_models_returns_seeded_entries and get_model_serves_seeded_entry.
  • crates/aisix-admin/src/a2a_agents_handlers.rs#L12: add a test that list_a2a_agents and get_a2a_agent return a seeded A2aAgent, or confirm the new lib.rs tests cover this module.
  • crates/aisix-admin/src/mcp_servers_handlers.rs#L9: add the same list/get coverage for McpServer, or confirm the new lib.rs tests cover this module.

Note that InMemoryStore currently exposes only put_model and put_apikey as #[cfg(test)] helpers, so seeding the other six kinds requires adding matching helpers in crates/aisix-admin/src/store.rs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/aisix-admin/src/lib.rs` around lines 1131 - 1198, Extend
crates/aisix-admin/src/store.rs with cfg(test) put helpers for provider_keys,
guardrails, cache_policies, observability_exporters, mcp_servers, and
a2a_agents, then update build_seedable_state and the tests in
crates/aisix-admin/src/lib.rs#L1131-L1198 to seed each kind and assert both list
and get-by-id responses, mirroring the model tests. Ensure
crates/aisix-admin/src/a2a_agents_handlers.rs#L12 and
crates/aisix-admin/src/mcp_servers_handlers.rs#L9 are covered by these lib.rs
tests; no direct handler changes are required unless coverage cannot exercise
their list/get functions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/aisix-admin/src/lib.rs`:
- Around line 1200-1230: Remove the stale rotation-coverage comment immediately
before openapi_apikey_schema_excludes_max_budget_usd, and delete the empty
CachePolicy CRUD and Health endpoint section comments. Preserve the
guardrail_payload function, the ObservabilityExporter CRUD comment, and all
surrounding tests and formatting.
In `@crates/aisix-admin/tests/etcd_integration.rs`:
- Around line 317-354: Extend the resource-route assertions in the integration
test to issue both PUT and DELETE requests to /admin/v1/models, asserting each
returns METHOD_NOT_ALLOWED and includes GET in the Allow header. Keep the
existing POST check and final etcd emptiness assertion so all refused write
methods verify that no data is written.
In `@crates/aisix-server/src/main.rs`:
- Around line 898-902: Update the file-source match arm in the admin_store
initialization to bind the second tuple element as Some(_) instead of
Some(path), preserving the existing condition and FileManagedStore construction.
In `@tests/e2e/src/cases/file-resource-source-e2e.test.ts`:
- Around line 179-213: Update the rejected-write assertions in the e2e test to
require the exact Allow header value "GET" instead of merely containing GET,
covering both the authenticated POST/DELETE responses and the unauthenticated
POST response. Preserve the existing status and response-body assertions.
In `@tests/e2e/src/cases/openai-sdk-compat.test.ts`:
- Around line 54-84: After seeding the API key in the test setup, add an
independent readiness poll using the seeded caller credentials against
authenticated GET /v1/models, continuing until it returns 200. Remove the
client.chat.completions.create-based propagation gate and keep that call
exclusively for the SDK chat behavior under test.
---
Outside diff comments:
In `@crates/aisix-admin/tests/etcd_integration.rs`:
- Around line 383-489: Extend the writes array with a valid a2a_agents canonical
document using the existing seed flow, then update the accepted-entry assertion
from 7 to 8. Add a corresponding snap.a2a_agents length assertion expecting
exactly one loaded agent, leaving the other resource assertions unchanged.
---
Nitpick comments:
In `@crates/aisix-admin/src/lib.rs`:
- Around line 1131-1198: Extend crates/aisix-admin/src/store.rs with cfg(test)
put helpers for provider_keys, guardrails, cache_policies,
observability_exporters, mcp_servers, and a2a_agents, then update
build_seedable_state and the tests in crates/aisix-admin/src/lib.rs#L1131-L1198
to seed each kind and assert both list and get-by-id responses, mirroring the
model tests. Ensure crates/aisix-admin/src/a2a_agents_handlers.rs#L12 and
crates/aisix-admin/src/mcp_servers_handlers.rs#L9 are covered by these lib.rs
tests; no direct handler changes are required unless coverage cannot exercise
their list/get functions.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f3cea19a-3b55-4ad2-b83e-bc29037270c5

📥 Commits

Reviewing files that changed from the base of the PR and between b77f1a6 and 152c57d.

📒 Files selected for processing (26)
  • README.md
  • config.example.yaml
  • crates/aisix-admin/src/a2a_agents_handlers.rs
  • crates/aisix-admin/src/apikeys_handlers.rs
  • crates/aisix-admin/src/cache_policies_handlers.rs
  • crates/aisix-admin/src/error.rs
  • crates/aisix-admin/src/etcd_store.rs
  • crates/aisix-admin/src/file_store.rs
  • crates/aisix-admin/src/guardrails_handlers.rs
  • crates/aisix-admin/src/lib.rs
  • crates/aisix-admin/src/mcp_servers_handlers.rs
  • crates/aisix-admin/src/models_handlers.rs
  • crates/aisix-admin/src/observability_exporters_handlers.rs
  • crates/aisix-admin/src/openapi.rs
  • crates/aisix-admin/src/provider_keys_handlers.rs
  • crates/aisix-admin/src/state.rs
  • crates/aisix-admin/src/store.rs
  • crates/aisix-admin/tests/etcd_integration.rs
  • crates/aisix-server/src/main.rs
  • tests/e2e/src/cases/apikey-budget-e2e.test.ts
  • tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts
  • tests/e2e/src/cases/config-forward-compat-e2e.test.ts
  • tests/e2e/src/cases/file-resource-source-e2e.test.ts
  • tests/e2e/src/cases/openai-sdk-compat.test.ts
  • tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts
  • tests/e2e/src/harness/admin.ts
💤 Files with no reviewable changes (8)
  • tests/e2e/src/cases/apikey-budget-e2e.test.ts
  • tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts
  • crates/aisix-admin/src/state.rs
  • crates/aisix-admin/src/observability_exporters_handlers.rs
  • crates/aisix-admin/src/cache_policies_handlers.rs
  • crates/aisix-admin/src/models_handlers.rs
  • crates/aisix-admin/src/guardrails_handlers.rs
  • crates/aisix-admin/src/provider_keys_handlers.rs

Comment threadcrates/aisix-admin/src/lib.rs Outdated
Comment threadcrates/aisix-admin/tests/etcd_integration.rs Outdated
Comment threadcrates/aisix-server/src/main.rs
Comment on lines +179 to +213
expect(postRes.status).toBe(405);
expect(postRes.headers.get("allow")).toContain("GET");
await postRes.text();

// The refused write did not change the resource set.
const relist = await fetch(`${app.adminUrl}/admin/v1/models`, { headers: auth });
expect(((await relist.json()) as unknown[]).length).toBe(2);

// DELETE and rotate are covered by the same guard.
const delRes = await fetch(`${app.adminUrl}/admin/v1/models/any-id`, {
method: "DELETE",
headers: auth,
});
expect(delRes.status).toBe(409);
expect(delRes.status).toBe(405);
expect(delRes.headers.get("allow")).toContain("GET");
await delRes.text();

// The rotate route was removed outright (it was POST-only), so the
// path no longer exists at all.
const rotateRes = await fetch(`${app.adminUrl}/admin/v1/api_keys/any-id/rotate`, {
method: "POST",
headers: auth,
});
expect(rotateRes.status).toBe(409);
expect(rotateRes.status).toBe(404);
await rotateRes.text();

// Auth ordering: an UNAUTHENTICATED write still gets 401, and the
// 401 body must not leak the resources-file path (that detail is
// only for authenticated admins).
// Method routing answers before auth: an unauthenticated write gets
// the same 405 — there is no write endpoint left to protect, and
// the 405 body carries no resources-file detail to leak.
const unauthed = await fetch(`${app.adminUrl}/admin/v1/models`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ display_name: "nope" }),
});
expect(unauthed.status).toBe(401);
const unauthedBody = (await unauthed.json()) as { error_msg: string };
expect(unauthedBody.error_msg).not.toContain(app.resourcesPath!);
expect(unauthed.status).toBe(405);
expect((await unauthed.text())).not.toContain(app.resourcesPath!);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert the exact Allow header value.

Use toBe("GET") for each rejected write. Add the same assertion for the unauthenticated request. toContain("GET") also accepts an invalid value such as GET, POST.

Based on PR objectives, rejected resource writes must return 405 with Allow: GET.

Proposed test update
- expect(postRes.headers.get("allow")).toContain("GET");+ expect(postRes.headers.get("allow")).toBe("GET");
...
- expect(delRes.headers.get("allow")).toContain("GET");+ expect(delRes.headers.get("allow")).toBe("GET");
...
expect(unauthed.status).toBe(405);
+ expect(unauthed.headers.get("allow")).toBe("GET");
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
expect(postRes.status).toBe(405);
expect(postRes.headers.get("allow")).toContain("GET");
awaitpostRes.text();
// The refused write did not change the resource set.
constrelist=awaitfetch(`${app.adminUrl}/admin/v1/models`,{headers: auth});
expect(((awaitrelist.json())asunknown[]).length).toBe(2);
// DELETE and rotate are covered by the same guard.
constdelRes=awaitfetch(`${app.adminUrl}/admin/v1/models/any-id`,{
method: "DELETE",
headers: auth,
});
expect(delRes.status).toBe(409);
expect(delRes.status).toBe(405);
expect(delRes.headers.get("allow")).toContain("GET");
awaitdelRes.text();
// The rotate route was removed outright (it was POST-only), so the
// path no longer exists at all.
constrotateRes=awaitfetch(`${app.adminUrl}/admin/v1/api_keys/any-id/rotate`,{
method: "POST",
headers: auth,
});
expect(rotateRes.status).toBe(409);
expect(rotateRes.status).toBe(404);
awaitrotateRes.text();
// Auth ordering: an UNAUTHENTICATED write still gets 401, and the
// 401 body must not leak the resources-file path (that detail is
// only for authenticated admins).
// Method routing answers before auth: an unauthenticated write gets
// the same 405 — there is no write endpoint left to protect, and
// the 405 body carries no resources-file detail to leak.
constunauthed=awaitfetch(`${app.adminUrl}/admin/v1/models`,{
method: "POST",
headers: {"content-type": "application/json"},
body: JSON.stringify({display_name: "nope"}),
});
expect(unauthed.status).toBe(401);
constunauthedBody=(awaitunauthed.json())as{error_msg: string};
expect(unauthedBody.error_msg).not.toContain(app.resourcesPath!);
expect(unauthed.status).toBe(405);
expect((awaitunauthed.text())).not.toContain(app.resourcesPath!);
expect(postRes.status).toBe(405);
expect(postRes.headers.get("allow")).toBe("GET");
awaitpostRes.text();
// The refused write did not change the resource set.
constrelist=awaitfetch(`${app.adminUrl}/admin/v1/models`,{headers: auth});
expect(((awaitrelist.json())asunknown[]).length).toBe(2);
constdelRes=awaitfetch(`${app.adminUrl}/admin/v1/models/any-id`,{
method: "DELETE",
headers: auth,
});
expect(delRes.status).toBe(405);
expect(delRes.headers.get("allow")).toBe("GET");
awaitdelRes.text();
// The rotate route was removed outright (it was POST-only), so the
// path no longer exists at all.
constrotateRes=awaitfetch(`${app.adminUrl}/admin/v1/api_keys/any-id/rotate`,{
method: "POST",
headers: auth,
});
expect(rotateRes.status).toBe(404);
awaitrotateRes.text();
// Method routing answers before auth: an unauthenticated write gets
// the same 405 — there is no write endpoint left to protect, and
// the 405 body carries no resources-file detail to leak.
constunauthed=awaitfetch(`${app.adminUrl}/admin/v1/models`,{
method: "POST",
headers: {"content-type": "application/json"},
body: JSON.stringify({display_name: "nope"}),
});
expect(unauthed.status).toBe(405);
expect(unauthed.headers.get("allow")).toBe("GET");
expect((awaitunauthed.text())).not.toContain(app.resourcesPath!);
🧰 Tools
🪛 ast-grep (0.45.0)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { execFile } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/e2e/src/cases/file-resource-source-e2e.test.ts` around lines 179 - 213,
Update the rejected-write assertions in the e2e test to require the exact Allow
header value "GET" instead of merely containing GET, covering both the
authenticated POST/DELETE responses and the unauthenticated POST response.
Preserve the existing status and response-body assertions.

Comment threadtests/e2e/src/cases/openai-sdk-compat.test.ts

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Removes Admin API resource writes, leaving read-only resource endpoints and moving management to declarative file or etcd paths.

Changes:

  • Removes write handlers, routes, store operations, rotation, and deprecation middleware.
  • Updates OpenAPI, documentation, and tests for the read-only contract.
  • Migrates test setup to direct etcd seeding.

Reviewed changes

Copilot reviewed 25 out of 26 changed files in this pull request and generated 12 comments.

Show a summary per file
FileDescription
README.mdDocuments the read-only Admin API.
config.example.yamlClarifies declarative resource management.
crates/aisix-server/src/main.rsWires read-only admin stores.
crates/aisix-admin/src/lib.rsRemoves write routes and middleware.
crates/aisix-admin/src/openapi.rsRemoves write operations and schemas.
crates/aisix-admin/src/store.rsMakes ConfigStore read-only.
crates/aisix-admin/src/state.rsRemoves file-write guard state.
crates/aisix-admin/src/error.rsRemoves write-related errors.
crates/aisix-admin/src/file_store.rsRetains snapshot reads only.
crates/aisix-admin/src/etcd_store.rsRetains etcd reads only.
crates/aisix-admin/src/models_handlers.rsRemoves model writes.
crates/aisix-admin/src/apikeys_handlers.rsRemoves API-key writes and rotation.
crates/aisix-admin/src/provider_keys_handlers.rsRemoves provider-key writes.
crates/aisix-admin/src/guardrails_handlers.rsRemoves guardrail writes.
crates/aisix-admin/src/cache_policies_handlers.rsRemoves cache-policy writes.
crates/aisix-admin/src/observability_exporters_handlers.rsRemoves exporter writes.
crates/aisix-admin/src/mcp_servers_handlers.rsRemoves MCP-server writes.
crates/aisix-admin/src/a2a_agents_handlers.rsRemoves A2A-agent writes.
crates/aisix-admin/tests/etcd_integration.rsTests direct-etcd writes and admin reads.
tests/e2e/src/harness/admin.tsRemoves Admin API write helpers.
tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.tsRemoves obsolete path comparison tests.
tests/e2e/src/cases/openai-sdk-compat.test.tsMigrates setup to etcd seeding.
tests/e2e/src/cases/file-resource-source-e2e.test.tsTests file-mode read-only behavior.
tests/e2e/src/cases/config-forward-compat-e2e.test.tsRemoves Admin write validation coverage.
tests/e2e/src/cases/apikey-lifecycle-e2e.test.tsReplaces rotation with declarative secret swapping.
tests/e2e/src/cases/apikey-budget-e2e.test.tsRemoves obsolete write-path validation test.
Suppressed comments (1)

crates/aisix-admin/src/mcp_servers_handlers.rs:9

  • This removal also eliminates the only production call to aisix_mcp::validate_spec. The file and etcd loaders only run the core JSON Schema, which does not reject specs with zero generatable operations or colliding sanitized tool names, so the newly exclusive declarative paths accept configurations the former write API rejected. Add the semantic validation to both loaders before removing this path.
use aisix_core::McpServer;

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment threadcrates/aisix-server/src/main.rs Outdated
use serde::{Deserialize, Serialize};
use serde_json::Value;
use uuid::Uuid;
use serde::Serialize;
Comment threadcrates/aisix-server/src/main.rs Outdated
Comment on lines 9 to 10
//! replaces the `key` field with a freshly-generated `sk-*` value and
//! bumps the revision, invalidating the old credential.
Comment on lines 8 to 9
//!
//! ids are UUID v4s generated on POST; PUT preserves the existing id.
@@ -38,7 +38,7 @@ const OPENAPI_JSON_BASE: &str = r##"{
"info": {
"title": "AISIX Admin API",
"version": "dev",
"description": "The AISIX Admin API configures an open-source AISIX gateway at runtime. Use it when you operate the gateway directly and need to create or update models, caller API keys, provider credentials, guardrails, cache policies, and observability exporters.\n\nThe write endpoints (POST, PUT, DELETE) are deprecated in favor of declarative configuration: load resources from a `resources_file` (`resources.yaml`) or write them to etcd directly. Write endpoints remain functional, and every mutating response carries a `Deprecation` header (RFC 9745) plus a `Link` header with `rel=\"deprecation\"` pointing at the migration documentation. Read endpoints are not deprecated.\n\nGateways connected to AISIX Cloud do not expose this listener. Configure them through AISIX Cloud."
"description": "The AISIX Admin API is the read-only operational surface of an open-source AISIX gateway: list and inspect the loaded models, caller API keys, provider credentials, guardrails, MCP servers, A2A agents, cache policies, and observability exporters, check per-model upstream health, and drive the playground.\n\nResource write endpoints were removed in favor of declarative configuration: declare resources in a `resources_file` (`resources.yaml`) and reload with SIGHUP, or write them to etcd directly. See the resources file reference at https://docs.api7.ai/ai-gateway/reference/resources-file.\n\nGateways connected to AISIX Cloud do not expose this listener. Configure them through AISIX Cloud."
Comment on lines 4 to 5
//! validate against the JSON schema, reject duplicate names (409),
//! generate a uuid v4 on POST, bump revision on PUT.
Comment on lines 4 to 5
//! validate against the JSON schema, reject duplicate names (409),
//! generate a uuid v4 on POST, bump revision on PUT.
Comment on lines 5 to 6
//! PUT. Additionally rejects a name containing the reserved tool-namespace
//! separator `__`, since the name prefixes the server's tools.
Comment on lines 8 to 9
//! every configuration path rejects an incomplete credential set; the checks
//! below are defense in depth.
@moonmingmoonming self-assigned this Aug 10, 2026
@moonming

Copy link
Copy Markdown
CollaboratorAuthor

Review triage — every inline comment dispositioned; fixes landed in f9c10cc and e34694b.

Fixed

  • 7 handler module docs rewritten to the read-only contract; stale rotate/CRUD section comments removed (e34694b)
  • OpenAPI Caller API Keys tag no longer says "and key rotation"; path/Entry descriptions no longer claim ids are generated by the Admin API or that update/rotate bumps revisions (f9c10cc, e34694b)
  • main.rs: unused Some(path) binding → Some(_); canonical product name; removed a comment referencing write rejections (e34694b)
  • etcd integration: refused-writes test now covers PUT and DELETE with Allow assertions, and the rotate-absence checks GET the rotate URIs too — POST-only 404 couldn't distinguish a deleted route from the old handler's unknown-id 404 (f9c10cc, e34694b)
  • sdk-compat e2e: readiness gate switched from the SDK chat path to an independent authenticated GET /v1/models probe per the harness gate rules (e34694b)
  • config.example.yaml: dead docs/api-admin.md link and Admin-API management claims replaced with the declarative sources (f9c10cc)

Not adopted, with reasons

  • Assert Allow with toBe("GET"): the real binary answers Allow: GET,HEAD (axum's MethodRouter advertises HEAD alongside GET), so an exact "GET" match fails against actual behavior. toContain("GET") plus the 405 status is the contract; the 405 itself already proves no write method is routed.
  • Duplicate key_hash uniqueness on the direct-etcd path (assert_unique_key removal): real gap, but pre-existing — the admin-side check never guarded direct etcd writes, which existed before this PR and are used by the control plane (which enforces uniqueness org-side) and the file source (which has its own per-kind identity check). Filed internally for loader-side conflict detection with /status/config visibility; tracked as a follow-up rather than blocking the removal.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts (1)

276-301: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Use an independent readiness check for key setup.

seedKey gates propagation with a POST /v1/chat/completions request. The rotation and deletion flows also use the chat authorization path for their assertions. A chat failure can stop the test before it checks the key transition.

Seed the caller keys, then verify readiness with GET /v1/models and require 200. Keep chat requests for the actual rotation and revocation assertions.

As per coding guidelines, E2E readiness gates must use an independent condition, and caller API keys must be checked with GET /v1/models returning 200.

Also applies to: 311-312

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts` around lines 276 - 301,
Update the key setup readiness flow around seedKey and the related
rotation/deletion cases to use an independent GET /v1/models request requiring
HTTP 200, rather than POST /v1/chat/completions. Keep chat requests in the
secret-swap and revocation assertions only, so readiness failures cannot mask
key-transition checks.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/aisix-core/src/bin/dump-schema.rs`:
- Around line 44-47: Update the comment near the STRICT shape documentation to
qualify unknown-field rejection as applying only where the resource schema is
closed. Preserve the existing distinction between declarative write contracts
and lenient etcd reads, while acknowledging resource-specific exceptions such as
open fields and custom guardrail validation documented in the schema guidance.
In `@tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts`:
- Around line 304-307: Update the combined setup guard in the affected E2E test
to also check that otlp is available, skipping and returning when any shared
setup value—including etcdReachable, app, seed, or otlp—is missing.
---
Outside diff comments:
In `@tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts`:
- Around line 276-301: Update the key setup readiness flow around seedKey and
the related rotation/deletion cases to use an independent GET /v1/models request
requiring HTTP 200, rather than POST /v1/chat/completions. Keep chat requests in
the secret-swap and revocation assertions only, so readiness failures cannot
mask key-transition checks.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: aa61ea23-b8c7-4503-b1c7-e50f6378ee7d

📥 Commits

Reviewing files that changed from the base of the PR and between 152c57d and f9c10cc.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (12)
  • README.md
  • config.example.yaml
  • crates/aisix-admin/Cargo.toml
  • crates/aisix-admin/src/apikeys_handlers.rs
  • crates/aisix-admin/src/error.rs
  • crates/aisix-admin/src/lib.rs
  • crates/aisix-admin/src/openapi.rs
  • crates/aisix-admin/tests/etcd_integration.rs
  • crates/aisix-core/src/bin/dump-schema.rs
  • crates/aisix-etcd/src/provider.rs
  • schemas/README.md
  • tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts
💤 Files with no reviewable changes (2)
  • crates/aisix-admin/Cargo.toml
  • crates/aisix-admin/src/error.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • config.example.yaml
  • README.md
  • crates/aisix-admin/src/apikeys_handlers.rs
  • crates/aisix-admin/src/lib.rs

Comment threadcrates/aisix-core/src/bin/dump-schema.rs
Comment on lines +304 to 307
if (!etcdReachable || !app || !seed) {
ctx.skip();
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Include otlp in the setup guard.

The guard omits otlp. Add it to the combined setup check so the test does not run with incomplete shared setup.

Based on learnings, E2E cases must preserve if (!etcdReachable || !app || !seed || !otlp) { ctx.skip(); return; }.

Proposed fix
- if (!etcdReachable || !app || !seed) {+ if (!etcdReachable || !app || !seed || !otlp) {
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if(!etcdReachable||!app||!seed){
ctx.skip();
return;
}
if(!etcdReachable||!app||!seed||!otlp){
ctx.skip();
return;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts` around lines 304 - 307,
Update the combined setup guard in the affected E2E test to also check that otlp
is available, skipping and returning when any shared setup value—including
etcdReachable, app, seed, or otlp—is missing.

Source: Learnings

The admin listener keeps its read surface (lists/gets for all 8
resource kinds incl. the former apikeys spelling, models/status,
health, OpenAPI + Scalar, playground, livez/readyz); resources are
managed exclusively through the declarative paths — resources_file
(SIGHUP reload) or direct etcd writes.
BREAKING CHANGE: POST/PUT/DELETE on /admin/v1/* answer 405 with
Allow: GET (409 file-managed rejection included); the api-key rotate
route is gone (404) — rotate declaratively by writing the same
resource id with a new key_hash. The published OpenAPI documents no
write operations; write-only component schemas (ApiKeyRequest,
ApiKeyEntry, ApiKeyRotateResponse, DeleteResponse) are removed.
- router: every /admin/v1/* resource route serves get() only; rotate
routes, file-managed write guard, and RFC 9745 deprecation-header
middleware deleted; 43 write/rotate/uniqueness handler fns removed
- store: ConfigStore is read-only (16 put_*/delete_* methods and
StoreError::ReadOnly removed); EtcdConfigStore keeps reads only;
FileManagedStore::new(snapshot) drops the path param; InMemoryStore
keeps #[cfg(test)] inherent writes for unit-test seeding
- openapi: 24 write ops + rotate path removed from the base document;
no-op deprecation-marker pass deleted; unreachable component
schemas pruned; new gate pins GET-only /admin/v1/* and zero
deprecated marks
- tests: write-path suites deleted; read tests seed via InMemoryStore;
new 405/Allow + rotate-404 contract tests; etcd integration tests
seed via direct etcd writes (the declarative front door) and pin
that refused writes never touch etcd
- e2e: AdminClient write helpers removed (SeedClient is the write
front door); file-resource-source pins the new read-only contract;
apikey-lifecycle rotate coverage became a declarative secret-swap
test; obsolete write-path cases deleted
- docs: README, config.example.yaml, crate module docs updated
Second-auditor findings on the removal PR, all test/doc-level (no
runtime changes):
- rotate-404 tests now GET the rotate URIs too — POST-only 404 could
not distinguish a deleted route from the old handler's unknown-id
404; GET answers 405 on a surviving POST-only route
- e2e: deleting a key's etcd entry revokes an in-use bearer
(fail-closed, unknown-token 401, other keys unaffected) — the
deletion branch had lost its only end-to-end proof
- etcd integration: a2a_agents round-trip + loader coverage (7 -> 8
kinds)
- apikeys read test pins the full PublicApiKey projection
(allowed_tools/disabled/expires_at), not just the id
- OpenAPI descriptions stop claiming ids are generated by the Admin
API and revisions increment on update/rotate
- stale write-path narrative removed: config.example.yaml (dead
docs/api-admin.md link), schemas/README, dump-schema comment,
apikeys_handlers module doc, aisix-etcd provider doc, README RBAC
row + e2e counts
- dead code: AdminError::{BadRequest,Conflict,Schema} variants and
the aisix-mcp/uuid dependencies left over from the write path
Per-comment review triage:
- 7 handler module docs rewritten from CRUD-era text to the surviving
read-only contract; stale rotate/CRUD section comments in the
aisix-admin test module removed
- OpenAPI 'Caller API Keys' tag no longer advertises key rotation
- main.rs: unused match binding -> Some(_); canonical product name;
dropped a comment referencing the removed write-rejection path
- etcd integration: refused-writes test now covers PUT and DELETE
(405 + Allow), not just POST
- sdk-compat e2e: readiness gate switched from the SDK chat path (the
behavior under test) to an independent authenticated GET /v1/models
probe, per the harness gate rules
Cold-audit closeout:
- removed_resource_writes_answer_405_with_allow_get now generates the
FULL matrix (9 route spellings x POST/PUT/DELETE) instead of a
sampled subset — a partial revert (e.g. PUT re-added on one {id}
route) previously passed the whole suite
- last stale write-path narrative: store.rs module doc (read-only
trait), aisix-core schema.rs/models docs (declarative writers, not
'Admin API ... 400'), e2e smoke/seed/app/forward-compat headers no
longer cite deleted characterization or held-back write cases,
openapi.rs base-doc comment names a surviving schema
Round-two auditor findings:
- OpenAPI: the two remaining Entry revision descriptions (McpServer,
A2aAgent) stop describing create/update lifecycle; a regression
assertion now rejects write-lifecycle prose on any documented
revision field
- schemas/README + schema.rs + dump-schema: scope the strict-contract
claim to the in-repo writers (aisix validate, file source) — the
control plane validates its own API schema, a raw direct etcd put
gets no synchronous validation (lenient read only), and
unknown-field rejection applies only where a resource closes fields;
the previous rewrite overclaimed all three
- deletion-revocation e2e: propagation barrier is now a fresh key
seeded after the delete (later etcd revision), so the revocation
assertion is a real assertion instead of a gate poll; a regression
fails the assert, not a 30s timeout
- apikeys projection test seeds rate_limit + allowed_agents and pins
the list entry's projection too; module doc describes PublicApiKey
as an explicit allowlist (not 'minus nothing')
- lifecycle prose: secret-swap invalidates 'as soon as the write
propagates' (not 'immediately'); README e2e counts scoped to
scenario files (183/496)
@moonming
moonmingforce-pushed the feat/remove-admin-write-path branch from 74b0db5 to f43eab2CompareAugust 11, 2026 01:59
@moonming
moonming merged commit b61d270 into mainAug 11, 2026
30 of 32 checks passed
@moonming
moonming deleted the feat/remove-admin-write-path branch August 11, 2026 02:20
Sign up for freeto 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.

2 participants

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); feat(admin)!: remove the Admin API resource write path by moonming · Pull Request #915 · api7/aisix · GitHub
Skip to content

feat(admin)!: remove the Admin API resource write path - #915

Merged
moonming merged 5 commits into
mainfrom
feat/remove-admin-write-path
Aug 11, 2026
Merged

feat(admin)!: remove the Admin API resource write path#915
moonming merged 5 commits into
mainfrom
feat/remove-admin-write-path

Conversation

@moonming

@moonmingmoonming commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Removes the Admin API resource write path. The admin listener (:3001) keeps its read surface — lists/gets for all 8 resource kinds (including the former apikeys spelling), /admin/v1/models/status, /admin/v1/health, OpenAPI + Scalar UI, playground, /livez, /readyz — but resources are now managed exclusively through the declarative paths: a resources_file (resources.yaml, reloaded on SIGHUP) or direct etcd writes.

This is the final step of the deprecation announced in v0.4.0 (RFC 9745 Deprecation headers) and executes the removal scheduled for v0.5.0+ after #848 lifted the write-path-exclusive validations into the canonical schemas so the declarative paths enforce them.

⚠️ Breaking changes

BeforeAfter
POST /admin/v1/<kind>, PUT/DELETE /admin/v1/<kind>/{id} (deprecated, functional)405 with Allow: GET
POST /admin/v1/api_keys/{id}/rotate (and apikeys spelling)404 — the route is gone
File mode: writes rejected with 409 naming the resources file405, same as every other mode
Rotate returned a fresh plaintext keyNo DP-side plaintext rotation. Rotate declaratively: write the same resource id with a new key_hash — the old plaintext stops authenticating as soon as the write propagates (pinned in apikey-lifecycle-e2e)

What to update:

  • Scripts that created/updated/deleted resources via :3001 → write resources.yaml (validate offline with aisix validate --resources <file>, reload with SIGHUP) or write entity-value JSON to etcd at {prefix}/{kind}/{id}.
  • Scripts that rotated caller keys via /rotate → hash the new secret client-side and update the resource's key_hash.
  • The published OpenAPI (/admin/openapi.json) no longer documents write operations; the write-only component schemas (ApiKeyRequest, ApiKeyEntry, ApiKeyRotateResponse, DeleteResponse) are gone from components.schemas.

What changed

Router / handlers — every /admin/v1/* resource route serves get(...) only; both rotate routes deleted; the file-managed write guard and the RFC 9745 deprecation-header middleware deleted (nothing left to mark). 43 write/rotate/uniqueness handler fns removed across the 8 handler modules.

Store layerConfigStore is now a read-only trait (16 put_*/delete_* methods removed, StoreError::ReadOnly gone). EtcdConfigStore keeps only reads (module doc rewritten: resources reach etcd through the declarative paths). FileManagedStore::new(snapshot) drops the path parameter — read-only by construction. InMemoryStore keeps #[cfg(test)] inherent write methods used by unit-test seeding.

OpenAPI — 24 write operations and the rotate path removed from the base document; the no-op write-deprecation marker pass deleted; unreachable component schemas pruned via a reachability walk from paths. New gate test pins that the published reference documents zero non-GET operations under /admin/v1/ and zero deprecated marks anywhere.

Tests — write-path unit tests (CRUD flows, rotate atomicity, write-auth, write-validation) deleted; read tests re-seeded through InMemoryStore; new contract tests pin 405 + Allow: GET on every collection/:id route (auth'd and unauthenticated) and 404 on both rotate spellings. The etcd integration test now seeds via direct etcd_client puts — the path operators actually use.

e2eAdminClient write helpers removed (reads stay; SeedClient is the write front door). file-resource-source-e2e pins the new file-mode contract (reads serve the file, writes 405, rotate 404, unauthenticated write 405 with no file-path leak). apikey-lifecycle-e2e's rotate coverage became a declarative secret-swap test (old plaintext dies immediately, id unchanged). Deleted: seed-vs-admin-characterization-e2e (its own comment scheduled retirement once the seed migration completed), apikey-budget-e2e (tested write-path 400s), the sdk-compat deprecation-header test, and forward-compat's admin strict-write test.

Docs — README, config.example.yaml, crate module docs updated to the read-only story.

Non-goals / follow-ups (filed internally)

  • 3 OpenAPI-parsing validations (mcp validate_spec, HeaderName typing, duplicate tool names) still lack declarative-path enforcement — blocked on a dependency-direction extraction, tracked internally.
  • etcd watch keeps last-good on an invalid PUT while resync drops the row — policy decision tracked internally.

Verification

  • cargo fmt --check, cargo clippy --workspace --all-targets -- -D warnings, cargo test --workspace — all green.
  • Real binary, file mode: boots with resources.yaml, GET list serves the file, POST /admin/v1/models → 405 + Allow: GET, rotate → 404, /admin/openapi.json documents no admin writes.
  • Local e2e stack run before push.

Summary by CodeRabbit

  • Changes
    • The Admin API is now read-only for managed resources.
    • Resource listing and detail views remain available through GET requests.
    • Creation, updates, deletion, and API-key rotation through the Admin API are no longer supported.
    • Unsupported write requests return 405 responses, while rotation routes return 404.
    • Manage resources through resources_file reloads or direct etcd writes.
    • Updated configuration, validation guidance, and end-to-end coverage to reflect the read-only administration model.

CopilotAI balanced review requested due to automatic review settings August 10, 2026 05:09
@coderabbitai

coderabbitaiBot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9acb9ae1-7d4d-42ce-83f0-61de46c03b1a

📥 Commits

Reviewing files that changed from the base of the PR and between f9c10cc and f43eab2.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (25)
  • README.md
  • config.example.yaml
  • crates/aisix-admin/src/a2a_agents_handlers.rs
  • crates/aisix-admin/src/apikeys_handlers.rs
  • crates/aisix-admin/src/cache_policies_handlers.rs
  • crates/aisix-admin/src/guardrails_handlers.rs
  • crates/aisix-admin/src/lib.rs
  • crates/aisix-admin/src/mcp_servers_handlers.rs
  • crates/aisix-admin/src/models_handlers.rs
  • crates/aisix-admin/src/observability_exporters_handlers.rs
  • crates/aisix-admin/src/openapi.rs
  • crates/aisix-admin/src/provider_keys_handlers.rs
  • crates/aisix-admin/src/store.rs
  • crates/aisix-admin/tests/etcd_integration.rs
  • crates/aisix-core/src/bin/dump-schema.rs
  • crates/aisix-core/src/models/mod.rs
  • crates/aisix-core/src/models/schema.rs
  • crates/aisix-server/src/main.rs
  • schemas/README.md
  • tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts
  • tests/e2e/src/cases/config-forward-compat-e2e.test.ts
  • tests/e2e/src/cases/openai-sdk-compat.test.ts
  • tests/e2e/src/cases/smoke.test.ts
  • tests/e2e/src/harness/app.ts
  • tests/e2e/src/harness/seed.ts
💤 Files with no reviewable changes (1)
  • tests/e2e/src/cases/smoke.test.ts
🚧 Files skipped from review as they are similar to previous changes (12)
  • crates/aisix-server/src/main.rs
  • config.example.yaml
  • crates/aisix-admin/src/provider_keys_handlers.rs
  • crates/aisix-admin/src/models_handlers.rs
  • schemas/README.md
  • crates/aisix-core/src/bin/dump-schema.rs
  • tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts
  • tests/e2e/src/cases/config-forward-compat-e2e.test.ts
  • crates/aisix-admin/src/apikeys_handlers.rs
  • README.md
  • crates/aisix-admin/tests/etcd_integration.rs
  • crates/aisix-admin/src/lib.rs

📝 Walkthrough

Walkthrough

The admin listener now exposes read-only resource routes. Resource writes use resources_file reloads, direct etcd writes, or declarative seed helpers. Stores, server wiring, tests, and documentation were updated.

Changes

Read-only admin resource surface

Layer / File(s)Summary
Read-only store contracts
crates/aisix-admin/src/store.rs, crates/aisix-admin/src/etcd_store.rs, crates/aisix-admin/src/file_store.rs, crates/aisix-admin/src/error.rs, crates/aisix-admin/src/state.rs
Store traits and implementations now support resource reads only.
GET-only admin routes
crates/aisix-admin/src/lib.rs, crates/aisix-admin/src/*_handlers.rs
Create, update, delete, and API-key rotation routes were removed.
Read-only server wiring
crates/aisix-server/src/main.rs
The server constructs read-only file-backed admin state and removes the file-managed write guard.
Direct etcd setup and integration coverage
crates/aisix-admin/tests/etcd_integration.rs, crates/aisix-admin/src/etcd_store.rs
Tests seed canonical documents directly in etcd and verify read-only behavior.
Declarative E2E flows and documentation
tests/e2e/src/cases/*, tests/e2e/src/harness/*, README.md, config.example.yaml, schemas/README.md
E2E setup and documentation now use declarative resource-management paths.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
participant EtcdClient
participant AdminAPI
participant ConfigLoader
EtcdClient->>EtcdClient: write canonical resource document
AdminAPI->>EtcdClient: list/get resource
EtcdClient-->>AdminAPI: resource document
ConfigLoader->>EtcdClient: load canonical resource documents
EtcdClient-->>ConfigLoader: configuration data
Loading

Possibly related PRs

  • api7/aisix#792: Related through direct etcd seeding and readiness migration.
  • api7/aisix#800: Related through removal of Admin API writes and E2E harness changes.
  • api7/aisix#848: Related through A2A and MCP admin-handler changes.

Suggested reviewers:jarvis9443


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error, 1 warning)

Check nameStatusExplanationResolution
Security Check❌ ErrorCategory 1: admin GETs serialize ProviderKey.api_key, McpServer.secret, and A2aAgent.secret; PublicApiKey also returns key_hash without redaction.Return redacted DTOs for every secret-bearing resource, omit key_hash and credential fields, and replace backend-detail error responses with generic messages.
E2e Test Quality Review⚠️ WarningE2E quality issue: config-forward-compat-e2e.test.ts test 3 deletes yellowKeyId created only by test 1, with no ordering declaration, creating a hidden test dependency.Make each test self-contained by seeding and cleaning its own rows, or explicitly declare and document sequential execution; also keep route-matrix coverage at the process level if all aliases are contractual.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly and concisely identifies the main breaking change: removing the Admin API resource write path.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/remove-admin-write-path

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/aisix-admin/tests/etcd_integration.rs (1)

383-489: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Add the missing a2a_agents canonical document.

The PR retains eight resource kinds, but writes contains seven entries. It omits a2a_agents. Add a valid A2A agent document, assert stats.accepted == 8, and assert snap.a2a_agents.len() == 1.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/aisix-admin/tests/etcd_integration.rs` around lines 383 - 489, Extend
the writes array with a valid a2a_agents canonical document using the existing
seed flow, then update the accepted-entry assertion from 7 to 8. Add a
corresponding snap.a2a_agents length assertion expecting exactly one loaded
agent, leaving the other resource assertions unchanged.
🧹 Nitpick comments (1)
crates/aisix-admin/src/lib.rs (1)

1131-1198: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Read coverage now spans only two of eight resource kinds. Deleting the write handlers also deleted each handler module's test module. The replacement read tests in lib.rs seed only models and api_keys. The other six kinds — provider_keys, guardrails, cache_policies, observability_exporters, mcp_servers, a2a_agents — have 405 write-refusal coverage but no test proving that GET serves a seeded entry. These handler bodies are hand-written per module, not macro-generated, so a wrong store call or a wrong response shape in one of the six would pass CI.

  • crates/aisix-admin/src/lib.rs#L1131-L1198: extend build_seedable_state with seed helpers for the remaining six kinds, then add list and get-by-id assertions for each, mirroring list_models_returns_seeded_entries and get_model_serves_seeded_entry.
  • crates/aisix-admin/src/a2a_agents_handlers.rs#L12: add a test that list_a2a_agents and get_a2a_agent return a seeded A2aAgent, or confirm the new lib.rs tests cover this module.
  • crates/aisix-admin/src/mcp_servers_handlers.rs#L9: add the same list/get coverage for McpServer, or confirm the new lib.rs tests cover this module.

Note that InMemoryStore currently exposes only put_model and put_apikey as #[cfg(test)] helpers, so seeding the other six kinds requires adding matching helpers in crates/aisix-admin/src/store.rs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/aisix-admin/src/lib.rs` around lines 1131 - 1198, Extend
crates/aisix-admin/src/store.rs with cfg(test) put helpers for provider_keys,
guardrails, cache_policies, observability_exporters, mcp_servers, and
a2a_agents, then update build_seedable_state and the tests in
crates/aisix-admin/src/lib.rs#L1131-L1198 to seed each kind and assert both list
and get-by-id responses, mirroring the model tests. Ensure
crates/aisix-admin/src/a2a_agents_handlers.rs#L12 and
crates/aisix-admin/src/mcp_servers_handlers.rs#L9 are covered by these lib.rs
tests; no direct handler changes are required unless coverage cannot exercise
their list/get functions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/aisix-admin/src/lib.rs`:
- Around line 1200-1230: Remove the stale rotation-coverage comment immediately
before openapi_apikey_schema_excludes_max_budget_usd, and delete the empty
CachePolicy CRUD and Health endpoint section comments. Preserve the
guardrail_payload function, the ObservabilityExporter CRUD comment, and all
surrounding tests and formatting.
In `@crates/aisix-admin/tests/etcd_integration.rs`:
- Around line 317-354: Extend the resource-route assertions in the integration
test to issue both PUT and DELETE requests to /admin/v1/models, asserting each
returns METHOD_NOT_ALLOWED and includes GET in the Allow header. Keep the
existing POST check and final etcd emptiness assertion so all refused write
methods verify that no data is written.
In `@crates/aisix-server/src/main.rs`:
- Around line 898-902: Update the file-source match arm in the admin_store
initialization to bind the second tuple element as Some(_) instead of
Some(path), preserving the existing condition and FileManagedStore construction.
In `@tests/e2e/src/cases/file-resource-source-e2e.test.ts`:
- Around line 179-213: Update the rejected-write assertions in the e2e test to
require the exact Allow header value "GET" instead of merely containing GET,
covering both the authenticated POST/DELETE responses and the unauthenticated
POST response. Preserve the existing status and response-body assertions.
In `@tests/e2e/src/cases/openai-sdk-compat.test.ts`:
- Around line 54-84: After seeding the API key in the test setup, add an
independent readiness poll using the seeded caller credentials against
authenticated GET /v1/models, continuing until it returns 200. Remove the
client.chat.completions.create-based propagation gate and keep that call
exclusively for the SDK chat behavior under test.
---
Outside diff comments:
In `@crates/aisix-admin/tests/etcd_integration.rs`:
- Around line 383-489: Extend the writes array with a valid a2a_agents canonical
document using the existing seed flow, then update the accepted-entry assertion
from 7 to 8. Add a corresponding snap.a2a_agents length assertion expecting
exactly one loaded agent, leaving the other resource assertions unchanged.
---
Nitpick comments:
In `@crates/aisix-admin/src/lib.rs`:
- Around line 1131-1198: Extend crates/aisix-admin/src/store.rs with cfg(test)
put helpers for provider_keys, guardrails, cache_policies,
observability_exporters, mcp_servers, and a2a_agents, then update
build_seedable_state and the tests in crates/aisix-admin/src/lib.rs#L1131-L1198
to seed each kind and assert both list and get-by-id responses, mirroring the
model tests. Ensure crates/aisix-admin/src/a2a_agents_handlers.rs#L12 and
crates/aisix-admin/src/mcp_servers_handlers.rs#L9 are covered by these lib.rs
tests; no direct handler changes are required unless coverage cannot exercise
their list/get functions.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f3cea19a-3b55-4ad2-b83e-bc29037270c5

📥 Commits

Reviewing files that changed from the base of the PR and between b77f1a6 and 152c57d.

📒 Files selected for processing (26)
  • README.md
  • config.example.yaml
  • crates/aisix-admin/src/a2a_agents_handlers.rs
  • crates/aisix-admin/src/apikeys_handlers.rs
  • crates/aisix-admin/src/cache_policies_handlers.rs
  • crates/aisix-admin/src/error.rs
  • crates/aisix-admin/src/etcd_store.rs
  • crates/aisix-admin/src/file_store.rs
  • crates/aisix-admin/src/guardrails_handlers.rs
  • crates/aisix-admin/src/lib.rs
  • crates/aisix-admin/src/mcp_servers_handlers.rs
  • crates/aisix-admin/src/models_handlers.rs
  • crates/aisix-admin/src/observability_exporters_handlers.rs
  • crates/aisix-admin/src/openapi.rs
  • crates/aisix-admin/src/provider_keys_handlers.rs
  • crates/aisix-admin/src/state.rs
  • crates/aisix-admin/src/store.rs
  • crates/aisix-admin/tests/etcd_integration.rs
  • crates/aisix-server/src/main.rs
  • tests/e2e/src/cases/apikey-budget-e2e.test.ts
  • tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts
  • tests/e2e/src/cases/config-forward-compat-e2e.test.ts
  • tests/e2e/src/cases/file-resource-source-e2e.test.ts
  • tests/e2e/src/cases/openai-sdk-compat.test.ts
  • tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts
  • tests/e2e/src/harness/admin.ts
💤 Files with no reviewable changes (8)
  • tests/e2e/src/cases/apikey-budget-e2e.test.ts
  • tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts
  • crates/aisix-admin/src/state.rs
  • crates/aisix-admin/src/observability_exporters_handlers.rs
  • crates/aisix-admin/src/cache_policies_handlers.rs
  • crates/aisix-admin/src/models_handlers.rs
  • crates/aisix-admin/src/guardrails_handlers.rs
  • crates/aisix-admin/src/provider_keys_handlers.rs

Comment threadcrates/aisix-admin/src/lib.rs Outdated
Comment threadcrates/aisix-admin/tests/etcd_integration.rs Outdated
Comment threadcrates/aisix-server/src/main.rs
Comment on lines +179 to +213
expect(postRes.status).toBe(405);
expect(postRes.headers.get("allow")).toContain("GET");
await postRes.text();

// The refused write did not change the resource set.
const relist = await fetch(`${app.adminUrl}/admin/v1/models`, { headers: auth });
expect(((await relist.json()) as unknown[]).length).toBe(2);

// DELETE and rotate are covered by the same guard.
const delRes = await fetch(`${app.adminUrl}/admin/v1/models/any-id`, {
method: "DELETE",
headers: auth,
});
expect(delRes.status).toBe(409);
expect(delRes.status).toBe(405);
expect(delRes.headers.get("allow")).toContain("GET");
await delRes.text();

// The rotate route was removed outright (it was POST-only), so the
// path no longer exists at all.
const rotateRes = await fetch(`${app.adminUrl}/admin/v1/api_keys/any-id/rotate`, {
method: "POST",
headers: auth,
});
expect(rotateRes.status).toBe(409);
expect(rotateRes.status).toBe(404);
await rotateRes.text();

// Auth ordering: an UNAUTHENTICATED write still gets 401, and the
// 401 body must not leak the resources-file path (that detail is
// only for authenticated admins).
// Method routing answers before auth: an unauthenticated write gets
// the same 405 — there is no write endpoint left to protect, and
// the 405 body carries no resources-file detail to leak.
const unauthed = await fetch(`${app.adminUrl}/admin/v1/models`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ display_name: "nope" }),
});
expect(unauthed.status).toBe(401);
const unauthedBody = (await unauthed.json()) as { error_msg: string };
expect(unauthedBody.error_msg).not.toContain(app.resourcesPath!);
expect(unauthed.status).toBe(405);
expect((await unauthed.text())).not.toContain(app.resourcesPath!);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert the exact Allow header value.

Use toBe("GET") for each rejected write. Add the same assertion for the unauthenticated request. toContain("GET") also accepts an invalid value such as GET, POST.

Based on PR objectives, rejected resource writes must return 405 with Allow: GET.

Proposed test update
- expect(postRes.headers.get("allow")).toContain("GET");+ expect(postRes.headers.get("allow")).toBe("GET");
...
- expect(delRes.headers.get("allow")).toContain("GET");+ expect(delRes.headers.get("allow")).toBe("GET");
...
expect(unauthed.status).toBe(405);
+ expect(unauthed.headers.get("allow")).toBe("GET");
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
expect(postRes.status).toBe(405);
expect(postRes.headers.get("allow")).toContain("GET");
awaitpostRes.text();
// The refused write did not change the resource set.
constrelist=awaitfetch(`${app.adminUrl}/admin/v1/models`,{headers: auth});
expect(((awaitrelist.json())asunknown[]).length).toBe(2);
// DELETE and rotate are covered by the same guard.
constdelRes=awaitfetch(`${app.adminUrl}/admin/v1/models/any-id`,{
method: "DELETE",
headers: auth,
});
expect(delRes.status).toBe(409);
expect(delRes.status).toBe(405);
expect(delRes.headers.get("allow")).toContain("GET");
awaitdelRes.text();
// The rotate route was removed outright (it was POST-only), so the
// path no longer exists at all.
constrotateRes=awaitfetch(`${app.adminUrl}/admin/v1/api_keys/any-id/rotate`,{
method: "POST",
headers: auth,
});
expect(rotateRes.status).toBe(409);
expect(rotateRes.status).toBe(404);
awaitrotateRes.text();
// Auth ordering: an UNAUTHENTICATED write still gets 401, and the
// 401 body must not leak the resources-file path (that detail is
// only for authenticated admins).
// Method routing answers before auth: an unauthenticated write gets
// the same 405 — there is no write endpoint left to protect, and
// the 405 body carries no resources-file detail to leak.
constunauthed=awaitfetch(`${app.adminUrl}/admin/v1/models`,{
method: "POST",
headers: {"content-type": "application/json"},
body: JSON.stringify({display_name: "nope"}),
});
expect(unauthed.status).toBe(401);
constunauthedBody=(awaitunauthed.json())as{error_msg: string};
expect(unauthedBody.error_msg).not.toContain(app.resourcesPath!);
expect(unauthed.status).toBe(405);
expect((awaitunauthed.text())).not.toContain(app.resourcesPath!);
expect(postRes.status).toBe(405);
expect(postRes.headers.get("allow")).toBe("GET");
awaitpostRes.text();
// The refused write did not change the resource set.
constrelist=awaitfetch(`${app.adminUrl}/admin/v1/models`,{headers: auth});
expect(((awaitrelist.json())asunknown[]).length).toBe(2);
constdelRes=awaitfetch(`${app.adminUrl}/admin/v1/models/any-id`,{
method: "DELETE",
headers: auth,
});
expect(delRes.status).toBe(405);
expect(delRes.headers.get("allow")).toBe("GET");
awaitdelRes.text();
// The rotate route was removed outright (it was POST-only), so the
// path no longer exists at all.
constrotateRes=awaitfetch(`${app.adminUrl}/admin/v1/api_keys/any-id/rotate`,{
method: "POST",
headers: auth,
});
expect(rotateRes.status).toBe(404);
awaitrotateRes.text();
// Method routing answers before auth: an unauthenticated write gets
// the same 405 — there is no write endpoint left to protect, and
// the 405 body carries no resources-file detail to leak.
constunauthed=awaitfetch(`${app.adminUrl}/admin/v1/models`,{
method: "POST",
headers: {"content-type": "application/json"},
body: JSON.stringify({display_name: "nope"}),
});
expect(unauthed.status).toBe(405);
expect(unauthed.headers.get("allow")).toBe("GET");
expect((awaitunauthed.text())).not.toContain(app.resourcesPath!);
🧰 Tools
🪛 ast-grep (0.45.0)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { execFile } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/e2e/src/cases/file-resource-source-e2e.test.ts` around lines 179 - 213,
Update the rejected-write assertions in the e2e test to require the exact Allow
header value "GET" instead of merely containing GET, covering both the
authenticated POST/DELETE responses and the unauthenticated POST response.
Preserve the existing status and response-body assertions.

Comment threadtests/e2e/src/cases/openai-sdk-compat.test.ts

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Removes Admin API resource writes, leaving read-only resource endpoints and moving management to declarative file or etcd paths.

Changes:

  • Removes write handlers, routes, store operations, rotation, and deprecation middleware.
  • Updates OpenAPI, documentation, and tests for the read-only contract.
  • Migrates test setup to direct etcd seeding.

Reviewed changes

Copilot reviewed 25 out of 26 changed files in this pull request and generated 12 comments.

Show a summary per file
FileDescription
README.mdDocuments the read-only Admin API.
config.example.yamlClarifies declarative resource management.
crates/aisix-server/src/main.rsWires read-only admin stores.
crates/aisix-admin/src/lib.rsRemoves write routes and middleware.
crates/aisix-admin/src/openapi.rsRemoves write operations and schemas.
crates/aisix-admin/src/store.rsMakes ConfigStore read-only.
crates/aisix-admin/src/state.rsRemoves file-write guard state.
crates/aisix-admin/src/error.rsRemoves write-related errors.
crates/aisix-admin/src/file_store.rsRetains snapshot reads only.
crates/aisix-admin/src/etcd_store.rsRetains etcd reads only.
crates/aisix-admin/src/models_handlers.rsRemoves model writes.
crates/aisix-admin/src/apikeys_handlers.rsRemoves API-key writes and rotation.
crates/aisix-admin/src/provider_keys_handlers.rsRemoves provider-key writes.
crates/aisix-admin/src/guardrails_handlers.rsRemoves guardrail writes.
crates/aisix-admin/src/cache_policies_handlers.rsRemoves cache-policy writes.
crates/aisix-admin/src/observability_exporters_handlers.rsRemoves exporter writes.
crates/aisix-admin/src/mcp_servers_handlers.rsRemoves MCP-server writes.
crates/aisix-admin/src/a2a_agents_handlers.rsRemoves A2A-agent writes.
crates/aisix-admin/tests/etcd_integration.rsTests direct-etcd writes and admin reads.
tests/e2e/src/harness/admin.tsRemoves Admin API write helpers.
tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.tsRemoves obsolete path comparison tests.
tests/e2e/src/cases/openai-sdk-compat.test.tsMigrates setup to etcd seeding.
tests/e2e/src/cases/file-resource-source-e2e.test.tsTests file-mode read-only behavior.
tests/e2e/src/cases/config-forward-compat-e2e.test.tsRemoves Admin write validation coverage.
tests/e2e/src/cases/apikey-lifecycle-e2e.test.tsReplaces rotation with declarative secret swapping.
tests/e2e/src/cases/apikey-budget-e2e.test.tsRemoves obsolete write-path validation test.
Suppressed comments (1)

crates/aisix-admin/src/mcp_servers_handlers.rs:9

  • This removal also eliminates the only production call to aisix_mcp::validate_spec. The file and etcd loaders only run the core JSON Schema, which does not reject specs with zero generatable operations or colliding sanitized tool names, so the newly exclusive declarative paths accept configurations the former write API rejected. Add the semantic validation to both loaders before removing this path.
use aisix_core::McpServer;

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment threadcrates/aisix-server/src/main.rs Outdated
use serde::{Deserialize, Serialize};
use serde_json::Value;
use uuid::Uuid;
use serde::Serialize;
Comment threadcrates/aisix-server/src/main.rs Outdated
Comment on lines 9 to 10
//! replaces the `key` field with a freshly-generated `sk-*` value and
//! bumps the revision, invalidating the old credential.
Comment on lines 8 to 9
//!
//! ids are UUID v4s generated on POST; PUT preserves the existing id.
@@ -38,7 +38,7 @@ const OPENAPI_JSON_BASE: &str = r##"{
"info": {
"title": "AISIX Admin API",
"version": "dev",
"description": "The AISIX Admin API configures an open-source AISIX gateway at runtime. Use it when you operate the gateway directly and need to create or update models, caller API keys, provider credentials, guardrails, cache policies, and observability exporters.\n\nThe write endpoints (POST, PUT, DELETE) are deprecated in favor of declarative configuration: load resources from a `resources_file` (`resources.yaml`) or write them to etcd directly. Write endpoints remain functional, and every mutating response carries a `Deprecation` header (RFC 9745) plus a `Link` header with `rel=\"deprecation\"` pointing at the migration documentation. Read endpoints are not deprecated.\n\nGateways connected to AISIX Cloud do not expose this listener. Configure them through AISIX Cloud."
"description": "The AISIX Admin API is the read-only operational surface of an open-source AISIX gateway: list and inspect the loaded models, caller API keys, provider credentials, guardrails, MCP servers, A2A agents, cache policies, and observability exporters, check per-model upstream health, and drive the playground.\n\nResource write endpoints were removed in favor of declarative configuration: declare resources in a `resources_file` (`resources.yaml`) and reload with SIGHUP, or write them to etcd directly. See the resources file reference at https://docs.api7.ai/ai-gateway/reference/resources-file.\n\nGateways connected to AISIX Cloud do not expose this listener. Configure them through AISIX Cloud."
Comment on lines 4 to 5
//! validate against the JSON schema, reject duplicate names (409),
//! generate a uuid v4 on POST, bump revision on PUT.
Comment on lines 4 to 5
//! validate against the JSON schema, reject duplicate names (409),
//! generate a uuid v4 on POST, bump revision on PUT.
Comment on lines 5 to 6
//! PUT. Additionally rejects a name containing the reserved tool-namespace
//! separator `__`, since the name prefixes the server's tools.
Comment on lines 8 to 9
//! every configuration path rejects an incomplete credential set; the checks
//! below are defense in depth.
@moonmingmoonming self-assigned this Aug 10, 2026
@moonming

Copy link
Copy Markdown
CollaboratorAuthor

Review triage — every inline comment dispositioned; fixes landed in f9c10cc and e34694b.

Fixed

  • 7 handler module docs rewritten to the read-only contract; stale rotate/CRUD section comments removed (e34694b)
  • OpenAPI Caller API Keys tag no longer says "and key rotation"; path/Entry descriptions no longer claim ids are generated by the Admin API or that update/rotate bumps revisions (f9c10cc, e34694b)
  • main.rs: unused Some(path) binding → Some(_); canonical product name; removed a comment referencing write rejections (e34694b)
  • etcd integration: refused-writes test now covers PUT and DELETE with Allow assertions, and the rotate-absence checks GET the rotate URIs too — POST-only 404 couldn't distinguish a deleted route from the old handler's unknown-id 404 (f9c10cc, e34694b)
  • sdk-compat e2e: readiness gate switched from the SDK chat path to an independent authenticated GET /v1/models probe per the harness gate rules (e34694b)
  • config.example.yaml: dead docs/api-admin.md link and Admin-API management claims replaced with the declarative sources (f9c10cc)

Not adopted, with reasons

  • Assert Allow with toBe("GET"): the real binary answers Allow: GET,HEAD (axum's MethodRouter advertises HEAD alongside GET), so an exact "GET" match fails against actual behavior. toContain("GET") plus the 405 status is the contract; the 405 itself already proves no write method is routed.
  • Duplicate key_hash uniqueness on the direct-etcd path (assert_unique_key removal): real gap, but pre-existing — the admin-side check never guarded direct etcd writes, which existed before this PR and are used by the control plane (which enforces uniqueness org-side) and the file source (which has its own per-kind identity check). Filed internally for loader-side conflict detection with /status/config visibility; tracked as a follow-up rather than blocking the removal.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts (1)

276-301: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Use an independent readiness check for key setup.

seedKey gates propagation with a POST /v1/chat/completions request. The rotation and deletion flows also use the chat authorization path for their assertions. A chat failure can stop the test before it checks the key transition.

Seed the caller keys, then verify readiness with GET /v1/models and require 200. Keep chat requests for the actual rotation and revocation assertions.

As per coding guidelines, E2E readiness gates must use an independent condition, and caller API keys must be checked with GET /v1/models returning 200.

Also applies to: 311-312

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts` around lines 276 - 301,
Update the key setup readiness flow around seedKey and the related
rotation/deletion cases to use an independent GET /v1/models request requiring
HTTP 200, rather than POST /v1/chat/completions. Keep chat requests in the
secret-swap and revocation assertions only, so readiness failures cannot mask
key-transition checks.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/aisix-core/src/bin/dump-schema.rs`:
- Around line 44-47: Update the comment near the STRICT shape documentation to
qualify unknown-field rejection as applying only where the resource schema is
closed. Preserve the existing distinction between declarative write contracts
and lenient etcd reads, while acknowledging resource-specific exceptions such as
open fields and custom guardrail validation documented in the schema guidance.
In `@tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts`:
- Around line 304-307: Update the combined setup guard in the affected E2E test
to also check that otlp is available, skipping and returning when any shared
setup value—including etcdReachable, app, seed, or otlp—is missing.
---
Outside diff comments:
In `@tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts`:
- Around line 276-301: Update the key setup readiness flow around seedKey and
the related rotation/deletion cases to use an independent GET /v1/models request
requiring HTTP 200, rather than POST /v1/chat/completions. Keep chat requests in
the secret-swap and revocation assertions only, so readiness failures cannot
mask key-transition checks.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: aa61ea23-b8c7-4503-b1c7-e50f6378ee7d

📥 Commits

Reviewing files that changed from the base of the PR and between 152c57d and f9c10cc.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (12)
  • README.md
  • config.example.yaml
  • crates/aisix-admin/Cargo.toml
  • crates/aisix-admin/src/apikeys_handlers.rs
  • crates/aisix-admin/src/error.rs
  • crates/aisix-admin/src/lib.rs
  • crates/aisix-admin/src/openapi.rs
  • crates/aisix-admin/tests/etcd_integration.rs
  • crates/aisix-core/src/bin/dump-schema.rs
  • crates/aisix-etcd/src/provider.rs
  • schemas/README.md
  • tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts
💤 Files with no reviewable changes (2)
  • crates/aisix-admin/Cargo.toml
  • crates/aisix-admin/src/error.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • config.example.yaml
  • README.md
  • crates/aisix-admin/src/apikeys_handlers.rs
  • crates/aisix-admin/src/lib.rs

Comment threadcrates/aisix-core/src/bin/dump-schema.rs
Comment on lines +304 to 307
if (!etcdReachable || !app || !seed) {
ctx.skip();
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Include otlp in the setup guard.

The guard omits otlp. Add it to the combined setup check so the test does not run with incomplete shared setup.

Based on learnings, E2E cases must preserve if (!etcdReachable || !app || !seed || !otlp) { ctx.skip(); return; }.

Proposed fix
- if (!etcdReachable || !app || !seed) {+ if (!etcdReachable || !app || !seed || !otlp) {
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if(!etcdReachable||!app||!seed){
ctx.skip();
return;
}
if(!etcdReachable||!app||!seed||!otlp){
ctx.skip();
return;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/e2e/src/cases/apikey-lifecycle-e2e.test.ts` around lines 304 - 307,
Update the combined setup guard in the affected E2E test to also check that otlp
is available, skipping and returning when any shared setup value—including
etcdReachable, app, seed, or otlp—is missing.

Source: Learnings

The admin listener keeps its read surface (lists/gets for all 8
resource kinds incl. the former apikeys spelling, models/status,
health, OpenAPI + Scalar, playground, livez/readyz); resources are
managed exclusively through the declarative paths — resources_file
(SIGHUP reload) or direct etcd writes.
BREAKING CHANGE: POST/PUT/DELETE on /admin/v1/* answer 405 with
Allow: GET (409 file-managed rejection included); the api-key rotate
route is gone (404) — rotate declaratively by writing the same
resource id with a new key_hash. The published OpenAPI documents no
write operations; write-only component schemas (ApiKeyRequest,
ApiKeyEntry, ApiKeyRotateResponse, DeleteResponse) are removed.
- router: every /admin/v1/* resource route serves get() only; rotate
routes, file-managed write guard, and RFC 9745 deprecation-header
middleware deleted; 43 write/rotate/uniqueness handler fns removed
- store: ConfigStore is read-only (16 put_*/delete_* methods and
StoreError::ReadOnly removed); EtcdConfigStore keeps reads only;
FileManagedStore::new(snapshot) drops the path param; InMemoryStore
keeps #[cfg(test)] inherent writes for unit-test seeding
- openapi: 24 write ops + rotate path removed from the base document;
no-op deprecation-marker pass deleted; unreachable component
schemas pruned; new gate pins GET-only /admin/v1/* and zero
deprecated marks
- tests: write-path suites deleted; read tests seed via InMemoryStore;
new 405/Allow + rotate-404 contract tests; etcd integration tests
seed via direct etcd writes (the declarative front door) and pin
that refused writes never touch etcd
- e2e: AdminClient write helpers removed (SeedClient is the write
front door); file-resource-source pins the new read-only contract;
apikey-lifecycle rotate coverage became a declarative secret-swap
test; obsolete write-path cases deleted
- docs: README, config.example.yaml, crate module docs updated
Second-auditor findings on the removal PR, all test/doc-level (no
runtime changes):
- rotate-404 tests now GET the rotate URIs too — POST-only 404 could
not distinguish a deleted route from the old handler's unknown-id
404; GET answers 405 on a surviving POST-only route
- e2e: deleting a key's etcd entry revokes an in-use bearer
(fail-closed, unknown-token 401, other keys unaffected) — the
deletion branch had lost its only end-to-end proof
- etcd integration: a2a_agents round-trip + loader coverage (7 -> 8
kinds)
- apikeys read test pins the full PublicApiKey projection
(allowed_tools/disabled/expires_at), not just the id
- OpenAPI descriptions stop claiming ids are generated by the Admin
API and revisions increment on update/rotate
- stale write-path narrative removed: config.example.yaml (dead
docs/api-admin.md link), schemas/README, dump-schema comment,
apikeys_handlers module doc, aisix-etcd provider doc, README RBAC
row + e2e counts
- dead code: AdminError::{BadRequest,Conflict,Schema} variants and
the aisix-mcp/uuid dependencies left over from the write path
Per-comment review triage:
- 7 handler module docs rewritten from CRUD-era text to the surviving
read-only contract; stale rotate/CRUD section comments in the
aisix-admin test module removed
- OpenAPI 'Caller API Keys' tag no longer advertises key rotation
- main.rs: unused match binding -> Some(_); canonical product name;
dropped a comment referencing the removed write-rejection path
- etcd integration: refused-writes test now covers PUT and DELETE
(405 + Allow), not just POST
- sdk-compat e2e: readiness gate switched from the SDK chat path (the
behavior under test) to an independent authenticated GET /v1/models
probe, per the harness gate rules
Cold-audit closeout:
- removed_resource_writes_answer_405_with_allow_get now generates the
FULL matrix (9 route spellings x POST/PUT/DELETE) instead of a
sampled subset — a partial revert (e.g. PUT re-added on one {id}
route) previously passed the whole suite
- last stale write-path narrative: store.rs module doc (read-only
trait), aisix-core schema.rs/models docs (declarative writers, not
'Admin API ... 400'), e2e smoke/seed/app/forward-compat headers no
longer cite deleted characterization or held-back write cases,
openapi.rs base-doc comment names a surviving schema
Round-two auditor findings:
- OpenAPI: the two remaining Entry revision descriptions (McpServer,
A2aAgent) stop describing create/update lifecycle; a regression
assertion now rejects write-lifecycle prose on any documented
revision field
- schemas/README + schema.rs + dump-schema: scope the strict-contract
claim to the in-repo writers (aisix validate, file source) — the
control plane validates its own API schema, a raw direct etcd put
gets no synchronous validation (lenient read only), and
unknown-field rejection applies only where a resource closes fields;
the previous rewrite overclaimed all three
- deletion-revocation e2e: propagation barrier is now a fresh key
seeded after the delete (later etcd revision), so the revocation
assertion is a real assertion instead of a gate poll; a regression
fails the assert, not a 30s timeout
- apikeys projection test seeds rate_limit + allowed_agents and pins
the list entry's projection too; module doc describes PublicApiKey
as an explicit allowlist (not 'minus nothing')
- lifecycle prose: secret-swap invalidates 'as soon as the write
propagates' (not 'immediately'); README e2e counts scoped to
scenario files (183/496)
@moonming
moonmingforce-pushed the feat/remove-admin-write-path branch from 74b0db5 to f43eab2CompareAugust 11, 2026 01:59
@moonming
moonming merged commit b61d270 into mainAug 11, 2026
30 of 32 checks passed
@moonming
moonming deleted the feat/remove-admin-write-path branch August 11, 2026 02:20
Sign up for freeto 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.

2 participants

@moonming