mirror of
https://github.com/nearai/ironclaw.git
synced 2026-09-03 08:06:01 +08:00
* feat(reborn): admin user-management API and UI Add an end-to-end admin user-management surface to the Reborn stack, built on the existing StoredUser records in ironclaw_reborn_identity (no new user store — see .claude/rules/discovery-claims.md for the planning miss this corrects). Layers: - identity: new RebornUserDirectory trait (list/get/create/update/ status/role/last-login/delete-cascade/count-active-admins) on the same FilesystemRebornIdentityStore, kept separate from the resolver so admin CRUD can't perturb mint/link invariants. delete_user cascades over external-identity + verified-email records. CONTRACT.md documents the three persisted record shapes. - product_workflow: RebornServicesApi admin_* methods with caller authorization (admin/owner role or env-bearer operator) and last-admin protection; new AdminUserService port + DTOs. - composition: admin_user_directory / admin_secrets / admin_token adapters; AdminApiTokenMinter port for minting the one-time API bearer on user-create. Mount now grants list+delete on /tenant-shared for the identity delete cascade. - webui_v2: admin_users REST routes (GET/POST /admin/users, GET/PATCH/DELETE /admin/users/:id, status, role, GET/PUT/DELETE per-user secrets) + descriptors (body/rate limits). - serve: wires a signed-session-store-backed minter (365-day API bearer that validates under the SSO login surface's own store). - frontend: un-hide the admin nav + Users tab, wire admin-api.js to the real endpoints. Tests: identity-store unit tests, product_workflow contract tests (authz + last-admin + one-time token), webui_v2 descriptor contract, and composition HTTP e2e (full lifecycle + API-token login + last-admin over HTTP). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(reborn): admin API e2e + JS client tests; fix admin request bodies Follow-up test coverage for the admin user-management surface, plus a frontend bug the JS test exposed. - fix(webui-v2): `admin-api.js` passed request bodies as raw JS objects, but `apiFetch` forwards `options.body` to `fetch` unchanged (it does not serialize — callers must `JSON.stringify`, cf. `createThread`). In a real browser every admin write (create/update/status/role/secret) would have sent the string "[object Object]" and been rejected. Now stringified. Caught by the new JS test driving the real `apiFetch`. - test(webui-v2): `admin-api.test.js` — 14 Node `--test` cases driving the real `apiFetch` (stubbing `globalThis.fetch`), asserting each method/path/body and the id/token normalization. `jsonBody()` guards the serialization fix above. - test(e2e): repoint `test_admin_api.py` from the retired v1 `/api/admin/*` monolith surface to the v2 `/api/webchat/v2/admin/*` routes on the real `ironclaw-reborn serve` binary (`reborn_v2_server` fixture, operator env-bearer). Adds the flagship `created_user_token_authenticates_as_that_user` round-trip, which proves serve.rs's minter wiring: the one-time api_token validates AS the new user at `/session` because the admin minter store and the SSO login store share `session_signing_secret`. Added to `reborn_coverage_tests.txt`. Integration-tier note: admin coverage stays at the crate tier (`ironclaw_reborn_composition/tests/admin_api_e2e.rs`); the `AdminUserService` wiring is sealed `pub(crate)` in the composition root and the `tests/integration` harness has no minter seam, so an int-tier test would require faking the port ("wire the unwired") or relocating the crate-tier test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(reborn): validate admin-minted API tokens without SSO `ironclaw-reborn serve` unconditionally wires the admin-API token minter (serve.rs) — "admin creates user" always returns a signed **session** bearer as the one-time `api_token`. But the `SessionAuthenticator` that validates session bearers was only wired on the SSO path: with no SSO provider configured, `build_webui_auth_surface` returned just the env-bearer authenticator. So in the default no-SSO deployment, an admin-created user's API token failed with 401 on every request — the feature was dead on arrival exactly where it is most likely used. Compose the env-bearer (operator) authenticator with a `SessionAuthenticator` over the same `signed_session_store` the minter writes to, in the no-SSO branch of the auth surface. Operator capabilities still follow the env token only (`CompositeAuthenticator:: mounts_operator_webui_config_routes`), so the minted session bearer stays non-operator per the ingress crate's SSO-identity-only invariant. `CompositeAuthenticator` (env-OR-session, previously used only on the SSO path) is made `pub` and reused rather than duplicating the shim. Regression: caught by the crate's binary e2e `tests/e2e/scenarios/test_admin_api.py::test_created_user_token_authenticates_as_that_user` (added in the prior commit), which now passes 10/10 against a freshly built `ironclaw-reborn` — the crate-tier `admin_api_e2e.rs` masked this because it hand-wired a `SessionAuthenticator` production serve.rs did not. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(reborn): gate admin authz on status + serialize last-admin mutations Two adversarial-testing findings on the admin user-management surface, each fixed with its regression test. 1. Suspended admin retained full admin powers. `authorize_admin` checked `role.is_admin()` only, never `status`, so a SUSPENDED admin (role still reads Admin) kept complete control of the admin API. Now authorization requires admin/owner role AND `status == Active`, read on every call (never cached) — suspending an admin revokes their access immediately. Covered by `admin_suspended_admin_is_forbidden_on_every_verb`, plus a widened `admin_member_caller_is_forbidden_on_every_verb` / `admin_unknown_caller_is_forbidden_on_every_verb` sweep that drives the 403 through EVERY admin verb (not just list) per the "test through the caller" rule, including self-privilege-escalation. 2. Last-admin protection had a TOCTOU race. `ensure_not_last_admin` re-reads the active-admin count then mutates; two concurrent demotions each read "2 admins", both pass, and both land → 0 admins, stranding the tenant. Added a per-tenant admin-mutation lock (reusing the existing weak-ref keyed-lock registry, namespaced so keyspaces can't collide) held across the check+mutation in set_role / set_status / delete. Covered by `admin_last_admin_protection_survives_concurrent_demotion` (multi- thread runtime, concurrent demotions → exactly one lands, an admin always survives). Also adds session-store characterization tests locking two intentional, security-relevant bounds of the stateless signed-session denylist: revocation does not survive a process restart, and denylist eviction can resurrect a revoked-but-unexpired token under >4096-revocation pressure. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(reborn): admin API adversarial e2e; fix malformed secret handle 500 Adds 7 adversarial HTTP tests to the crate-tier admin e2e (real router + authenticator), and fixes a bug one of them surfaced. Fix: a malformed secret handle (path-traversal-shaped, e.g. `..%2F..`) was fail-closed (SecretHandle validation rejects it, nothing written) but the rejection mapped to `AdminUserError::Internal` → **500**. The handle is taken raw from the request path with no edge validation (a stale comment claimed otherwise), so a client-supplied bad handle is the client's fault: add `AdminUserError::InvalidInput` → **400**, and map the `SecretHandle` construction failure to it in put/delete. New e2e tests (crate `ironclaw_reborn_composition`, `admin_api_e2e.rs`): - deleted admin's token → 403 on admin routes (delete revokes admin access via no-record); documents (does not lock) that the stateless token still authenticates non-admin `/session` — a separate session-revocation gap. - suspended admin's token → 403 (exercises the status-gate fix). - minted admin *session* bearer is denied operator routes (no `operator_webui_config`) while still allowed on admin user CRUD. - forged / tampered / foreign-secret / expired tokens → 401. - oversized create body → 413 (per-route 16 KiB cap, before the facade). - secret-handle path traversal contained → now 400 (pins the fix above). - malformed user_id → 404, invalid role/status enum → 422; never 500. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(reborn): address PR #5779 admin-API review (security, pagination, boundary types, UI) Reviewer findings on the admin user-management surface: Security - serve: enforce the >=32-byte session-signing entropy floor unconditionally. The admin API token minter is always wired and signs user-visible session tokens, so a weak IRONCLAW_REBORN_WEBUI_TOKEN was an offline HMAC-forgery oracle even without SSO configured. - identity: block suspended accounts at resolve_or_create so a suspended user cannot mint a fresh SSO session (previously status gated only the admin routes). New UserSuspended error maps to a fail-closed 403 in the SSO host adapter. - identity: backfill the resolving tenant onto legacy tenantless user records on login, so pre-admin records stop being visible to every tenant's admin listing. - composition: scope /tenant-shared delete authority to the reborn-identity subtree; the broad tenant-shared grant keeps read+write+list but no longer grants delete. Boundary types - webui_v2: parse {handle} into SecretHandle at the HTTP edge (400 on a malformed handle) and thread SecretHandle through the facade + port instead of a raw String re-validated downstream. Pagination - admin user listing takes a bounded limit + opaque cursor through the port, facade, identity directory, and JS client; the directory reads at most `limit` matching records per page instead of scanning and allocating the whole tenant. Default page size 100, max 200. Frontend - detect the forbidden state from ApiError.status/payload, not a message string match; remove the dead "Create Token" re-issue action (no re-issue endpoint exists). Tests - suspended-login block, tenant backfill, and pagination in the identity store; last-admin DELETE protection, status-filter forwarding, cursor paging, and malformed-cursor 400 at the facade; fetchAdminUser / deleteAdminUser / paginated fetchAdminUsers in the JS client. Docs - webui_v2 route table now lists the admin routes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(reborn): close delete-vs-relink race with a delete tombstone; document last-admin lock scope C4 — delete/SSO-relink race: the delete cascade removes a user's external identities before its verified-email index, leaving a window where a concurrent SSO login (via another provider on the same verified email) would re-link a fresh identity record to the id being deleted, so future logins fast-path to a ghost. delete_user now writes a per-user tombstone before the cascade and removes it after; resolve_or_create refuses to re-link (or adopt) a tombstoned user, failing closed with a retryable error until the delete settles. A crash mid-cascade leaves the tombstone in place, which fails safe. C5 — last-admin lock: documented the guarantee boundary. The per-tenant mutation lock is airtight for the single-process `ironclaw-reborn serve` binary (the only shipping deployment); a durable cross-replica lease is deferred until a multi-replica mode exists, since a hand-rolled filesystem lease would add crash-recovery/stale-takeover risk outweighing the bounded race it replaces. Regression: resolve_or_create_refuses_to_relink_a_user_mid_delete, plus the existing delete-cascade test confirms the tombstone is cleaned up. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ci): unblock PR 5779 checks * fix(ci): update composition pub-use snapshot * fix(ci): stabilize trigger poller mutator test --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: serrrfirat <f@nuff.tech>
66 lines
2.1 KiB
TOML
66 lines
2.1 KiB
TOML
[advisories]
|
|
unmaintained = "workspace"
|
|
yanked = "deny"
|
|
ignore = [
|
|
# Pre-existing advisories — tracked for upgrade in separate PRs
|
|
# tokio-tar PAX header parsing — sandbox containers only
|
|
"RUSTSEC-2025-0111",
|
|
# rustls-webpki advisories — 0.102.8 remains pinned by a libsql 0.9.30 transitive dep
|
|
# (via rustls 0.22 → hyper-rustls 0.25); keep ignored until that pin is gone.
|
|
# RUSTSEC-2026-0104: panic on empty `onlySomeReasons` BIT STRING during CRL parsing.
|
|
# We do not use CRLs, and the advisory explicitly notes apps that don't parse CRLs
|
|
# are unaffected. Same transitive pin as 0049/0098/0099 — tracked with them.
|
|
"RUSTSEC-2026-0049",
|
|
"RUSTSEC-2026-0098",
|
|
"RUSTSEC-2026-0099",
|
|
"RUSTSEC-2026-0104",
|
|
# rsa Marvin timing side-channel — test-only dependency used to mint
|
|
# throwaway local OIDC JWT fixtures; no production network-observable
|
|
# private-key operation is exposed by this dependency.
|
|
"RUSTSEC-2023-0071",
|
|
]
|
|
|
|
[licenses]
|
|
version = 2
|
|
private = { ignore = true }
|
|
allow = [
|
|
"MIT",
|
|
# MIT-0 (MIT No Attribution) is strictly more permissive than MIT —
|
|
# required by `jsonschema` (used for workspace document schema validation).
|
|
"MIT-0",
|
|
"Apache-2.0",
|
|
"Apache-2.0 WITH LLVM-exception",
|
|
"BSD-2-Clause",
|
|
"BSD-3-Clause",
|
|
"ISC",
|
|
"Unicode-3.0",
|
|
"Unicode-DFS-2016",
|
|
"OpenSSL",
|
|
"Zlib",
|
|
"MPL-2.0",
|
|
"0BSD",
|
|
"BSL-1.0",
|
|
"CC0-1.0",
|
|
"Unlicense",
|
|
"CDLA-Permissive-2.0",
|
|
]
|
|
unused-allowed-license = "allow"
|
|
|
|
[bans]
|
|
multiple-versions = "warn"
|
|
wildcards = "deny"
|
|
# monty (Pydantic's embedded Python) is git-only (not on crates.io),
|
|
# so it inherently lacks a version constraint. Allow path-dep wildcards.
|
|
allow-wildcard-paths = true
|
|
|
|
[sources]
|
|
unknown-registry = "deny"
|
|
unknown-git = "deny"
|
|
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
|
|
allow-git = [
|
|
# Monty (Pydantic's embedded Python interpreter) — not yet on crates.io.
|
|
# Pulls in ruff_* crates from samuelcolvin/ruff at a pinned revision.
|
|
"https://github.com/pydantic/monty.git",
|
|
"https://github.com/samuelcolvin/ruff.git",
|
|
]
|