Skip to content

feat(auth): rfc 0029 green (query/mcp binding) — one resolver on every surface - #425

Merged
jensholdgaard merged 3 commits into
mainfrom
rfc0029-green-query-binding
Jul 7, 2026
Merged

feat(auth): rfc 0029 green (query/mcp binding) — one resolver on every surface#425
jensholdgaard merged 3 commits into
mainfrom
rfc0029-green-query-binding

Conversation

@jensholdgaard

@jensholdgaard jensholdgaard commented Jul 7, 2026

Copy link
Copy Markdown
Owner

What

RFC 0029 query/MCP binding (slice 3b) — the read side joins the ingest side (#424) on one async AuthResolver, and the transitional enforcement_store() bridge retires exactly as its own doc comment promised.

  • querier /v1/query + MCP (transport bearer gate and per-tool tenant check): Option<Arc<TokenStore>>AuthResolver throughout; check_tenant is now async (an OIDC unseen-kid miss may refetch the JWKS).
  • Startup: both network roles build their resolver via auth_resolver — OIDC discovery failure is a startup error naming auth.oidc. The .1 served test gained a fixture issuer since an oidc-configured querier now also discovers at startup.
  • Bridge retired: AuthConfig::enforcement_store removed from ourios-core; its tests migrated to direct static_tokens assertions (the oidc-only empty-store semantics are superseded by resolver enforcement, asserted on the wire).

§5 — .3/.4/.5 live on the served binary

Scenario Evidence
RFC0029.3 claim binding tenant_claim ["a","b"]: in-set ingest acks; out-of-set whole-batch PERMISSION_DENIED; query enforces 401 → 400 → 403 in order, in-set 200
RFC0029.4 wildcard ["*"] ingests + queries alpha/beta/entirely-new-tenant
RFC0029.5 coexistence one config, both halves: static bearer confined to acme, JWT confined to globex, side by side on ingest and query — each authenticating via its own path (and the static token exercising the RFC 0020 ${env:…} secret-hygiene rule); static-only / oidc-only / open-mode arms cited to their standing tests
RFC0029.2/.6 flipped to discharged markers naming the core-verifier oracles (#423)

Remaining before the status flip: the Dex testcontainers arm (.7).

Full gate: 978 / 0, clippy -D warnings, rustdoc, fmt, cargo-deny.

Invariants

§3.7: the OIDC binding flows through the same tenant checks on every surface — no new path. H6: nothing OIDC-specific leaks through the query DSL surface; rejections keep their RFC 0026 shapes (401/400/403, undifferentiated bodies).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Authentication handling is now unified across query and MCP paths, with support for both static tokens and OIDC in the same flow.
    • Tenant binding checks now work consistently for ingest and query requests, including wildcard tenant access.
  • Bug Fixes

    • Improved enforcement for OIDC-only setups so requests are rejected unless properly authenticated.
    • Fixed routing behavior so authenticated and open modes are handled more consistently across server entry points.

…y surface

The §3.3 binding completes across the read side: the querier's
/v1/query handler, the MCP transport-level bearer gate, and the
per-tool tenant check all resolve through the same async AuthResolver
the ingest listeners use — static store first, then OIDC — and the
transitional `enforcement_store()` bridge is retired exactly as its
own doc promised (RFC 0026 gates now consume the full auth config).

- querier.rs / mcp.rs: `Option<Arc<TokenStore>>` → `AuthResolver`
  throughout; `check_tenant` is async (an OIDC unseen-kid miss may
  refetch); open mode = `resolver.is_open()`.
- main.rs: both network roles build their resolver via the renamed
  `auth_resolver` (OIDC discovery at startup; failure names auth.oidc).
  The querier role therefore now requires a reachable issuer for an
  oidc-configured startup — the .1 served test gained the fixture
  issuer accordingly.
- ourios-core: `AuthConfig::enforcement_store` removed with its tests
  migrated to direct `static_tokens` assertions (the empty-store
  oidc-only semantics are superseded by resolver enforcement, asserted
  on the wire by the served arms).

§5: RFC0029.3/.4/.5 are live against the served binary —
.3: tenant_claim [a,b] drives RFC 0026 verbatim (in-set ingest acks,
out-of-set whole-batch PERMISSION_DENIED, query 401→400→403 order);
.4: wildcard ingests/queries arbitrary tenants; .5: one config with
both halves — the static bearer and a JWT authenticate via their own
paths, each confined to its own binding on ingest and query (the
static token exercising the RFC 0020 ${env:…} secret-hygiene rule).
.2/.6 stubs flipped to discharged markers naming their core oracles.
Only .7 (Dex) remains.

Full gate: 978 passed / 0 failed, clippy -D warnings, rustdoc, fmt,
cargo-deny.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jensholdgaard
jensholdgaard requested a review from Copilot July 7, 2026 19:24
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@jensholdgaard, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 35 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1ce51a93-f102-4567-ba48-fad9a846588a

📥 Commits

Reviewing files that changed from the base of the PR and between 42b0810 and f5d1d73.

📒 Files selected for processing (4)
  • crates/ourios-server/src/main.rs
  • crates/ourios-server/src/mcp.rs
  • crates/ourios-server/src/querier.rs
  • crates/ourios-server/tests/it/rfc0029_oidc.rs
📝 Walkthrough

Walkthrough

The AuthConfig::enforcement_store() bridge is removed from ourios-core, replaced by a shared async auth_resolver in main.rs feeding a new AuthResolver abstraction. Querier and MCP authentication paths are converted to use AuthResolver.authenticate(...).await and is_open(). Tests are updated accordingly, and new RFC0029 OIDC claim-binding tests are added.

Changes

AuthResolver migration

Layer / File(s) Summary
Remove enforcement_store bridge
crates/ourios-core/src/auth/mod.rs
Deletes AuthConfig::enforcement_store(), updates TokenStore docs, and reworks the bridge test to assert on static_tokens directly.
Wire async auth_resolver into startup
crates/ourios-server/src/main.rs
Introduces auth_resolver(&config).await? used by both receiver and querier roles, replacing separate ingest/enforcement resolvers, with updated RFC0026.1/RFC0029.1 test assertions.
MCP async auth checks
crates/ourios-server/src/mcp.rs
Converts OuriosMcp.auth, check_tenant, require_bearer, and mcp_router to use AuthResolver asynchronously, awaiting tenant checks in tool handlers and gating host filtering via is_open().
Querier async auth checks
crates/ourios-server/src/querier.rs
Updates QuerierConfig.auth, router, router_with_mcp, router_with_auth, and handle_query to use AuthResolver.authenticate(...).await.
Test wiring for AuthResolver::static_only
crates/ourios-server/tests/it/rfc0026_auth.rs, crates/ourios-server/tests/it/rfc0027_mcp.rs
Updates router construction calls to pass AuthResolver::static_only(...) instead of raw Option<Arc<TokenStore>> values.
RFC0029 OIDC claim-binding tests
crates/ourios-server/tests/it/rfc0029_oidc.rs
Adds a loopback OIDC issuer fixture for RFC0029.1, stubs RFC0029.2, widens helper visibility, and adds new claim_binding module tests for RFC0029.3–0029.5 covering claim enforcement, wildcard tenants, and dual auth coexistence.

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

Possibly related PRs

  • jensholdgaard/ourios#390: Introduces the RFC0026 token-store and auth.tokens config that the AuthResolver in this PR ultimately wraps.
  • jensholdgaard/ourios#413: Added the original TokenStore-based bearer gate for /mcp in the same mcp.rs/querier.rs files now refactored to AuthResolver.
  • jensholdgaard/ourios#422: Introduced AuthConfig::enforcement_store() and the OIDC-only bridge behavior that this PR removes.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: RFC 0029 auth binding consolidated onto a single resolver across query and MCP surfaces.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch rfc0029-green-query-binding

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.

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (3)
crates/ourios-core/src/auth/mod.rs (1)

562-568: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Stale reference to the removed enforcement bridge.

The bridge was deleted, but this test's doc comment still claims it validates that "the oidc-only enforcement bridge rejects every bearer rather than opening the gates" — behavior the test no longer exercises (it only asserts static_tokens/oidc shape). The _oidc_only_bridge suffix in the test name is likewise now misleading. Consider trimming the comment and renaming to reflect that enforcement is now a resolver/serving property.

🤖 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/ourios-core/src/auth/mod.rs` around lines 562 - 568, The test name and
doc comment in auth_config_rules_and_oidc_only_bridge still refer to the removed
enforcement bridge, but the test now only checks config shape for static_tokens
and oidc. Update the documentation to remove the claim about “the oidc-only
enforcement bridge rejects every bearer” and rename the test to reflect its
current scope as a resolver/serving rules check, using the
auth_config_rules_and_oidc_only_bridge symbol as the locator.
crates/ourios-server/tests/it/rfc0029_oidc.rs (2)

580-582: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Content-Length: 16 is hand-tied to the literal body.

The header hardcodes 16 while the body template_id == 1 is pushed in the same call. Any future edit to the query string desynchronizes the length, which will stall the read or trip a request error rather than fail loudly. Deriving the length from the body removes the footgun.

♻️ Compute Content-Length from the body
-        request.push_str(
-            "Content-Type: text/plain\r\nContent-Length: 16\r\nConnection: close\r\n\r\ntemplate_id == 1",
-        );
+        let body = "template_id == 1";
+        write!(
+            request,
+            "Content-Type: text/plain\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
+            body.len(),
+        )
+        .expect("write body");
🤖 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/ourios-server/tests/it/rfc0029_oidc.rs` around lines 580 - 582, The
request in the RFC0029 OIDC test hardcodes Content-Length to 16 while appending
the body in the same call, so the header can drift from the actual payload.
Update the construction around the request.push_str call to derive
Content-Length from the body string instead of a literal, using the same body
value that contains template_id == 1 so future edits stay in sync.

533-534: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Nulling child stderr blinds the 15s startup wait.

If the child fails to announce its roles, the only diagnostic is the panic!("role announcements never appeared") after a 15s timeout — stderr is discarded. rfc0029_1_oidc_only_starts_and_enforces deliberately buffers stderr for exactly this reason. Consider capturing stderr here (at least on the timeout path) so spawn failures across the three claim_binding tests are debuggable.

🤖 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/ourios-server/tests/it/rfc0029_oidc.rs` around lines 533 - 534, The
child process in the `claim_binding` test setup is discarding stderr, which
makes the 15s startup timeout impossible to diagnose. Update the test harness
around the child spawn/role-announcement wait to capture stderr like
`rfc0029_1_oidc_only_starts_and_enforces` does, and surface that buffered output
on the timeout path or spawn failure. Keep the fix localized to the
`claim_binding`-related startup helper so failures in all three tests remain
debuggable.
🤖 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.

Nitpick comments:
In `@crates/ourios-core/src/auth/mod.rs`:
- Around line 562-568: The test name and doc comment in
auth_config_rules_and_oidc_only_bridge still refer to the removed enforcement
bridge, but the test now only checks config shape for static_tokens and oidc.
Update the documentation to remove the claim about “the oidc-only enforcement
bridge rejects every bearer” and rename the test to reflect its current scope as
a resolver/serving rules check, using the auth_config_rules_and_oidc_only_bridge
symbol as the locator.

In `@crates/ourios-server/tests/it/rfc0029_oidc.rs`:
- Around line 580-582: The request in the RFC0029 OIDC test hardcodes
Content-Length to 16 while appending the body in the same call, so the header
can drift from the actual payload. Update the construction around the
request.push_str call to derive Content-Length from the body string instead of a
literal, using the same body value that contains template_id == 1 so future
edits stay in sync.
- Around line 533-534: The child process in the `claim_binding` test setup is
discarding stderr, which makes the 15s startup timeout impossible to diagnose.
Update the test harness around the child spawn/role-announcement wait to capture
stderr like `rfc0029_1_oidc_only_starts_and_enforces` does, and surface that
buffered output on the timeout path or spawn failure. Keep the fix localized to
the `claim_binding`-related startup helper so failures in all three tests remain
debuggable.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f5ee407a-cccf-4963-bcf3-c64e7c8d9776

📥 Commits

Reviewing files that changed from the base of the PR and between bb02d91 and 42b0810.

📒 Files selected for processing (7)
  • crates/ourios-core/src/auth/mod.rs
  • crates/ourios-server/src/main.rs
  • crates/ourios-server/src/mcp.rs
  • crates/ourios-server/src/querier.rs
  • crates/ourios-server/tests/it/rfc0026_auth.rs
  • crates/ourios-server/tests/it/rfc0027_mcp.rs
  • crates/ourios-server/tests/it/rfc0029_oidc.rs

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR completes RFC 0029 “slice 3b” by wiring the querier /v1/query and MCP surfaces onto the same async AuthResolver used by ingest, and removes the transitional AuthConfig::enforcement_store() bridge from core/server configuration.

Changes:

  • Replaces querier/MCP auth plumbing from Option<Arc<TokenStore>> + authenticate_bearer(...) to AuthResolver + async authenticate(...) (including async per-tool tenant checks for MCP).
  • Updates server startup to construct an AuthResolver (including OIDC discovery) for network roles, and removes the enforcement_store() bridge.
  • Expands RFC 0029 served-binary integration tests to cover claim-binding enforcement and wildcard/coexistence behavior across ingest + query.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
crates/ourios-server/src/querier.rs Switches query endpoint auth from static store lookup to async AuthResolver authentication.
crates/ourios-server/src/mcp.rs Updates MCP bearer gating and per-tool tenant enforcement to use async AuthResolver.
crates/ourios-server/src/main.rs Retires enforcement_store() usage and builds role auth via auth_resolver() at startup.
crates/ourios-core/src/auth/mod.rs Removes AuthConfig::enforcement_store() and updates related docs/tests.
crates/ourios-server/tests/it/rfc0029_oidc.rs Adds served-binary tests for RFC 0029 claim binding/wildcard/coexistence on query.
crates/ourios-server/tests/it/rfc0027_mcp.rs Adjusts MCP router tests for new AuthResolver parameter type.
crates/ourios-server/tests/it/rfc0026_auth.rs Adjusts query auth tests for new AuthResolver parameter type.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/ourios-server/src/querier.rs Outdated
Comment thread crates/ourios-server/src/querier.rs Outdated
Comment thread crates/ourios-server/src/mcp.rs Outdated
Comment thread crates/ourios-server/src/main.rs Outdated
…ments

auth_resolver returns None when no network role is enabled and is
built exactly once otherwise — one OIDC discovery, one shared JWKS
cache/throttle across receiver + querier. QuerierConfig/router/
require_bearer docs describe the resolver, not the retired store.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Comment thread crates/ourios-server/src/mcp.rs Outdated
Comment thread crates/ourios-server/src/mcp.rs Outdated
Comment thread crates/ourios-server/tests/it/rfc0029_oidc.rs
…hed binding

require_bearer caches the resolved AuthBinding on the request; the
per-tool tenant check reads it from the forwarded parts (sync again),
failing closed when absent — no second verification, no second
possible JWKS fetch per tool call. spawn_with_auth inherits the
child's stderr so startup failures aren't bare timeouts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.

@jensholdgaard
jensholdgaard merged commit a2602fc into main Jul 7, 2026
22 checks passed
@jensholdgaard
jensholdgaard deleted the rfc0029-green-query-binding branch July 7, 2026 19:53
jensholdgaard added a commit that referenced this pull request Jul 7, 2026
#426)

* feat(auth): rfc 0029 green — dex end-to-end acceptance (.7) and status flip

The last §5 arm: a real Dex container (testcontainers, CI-gated like
the localstack job) mints client-credentials tokens the served binary
verifies against Dex's real JWKS.

- The static client carries the tenant list via
  clientCredentialsClaims.groups (tenant_claim: groups) and the name
  label via its display name (name_claim: name, scope profile) — the
  OTel Collector oauth2client flow verbatim.
- Arms: startup discovery against Dex; in-claim gRPC ingest acks;
  cross-tenant batch PERMISSION_DENIED; in-claim query 200; MCP 401
  bearer-less / success with the Dex bearer; real-TTL expiry (8 s
  tokens, zero skew) collapses to the undifferentiated 401; SIGTERM
  flushes the audit sink and the read-back ingest_denied event carries
  the name_claim value; no JWT material in error bodies or the log
  surface.
- Image: Dex `master` pinned by digest — the client-credentials grant
  and clientCredentialsClaims are merged upstream (dexidp/dex#4691)
  but post-v2.45.1; the RFC §6 note + ci.yml comment record the bump
  path to v2.46. New required `dex oidc (testcontainers)` CI job runs
  the arm by exact name.
- reqwest (rustls + json) joins the server dev-deps for minting and
  readiness polling — the same stack the verifier itself uses.

RFC 0029 status red → green: .1–.6 discharged across #420#425, .7
lands here and runs in this PR's own required CI job.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(auth): surface dex container logs on readiness timeout

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(auth): dex needs one connector — enablePasswordDB, inert for client-credentials

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(auth): dex arm hardening — ttl headroom, port retry, async sigterm; ci creds + rfc quote

20s tokens with the expiry wait driven by the response's expires_in;
container start retries a fresh port on the reserve race; SIGTERM via
tokio Command with an asserted status; persist-credentials: false on
the dex job checkout; the RFC image note no longer swallows the
following paragraph.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants