feat(adr0025): Phase 1 (1.1+1.2)#952
Merged
Merged
Conversation
gtema
commented
Jul 8, 2026
Collaborator
- feat(adr0025): Phase 1 - host function registration
- feat(adr0025): Phase 1 - auth-method dispatch (PR 1.2)
Wires provision_user/find_user (ADR 0025 §6 B/C) into the WasmPluginRegistry loaded in Phase 0: extism::Function registration for a plugin's granted capabilities, backed by a new HostFunctions trait that openstack-keystone-core will implement (kept out of dynamic-plugin-runtime to preserve the one-way core -> dynamic-plugin-runtime dependency). Since wasmtime requires every guest-declared import to resolve at instantiation, both functions are always registered whenever a HostFunctions provider is configured, and each closure independently rejects a call its plugin's capabilities config didn't grant - a per-capability import omission isn't viable against a single compiled module, but the guest-visible result is identical: an ungranted capability can never be successfully exercised. The reference plugin fixture gains call_provision_user/call_find_user guest exports (via extism-pdk's #[host_fn] extern "ExtismHost" block) so the wiring is proven end-to-end against a real compiled .wasm module, not just a mock. - New (plugin_name, external_id) -> user_id mapping table in identity-driver-sql, atomically upserted via ON CONFLICT DO NOTHING plus a re-select (idempotent under concurrent first-logins, per SS6.B). Two new IdentityBackend/IdentityApi methods expose it. - CoreHostFunctions enforces provision_domain_id/allowed_provision_domains before ever reaching the identity backend, is idempotent (repeat provisioning resolves the existing mapping, never a duplicate user), and self-heals a lost provisioning race by deleting the orphaned user and resolving to the winner. - ResolvedIdentityHandle is an HMAC-SHA256-signed token over (plugin_name, user_id, domain_id) rather than an in-memory handle -> identity map: extism bakes host functions into the *compiled* module, shared across every concurrent invocation, so there is no per-Store-scoped place to keep such a map safely isolated between concurrent requests. The signed token gives the same guarantee (unforgeable, resolves only to what this plugin's own provision_user/find_user actually returned) without shared mutable state. - find_user re-validates the user's live domain on every call, not only at provisioning time, so a user moved outside the plugin's configured domain(s) is immediately unreachable (SS4 "Admin-Authorized External Identity Linking"). - Mandatory CADF audit (SS6.E) via AuditDispatcher::dispatch_critical (fail-closed) on every provision_user/find_user call, using the existing pre-authentication Initiator precedent (build_initiator_unknown) since these calls happen before any ValidatedSecurityContext exists. assign_role (SS6.D): - Bounded on all three axes host-side: role name (assign_role_allowed allowlist), target project/domain (reuses provision_user's provision_domain_id/allowed_provision_domains restriction - a project target resolves its owning domain via ResourceApi::get_project first), and scope type (RoleAssignmentTarget has no System variant at all, so there is no code path to request one, not just a runtime check). - The actor is always the identity CoreHostFunctions::verify_handle resolves from the caller's own resolved_identity handle - the same anti-impersonation constraint as provision_user/find_user. http_fetch (SS6.A), the SSRF-hardened outbound HTTP bridge: - allowed_hosts is a hostname allowlist, but every candidate address is re-resolved and validated at connect time (private/loopback/link-local/ multicast/cloud-metadata ranges rejected, including 169.254.169.254), and reqwest::ClientBuilder::resolve pins the connection to that exact validated IpAddr - the socket never lets the HTTP client re-resolve the hostname a second time, closing the standard DNS-rebinding bypass of a hostname-only allowlist. - No redirects followed by default; http_fetch_follow_redirects re- validates host + connects-to-validated-IP on every hop, bounded to 5. - The host-injected auth header/secret (http_fetch_auth_header + http_fetch_auth_secret_env) is attached after the guest's own headers, so it can never be seen or overridden by guest code. - A hardcoded 4 MiB response-body cap (checked against Content-Length up front, and against the actual buffered size, since a chunked response may omit Content-Length) - not itself a config field per the ADR, called out in the module doc as a fixed constant. Assisted-By: Claude Sonnet 5 <noreply@anthropic.com> Signed-off-by: Artem Goncharov <artem.goncharov@gmail.com>
Wire mode=full_auth dynamic plugins into POST /v3/auth/tokens: an
unrecognized identity.<method> name now dispatches to a loaded
plugin's authenticate entry point instead of silently no-opping.
CoreHostFunctions/WasmPluginRegistry existed since PR 1.1 but had
no production call site until now.
- add AuthenticationContext::WasmPlugin{plugin_name, plugin_sha256,
claims, token}; auth_type() returns Cow<str> to carry the
plugin-specific method name
- add Credentials.plugin_claims, nested by plugin_name, so a
plugin's claims land under plugin_claims.<plugin_name>.* in OPA
input and can never shadow a top-level field
- add core::dynamic_plugin_auth::authenticate_via_wasm_plugin:
registry lookup, mode check, exposed_headers allowlist +
HARD_DENYLISTED_HEADERS re-check, trusted-proxy remote_addr
resolution (new [dynamic_plugins].trusted_proxies, separate from
api_key's), invoke, response-bounds decode, identity-binding
verification, CADF audit - fails closed on every path except an
unmatched/wrong-mode plugin, which degrades like any other
unmatched method
- extract crate::net::resolve_client_ip so dispatch and the
existing api_key path share one XFF-resolution implementation
- populate Service.dynamic_plugin_registry/core_host_functions
post-construction (load_dynamic_plugins), mirroring
subscribe_event_hooks's existing chicken-and-egg fix for wiring
that needs an already-built ServiceState
- add Identity.extra catch-all (serde flatten) so an unrecognized
method's request body survives deserialization
- rewrite the reference plugin fixture to the real
AuthPluginRequest/AuthPluginResponse contract, exercising
provision_user and a bad_handle test hook
Signed-off-by: Artem Goncharov <artem.goncharov@gmail.com>
|
🦢 Load Test Results Goose Attack ReportPlan Overview
Request Metrics
Response Time Metrics
Status Code Metrics
Transaction Metrics
Scenario Metrics
Error Metrics
|
|
| Branch | claude/adr-0025-implementation-plan-adh5ii |
| Testbed | ubuntu-latest |
Click to view all benchmark results
| Benchmark | Latency | Benchmark Result nanoseconds (ns) (Result Δ%) | Upper Boundary nanoseconds (ns) (Limit %) |
|---|---|---|---|
| Command_Serde/apply/remove | 📈 view plot 🚷 view threshold | 127,120.00 ns(-49.70%)Baseline: 252,705.03 ns | 1,513,172.56 ns (8.40%) |
| Command_Serde/apply/set | 📈 view plot 🚷 view threshold | 134,830.00 ns(-37.82%)Baseline: 216,834.14 ns | 813,164.52 ns (16.58%) |
| Command_Serde/pack/delete | 📈 view plot 🚷 view threshold | 125.18 ns(+2.02%)Baseline: 122.70 ns | 144.44 ns (86.67%) |
| Command_Serde/pack/delete_index | 📈 view plot 🚷 view threshold | 111.38 ns(+0.39%)Baseline: 110.94 ns | 130.43 ns (85.39%) |
| Command_Serde/pack/set | 📈 view plot 🚷 view threshold | 203.64 ns(+2.85%)Baseline: 198.00 ns | 236.59 ns (86.07%) |
| Command_Serde/pack/set_index | 📈 view plot 🚷 view threshold | 112.03 ns(+1.20%)Baseline: 110.70 ns | 129.65 ns (86.41%) |
| Command_Serde/unpack/delete | 📈 view plot 🚷 view threshold | 193.30 ns(-0.22%)Baseline: 193.72 ns | 236.88 ns (81.60%) |
| Command_Serde/unpack/delete_index | 📈 view plot 🚷 view threshold | 157.59 ns(-2.32%)Baseline: 161.33 ns | 200.85 ns (78.46%) |
| Command_Serde/unpack/set | 📈 view plot 🚷 view threshold | 269.92 ns(-0.85%)Baseline: 272.23 ns | 327.75 ns (82.36%) |
| Command_Serde/unpack/set_index | 📈 view plot 🚷 view threshold | 160.21 ns(+0.15%)Baseline: 159.97 ns | 197.27 ns (81.21%) |
| Payload_encryption/pack/remove_cmd | 📈 view plot 🚷 view threshold | 115.43 ns(-1.13%)Baseline: 116.75 ns | 140.01 ns (82.44%) |
| Payload_encryption/pack/set_cmd | 📈 view plot 🚷 view threshold | 205.43 ns(-0.52%)Baseline: 206.50 ns | 262.71 ns (78.20%) |
| Payload_encryption/unpack/remove_cmd | 📈 view plot 🚷 view threshold | 205.45 ns(-0.10%)Baseline: 205.65 ns | 257.60 ns (79.75%) |
| Payload_encryption/unpack/set_cmd | 📈 view plot 🚷 view threshold | 288.89 ns(+1.69%)Baseline: 284.09 ns | 351.20 ns (82.26%) |
| Raft_1Node_Latency/prefix/1node | 📈 view plot 🚷 view threshold | 2,490,000.00 ns(-12.24%)Baseline: 2,837,377.34 ns | 6,283,722.79 ns (39.63%) |
| Raft_1Node_Latency/read/1node | 📈 view plot 🚷 view threshold | 42,962.00 ns(+55.47%)Baseline: 27,633.34 ns | 73,323.32 ns (58.59%) |
| Raft_1Node_Latency/remove/1node | 📈 view plot 🚷 view threshold | 372,680.00 ns(-25.74%)Baseline: 501,828.75 ns | 1,555,659.01 ns (23.96%) |
| Raft_1Node_Latency/write/1node | 📈 view plot 🚷 view threshold | 386,060.00 ns(-29.64%)Baseline: 548,688.44 ns | 1,866,721.97 ns (20.68%) |
| build_snapshot/default | 📈 view plot 🚷 view threshold | 110,210.00 ns(+0.15%)Baseline: 110,039.50 ns | 130,594.55 ns (84.39%) |
| fernet token/project | 📈 view plot 🚷 view threshold | 1,441.10 ns(+3.20%)Baseline: 1,396.43 ns | 1,625.11 ns (88.68%) |
| get_data_keyspace | 📈 view plot 🚷 view threshold | 0.31 ns(-1.12%)Baseline: 0.32 ns | 0.36 ns (85.46%) |
| get_db | 📈 view plot 🚷 view threshold | 0.31 ns(-1.15%)Baseline: 0.32 ns | 0.36 ns (85.43%) |
| get_fernet_token_timestamp/project | 📈 view plot 🚷 view threshold | 144.49 ns(+1.27%)Baseline: 142.68 ns | 175.67 ns (82.25%) |
| get_keyspace | 📈 view plot 🚷 view threshold | 4.40 ns(-12.16%)Baseline: 5.01 ns | 10.66 ns (41.29%) |
Open
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.