Skip to content

RBAC Support - #6729

Merged
gz merged 55 commits into
mainfrom
openid
Aug 1, 2026
Merged

RBAC Support#6729
gz merged 55 commits into
mainfrom
openid

Conversation

@gz

@gz gz commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Describe Manual Test Plan

This PR was tested extensively by bringing up instances with different authentication models and running fda/python/REST calls against it repeatedly evaluating various scenarios.

Checklist

  • Unit tests added/updated
  • Integration tests added/updated
  • Documentation updated
  • Changelog updated

Breaking Changes?

Mark if you think the answer is yes for any of these components:

Describe Incompatible Changes

@gz gz changed the title Openid RBAC Support Jul 25, 2026

@mythical-fred mythical-fred 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.

High-level pass (draft — I'll do the detailed review when marked ready).

The RFC #6422 structure lands cleanly. Things I like from a first read:

  • Single source of truth for the access-control model. ROUTE_MIN_ROLE
    in rbac.rs is the entire policy, one line per (method, path), and
    min_role_for re-exports it to OpenAPI so the reference and the runtime
    cannot drift. This is exactly the right shape — reviewing a permissions
    change is now a one-diff-in-one-file operation.
  • Deny-by-default with a test that enforces it. A registered /v0 route
    missing from the table is refused (fail closed), and
    every_registered_v0_route_is_classified fails the build when someone
    adds a new endpoint without classifying it. That is the correct default
    for auth, and the correct place to catch omissions.
  • Systematic role-matrix test. every_route_admits_exactly_its_minimum_role
    is the check I would have asked for — every role × every classified route,
    both directions. Reverting a Role::* in the table or breaking satisfies
    can't slip through.
  • Invariants at the schema. oidc_trust_owner_is_platform
    (tenant_id IS NULLrole = 'owner') and the partial UNIQUE on
    (name) for owner trusts make the "owner is platform-wide, others are
    tenant-scoped" split unforgeable at the DB rather than in Rust. The
    API-key scopes text[] → role text migration with the write ∈ scopes
    backfill preserves existing access without a compat shim.
  • Role is a single total order (Read < Write < Admin < Owner via
    derived Ord), and satisfies is one comparison. No permission bitset,
    no matrix — the doc comment even calls out the ordering invariant. Good.

Open questions I'll want answered on the ready-for-review pass (no action
now, just noting so I don't forget):

  1. Concurrent role revocation. If an admin demotes user X to read mid-
    request, the current request continues at the old role because the
    principal was resolved at auth-validate time. Is that intentional (bounded
    staleness, no per-request DB hit) or a gap to close?
  2. Audit sink. audit() writes to tracing at info/debug — that is a
    reasonable start, but production auditability usually wants a structured
    sink that survives log rotation and can be replayed. Is the plan to
    promote this to a durable event log later, or is tracing the durable
    answer?
  3. Owner trust caps. V35 says role is "capped at the creator's role at
    write time." Is that enforced in oidc_trust insert, or is a
    write-role user able to POST a trust with role: admin? Worth an
    explicit test.
  4. Owner acting across tenants. The audit line already flags
    home_tenant != acting_tenant — is there a rate limit or explicit
    opt-in step, or does an owner silently switch tenants by header on
    every request?

All CI concerns (checks, migrations forward/backward) I'll cover on the
ready-for-review pass; nothing structural to change from what I can see.
Nice work.

@gz
gz marked this pull request as ready for review July 26, 2026 07:18

@mythical-fred mythical-fred 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.

Full pass on the ready-for-review tip (ae5664df).

The four questions from my draft review are addressed:

  1. Role revocation vs in-flight requests — the role is resolved once per request in auth_validator/api_key_auth, so a revocation caught between validate and handler cannot revert an in-flight request. That is the accepted RBAC latency across most systems; worth an explicit note somewhere (rustdoc on AuthenticatedPrincipal?) so a future reader does not expect strict per-op enforcement.
  2. Structured audit sink — audit now routes through tracing at debug, which is fine as an internal sink; a structured downstream target can subscribe to the audit: span key later without touching the enforcement path.
  3. Role capped at creator's role — enforced at write time in three places (api_key.rs::post_api_key, api_oidc_trust.rs::post_oidc_trust, tenant.rs::check_grantable_role), all raising RoleExceedsCreator. Owner path is gated separately by requiring the caller be an owner (via ROUTE_MIN_ROLE) and by the CHECK constraint on the trust table. Good.
  4. Owner cross-tenant switching — owner selects via Feldera-Tenant, resolved through resolve_owner_acting_tenant (name/UUID, never creates a tenant). A separate rate limit or opt-in header still seems worth doing eventually — an owner token being replayed to switch tenants at high rate would be indistinguishable from legitimate use here — but it is a defense-in-depth item, not a blocker for merge.

Full-pass verdict: LGTM. Notable strengths:

  • ROUTE_MIN_ROLE + every_registered_v0_route_is_classified + no_stale_route_table_entries + every_v0_operation_documents_its_min_role is the right shape: policy, code, docs, and tests all triangulated. A new endpoint that skips classification fails the build, not production.
  • Merging V35 into a single migration is the right call — the two migrations reference each other and running one without the other would leave the schema in a state RBAC does not tolerate. Existing installs still play forward cleanly (single V35 append, backfill preserves write).
  • claim_matches was rewritten to an ordered-literal scan with a dedicated non-overlap test (literals_do_not_overlap) — that closes the earlier worry about pattern semantics.
  • Model-test of OIDC trust + membership operations is exactly the coverage the draft was missing; the note on why it lives outside the StorageAction enum (stack budget) is well-earned.
  • oidc_trust_owner_is_platform CHECK and the partial unique index on owner-trust names are both in the migration, so an application bug cannot produce an ill-shaped row.
  • No AI-attribution trailers in any commit; rustdoc-fix commit is genuinely rustdoc-only (removes broken intra-doc links, wraps a bare URL, no behavior change).

Two minor observations (non-blocking, feel free to ignore):

  • docs/roles.md line ~48 says an owner trust "outranks any tenant-scoped trust the same token also matches". That is a correct description of the code (match_oidc_trust returns both and the auth path prefers the owner match), but the operational implication — a workload that today runs at write in one tenant will silently upgrade to owner across the platform the moment someone registers a matching owner trust — is subtle. Worth restating in the security-implications box on that page in a follow-up, or possibly logging a warn! when an owner-trust match preempts a tenant-scoped match for the same token, so operators can see it happen.
  • create_tenant/add_tenant_user fall back to provider = "manual" when no AuthConfiguration is attached. The comment says the owner routes are unreachable without auth, which is true today via ROUTE_MIN_ROLE. If the auth-none tenant bootstrap path is ever wired up, a "manual" provider will not match any real iss, so no login will resolve into a tenant created that way. Consider expect("AuthConfiguration present on any /v0 route") instead of a silent fallback, so a future regression breaks noisily.

Keep the subject and audience patterns on an owner trust narrow: only a
platform owner can create one, but a broad pattern promotes every workload it
matches. An owner trust always requires the `Feldera-Tenant` header to name the
tenant to act in.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Restating the operational implication here would help: a workload running today at write in one tenant silently upgrades to owner across the whole platform the moment someone registers a matching owner trust. The rule is correct; consider a warn! in match_oidc_trust (or in the auth path where owner match preempts a tenant match for the same token) so operators can see it happen. Not a blocker.

let provider = req
.app_data::<crate::auth::AuthConfiguration>()
.map(|c| c.provider.issuer().to_string())
.unwrap_or_else(|| "manual".to_string());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The "manual" fallback is unreachable today because ROUTE_MIN_ROLE requires Owner for /v0/tenants and there is no way to be an owner without an AuthConfiguration. If we ever wire up an auth-none tenant bootstrap path, a tenant keyed to provider="manual" would silently never match any real iss. Consider .expect("AuthConfiguration present on any /v0 route") so a future regression breaks loudly. Same at line 326 in add_tenant_user.

None => {
let config = req.app_data::<Config>().cloned().unwrap_or_default();
Err((
AuthenticationError::from(config)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Worth a note in the rustdoc of AuthenticatedPrincipal (or here) that the role is resolved once per request, not per handler op, so a revocation landing between auth_validator and the handler cannot revert an in-flight request. That is the right tradeoff for the RBAC hot path, but a future reader might expect strict per-op enforcement.

@mythical-fred mythical-fred 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.

Fixes the actix match_pattern() misclassification — turns out it was there all along. The bug: actix resolves the matched pattern by path only, so GET .../connectors/{connector_name}/completion_token was reporting POST .../connectors/{connector_name}/{action} as its template and getting denied as unclassified. Correct diagnosis and the right fix.

The new classify(method, path) walks ROUTE_MIN_ROLE bucketed by (method, segment_count) and picks the candidate with the most literal-segment matches. Method is part of the key, so GET .../start no longer sees the POST .../{action} entry. Literal-beats-placeholder tiebreak matches every router convention, and the every_table_entry_resolves_from_a_concrete_path meta-test is exactly the guard you want: the table itself is round-trip-checked, so any future entry whose shape is shadowed will fail the suite rather than silently 403 at runtime.

authorize now takes (method, routed, path, principal) and returns the matched pattern on success so the audit line still logs the template, not the concrete path. routed=false short-circuits so a 404 is still actix's job.

The integration test registers the placeholder route before the literal one (the actual failure ordering) and asserts both the token endpoint and the /{action} endpoint keep their own rules. Good.

Nit, not blocking: candidates_by_shape() allocates a Vec per shape at init and classify walks it linearly. For the current table size that's fine; if the table grows into the hundreds it's worth revisiting. The #[allow(clippy::type_complexity)] is the honest tell that the map value type is dense — a small struct Candidate { pattern, role } would read better than the tuple.

Carrying my APPROVE.

Comment thread docs.feldera.com/docs/get-started/enterprise/authentication/index.mdx Outdated
Comment thread docs.feldera.com/docs/get-started/enterprise/authentication/index.mdx Outdated
Comment thread docs.feldera.com/docs/get-started/enterprise/authentication/index.mdx Outdated
Comment thread docs.feldera.com/docs/get-started/enterprise/authentication/index.mdx Outdated
Comment thread docs.feldera.com/docs/get-started/enterprise/authentication/index.mdx Outdated
Comment thread docs.feldera.com/docs/get-started/enterprise/authentication/index.mdx Outdated
Comment thread docs.feldera.com/docs/get-started/enterprise/authentication/index.mdx Outdated
Comment thread docs.feldera.com/docs/get-started/enterprise/authentication/index.mdx Outdated
Comment thread docs.feldera.com/docs/get-started/enterprise/authentication/index.mdx Outdated
Comment thread docs.feldera.com/docs/get-started/enterprise/authentication/index.mdx Outdated
gz added a commit that referenced this pull request Jul 27, 2026
Lalith's review of #6729. The steps to take before switching authentication on
now open the section as a warning, rather than trailing it; the tenant takeover
is explained in two sentences instead of walking through what the id turns into;
and the surviving-a-provider-change advice states the recommendation outright.

Two departures from the suggested text. The opening no longer says the changes
"move users between tenants", which the review itself catches as contradicting
"users cannot survive a provider change": what moves is the tenant a login lands
in, not the identities. And the web console does not delete the tenant a rename
displaced, so the claim that it does is left out; the list has its own delete.

Signed-off-by: Gerd Zellweger <mail@gerdzellweger.com>

@mihaibudiu mihaibudiu 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.

I will submit reviews progressively as I review

Comment thread crates/fda/src/cli.rs
Comment thread crates/fda/src/cli.rs Outdated
Comment thread crates/fda/src/cli.rs
Comment thread crates/fda/src/cli.rs
Comment thread crates/fda/src/cli.rs
Comment thread crates/fda/src/main.rs
Comment thread crates/fda/src/main.rs
Comment thread crates/fda/src/main.rs
Comment thread crates/fda/src/main.rs
Comment thread crates/fda/src/main.rs
Comment thread crates/pipeline-manager/src/auth.rs Outdated
.iter()
.any(|(t, r)| t.is_none() && *r == Role::Owner)
{
let Some(selector) = header_tenant(&req) else {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I was evaluating some aspects of this PR against our requirements, and a question came to mind.

Let's imagine a new Feldera environment being provisioned with authentication enabled and an owner user configured with an OIDC trust relationship. In this setup, there won't be any tenants yet.

When this owner user makes the first POST request to /v0/tenants, what should they provide as the Feldera-Tenant header in the request?

After briefly looking through the implementation with Claude, my understanding is that if we don't provide anything in the header, we'll get an OwnerTrustNeedsTenant error. On the other hand, if we provide the header with the name of the new tenant we're trying to create, we'll get an UnknownTenantName error.

Is my understanding correct, or is this scenario already properly handled? If it is, what should be provided in the tenant header in this scenario?

@mihaibudiu mihaibudiu 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.

This review includes the api/

Comment thread crates/pipeline-manager/migrations/V35__rbac.sql Outdated
Comment thread crates/pipeline-manager/migrations/V35__rbac.sql Outdated
Comment thread crates/pipeline-manager/migrations/V35__rbac.sql Outdated
-- The auth hot path resolves a federated token by its issuer, so index it.
CREATE INDEX IF NOT EXISTS idx_oidc_trust_issuer ON oidc_trust_relationship (issuer);

-- BEGIN tenant identity

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.

this must be some migration stuff, it's not easy to read.

Comment thread crates/pipeline-manager/src/api/endpoints/oidc_trust.rs Outdated
Comment thread crates/pipeline-manager/src/api/rbac.rs Outdated
Comment thread crates/pipeline-manager/src/api/rbac.rs Outdated
Comment thread crates/pipeline-manager/src/api/rbac.rs
Comment thread crates/pipeline-manager/src/api/rbac.rs Outdated
Comment thread crates/pipeline-manager/src/api/rbac.rs Outdated

@mihaibudiu mihaibudiu 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.

These are comments for the db module

Comment thread crates/pipeline-manager/src/db/operations/utils.rs Outdated
Comment thread crates/pipeline-manager/src/db/types/api_key.rs
Comment thread crates/pipeline-manager/src/db/types/oidc_trust.rs Outdated
use uuid::Uuid;

fn row_to_descr(row: &tokio_postgres::Row) -> Result<OidcTrustDescr, DBError> {
let id: Uuid = row.get(0);

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.

Can't this fail? What is the result if the types do not match?

if res > 0 {
Ok(())
} else {
Err(DBError::UnknownOidcTrust {

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.

what if the tenant_id is actually wrong?

Comment thread crates/pipeline-manager/src/db/storage_postgres.rs
Comment thread crates/pipeline-manager/src/db/storage_postgres.rs
Comment thread crates/pipeline-manager/src/db/test.rs Outdated
Comment thread crates/pipeline-manager/src/db/test.rs Outdated
Comment thread crates/pipeline-manager/src/db/test.rs Outdated
gz added a commit that referenced this pull request Jul 28, 2026
Mihai's review of #6729.

`run_auth_token_command` ran `sh -c`, which Windows has no answer for, though
the release builds ship `fda.exe`. Pick the platform's shell instead, and mark
the three tests that drive POSIX syntax as Unix-only.

The rest is prose. The `--auth-token-command` examples are backquoted, and each
now reads as one line: clap joins a paragraph's lines, so a shell comment above
a command came out run together with it. The tenant help drops "asserts" and the
rename caveat, and says what `--displace-existing` is for, which is making the
rename atomic. A comment records what a `displaced` tenant in the response is.

Signed-off-by: Gerd Zellweger <mail@gerdzellweger.com>

@mythical-fred mythical-fred 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.

APPROVE-carry.

Single new commit 25e19fc4 on top of the prior tip, applying Mihai's review feedback:

  • run_auth_token_command now picks cmd /C on Windows and sh -c elsewhere. Legit: fda.exe ships on Windows but had no shell there. The three POSIX-syntax tests (printf, true, echo ... >&2; exit 3) are correctly gated #[cfg(unix)].
  • The --auth-token-command examples switch from # shell comments (which clap paragraph-joined into one line) to backticked bullets — reads correctly now.
  • Rename doc drops the misleading "asserts" clause and re-frames --displace-existing around atomicity, and a comment on the displaced response field records what it means. Good.

No functional change to RBAC or the tenant surface. Prior APPROVE stands.

gz added a commit that referenced this pull request Jul 28, 2026
Mihai's review of #6729.

No route sits at the "any authenticated principal" tier, so `ROUTE_MIN_ROLE`
holds a `Role` rather than an `Option<Role>`: the lookup, the middleware and the
OpenAPI annotation all lose an arm they never took. A new test pins the one
invariant the methods really carry, that a route which can change something
never admits `read`, with the compile-only and profiling POSTs listed as what
they are.

Admin and owner requests are audited at info now. Those are the requests an
operator has to be able to account for afterwards: admin manages a tenant's
members and trusts, owner acts across tenants including ones it does not belong
to. Read and write traffic is one line per request and stays at debug.

The rest is prose. Implementation notes move from doc comments into the bodies
they describe, HTTP status codes leave the database layer, which has no such
thing, the migration header says what the migration does rather than what the
schema used to lack, and the comment on the trust table's CHECK sits on the
CHECK. `displaced` and the scope `Option` say what they mean, and the API
reference for renaming a tenant is down to what a caller needs.

Signed-off-by: Gerd Zellweger <mail@gerdzellweger.com>

@mythical-fred mythical-fred 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.

APPROVE-carry.

Four new commits since 25e19fc apply Mihai's + lalithsuresh's review feedback across the RBAC surface:

  • a18e3f14 [rbac surface]: ROUTE_MIN_ROLE drops the Option<Role> arm (no route sits at "any authenticated"), so lookup/middleware/OpenAPI shed a case they never took; new invariant test pins that a state-changing route never admits read. Admin/owner audit moved to info (operators need those). Migration header rewritten to say what it does. displaced and the scope Option gain names that match their meaning.
  • 7e80ff7a [owner is config-only]: owner is no longer mintable at runtime. New --owner-trusts / FELDERA_OWNER_TRUSTS accepts [{issuer,subject,audience?}]; the DB row for owner trust is gone, so oidc_trust_relationship.tenant_id becomes NOT NULL, the CHECK is role IN ('read','write','admin'), and the platform query parameter + Option<TenantId> scope disappear from the whole storage layer. Owner naming no tenant now acts in the default tenant (the automation-first-tenant path). GET /v0/config/owners reports configured owners; the admin page swaps its owner-trust panel for a config view.
  • edd7f6dc [owner from login provider]: is_configured_owner || owner_trusts.admits on the browser login path too, closing the "write a trust for your own IdP and it does nothing" trap.
  • 39acde4a [rbac demo]: exercises the new surface — workload token reaches tenant routes with no Feldera-Tenant header, GET /v0/config/owners, role: owner on create_oidc_trust refused as 400 (invalid variant) rather than 403.

Spot-checked:

  • OwnerTrusts newtype + FromStr refuses empty issuer/subject at startup (fail-loud); wildcard patterns still go through claim_matches; no audience = accept any (documented, and there is a test for the empty audiences case).
  • oidc_trust_auth: SSRF/DoS gate consults owner_trusts.names_issuer(iss) in addition to the DB, so a config-only owner issuer doesn't get shut out of key fetches; issuer match is exact (test covers the .evil.test suffix).
  • bearer_auth (login path): only a provider-verified email may match an owners entry (email_verified == true), so an unverified email can't confer owner; owner_trusts.admits also consulted with iss/sub/aud from the login token.
  • resolve_owner_acting_tenant strictly resolves by UUID or name — never creates a tenant, so a typo becomes 404 rather than crossing into the wrong tenant.
  • Migration is idempotent: IF NOT EXISTS, pg_constraint lookup for the old auto-named UNIQUE, feldera.auth_issuer GUC fallback, rank-by-pipeline-count tiebreaker, <name> (<id>) for the losers.
  • 4 new commits × 4 well-composed commit messages, no AI-attribution trailers, Signed-off-by on all. CI checks currently empty (probably queued or run recently); prior tip was green.

Nothing to block on. Mihai's inline nits on the earlier tips have all landed; lalithsuresh's docs suggestions on authentication/index.mdx from 07-27 are on a different set of files and don't fall in this delta.

Comment thread scripts/rbac_up.sh
case "$a" in
--keep-db) KEEP_DB=1 ;;
--rebuild) REBUILD=1 ;;
*) echo "unknown arg: $a"; exit 1 ;;

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.

no usage?
no help?

return resp


def _sequence(responses: Iterable[object]):

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.

this could use some documentation

// A trust that names no issuer or no subject would match on the wrong
// half of the pair, so it is refused at startup rather than at auth time.
assert!(r#"[{"issuer": "", "subject": "x"}]"#.parse::<OwnerTrusts>().is_err());
assert!(r#"[{"issuer": "https://idp.example", "subject": ""}]"#

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.

are these error checks specific enough?

Comment thread scripts/dummy_oidc.py
f'<span class="role-name">{role}</span>'
f'<span class="role-hint">{hint}</span></a>'
)
return f"""<!doctype html>

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.

I would have these in separate html files, but maybe for a demo it doesn't matter

}

#[cfg(test)]
pub(crate) fn for_test(role: Role) -> Self {

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.

this is a strange function

owners
.iter()
.map(|o| o.trim())
// Skip empty entries: a trailing/double comma in FELDERA_OWNERS yields

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.

why FELDERA_OWNERS?

}
};
if header.alg != Algorithm::RS256 {
return unauthorized(format!("Unsupported JWT algorithm {:?}", header.alg), req);

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.

is there a list of the supported ones somewhere?

/// a filter, which is why it is gated on `role == Owner` by the callers.
///
/// The selector is looked up strictly by UUID or name and never creates a
/// tenant; a miss is a 404, so a typo cannot cross into the wrong tenant.

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.

indeed, this layer does not really control the http errors, that's done somewhere else

}
let keys = fetch_issuer_jwks(issuer).await?;
let mut cache = state.issuer_jwk_cache.lock().await;
cache.insert(issuer, keys);

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.

is there expiration information about this entry, or does everything expire periodically?

pub(crate) fn new() -> Self {
Self {
cache: TimedCache::with_lifespan_and_capacity(
DEFAULT_JWK_CACHE_LIFETIME_SECONDS,

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.

I guess this must be the expiration
Isn't this capacity small?

@ryanjdillon ryanjdillon 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.

Great work on this! the RBAC model and OIDC workload-identity trusts address several pain points for us. We're running Feldera with issuerTenant: true and EntraID federated through authentik as our IdP.

A few things we're excited about:

  • OIDC trusts will let us replace static API keys in CI/CD with short-lived tokens
  • The role hierarchy gives us much better control than the current flat Read/Write scopes
  • Default-to-Read for new API keys is a good call

We'd also love to see per-pipeline permissions. We have pipelines with different sensitivity levels sharing a tenant, and currently any user with Write access can modify any pipeline. Ideally:

  • User A can query a pipeline but not modify it
  • User B has full read/write on that pipeline
  • User C has no access to it at all
  • API keys scoped to specific pipelines

We understand this is a bigger lift than tenant-level RBAC, but it would be valuable for teams running shared tenants with mixed workloads. Happy to discuss our use case in more detail if helpful.

@Karakatiza666

Copy link
Copy Markdown
Contributor
image image image image [Screencast From 2026-07-29 23-23-23.webm](https://github.com/user-attachments/assets/65a06f99-1c7e-4c61-a42c-a6898339139b)

Karakatiza666 pushed a commit that referenced this pull request Jul 30, 2026
Lalith's review of #6729. The steps to take before switching authentication on
now open the section as a warning, rather than trailing it; the tenant takeover
is explained in two sentences instead of walking through what the id turns into;
and the surviving-a-provider-change advice states the recommendation outright.

Two departures from the suggested text. The opening no longer says the changes
"move users between tenants", which the review itself catches as contradicting
"users cannot survive a provider change": what moves is the tenant a login lands
in, not the identities. And the web console does not delete the tenant a rename
displaced, so the claim that it does is left out; the list has its own delete.

Signed-off-by: Gerd Zellweger <mail@gerdzellweger.com>
Karakatiza666 pushed a commit that referenced this pull request Jul 30, 2026
Mihai's review of #6729.

`run_auth_token_command` ran `sh -c`, which Windows has no answer for, though
the release builds ship `fda.exe`. Pick the platform's shell instead, and mark
the three tests that drive POSIX syntax as Unix-only.

The rest is prose. The `--auth-token-command` examples are backquoted, and each
now reads as one line: clap joins a paragraph's lines, so a shell comment above
a command came out run together with it. The tenant help drops "asserts" and the
rename caveat, and says what `--displace-existing` is for, which is making the
rename atomic. A comment records what a `displaced` tenant in the response is.

Signed-off-by: Gerd Zellweger <mail@gerdzellweger.com>
Karakatiza666 pushed a commit that referenced this pull request Jul 30, 2026
Mihai's review of #6729.

No route sits at the "any authenticated principal" tier, so `ROUTE_MIN_ROLE`
holds a `Role` rather than an `Option<Role>`: the lookup, the middleware and the
OpenAPI annotation all lose an arm they never took. A new test pins the one
invariant the methods really carry, that a route which can change something
never admits `read`, with the compile-only and profiling POSTs listed as what
they are.

Admin and owner requests are audited at info now. Those are the requests an
operator has to be able to account for afterwards: admin manages a tenant's
members and trusts, owner acts across tenants including ones it does not belong
to. Read and write traffic is one line per request and stays at debug.

The rest is prose. Implementation notes move from doc comments into the bodies
they describe, HTTP status codes leave the database layer, which has no such
thing, the migration header says what the migration does rather than what the
schema used to lack, and the comment on the trust table's CHECK sits on the
CHECK. `displaced` and the scope `Option` say what they mean, and the API
reference for renaming a tenant is down to what a caller needs.

Signed-off-by: Gerd Zellweger <mail@gerdzellweger.com>

@mythical-fred mythical-fred 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.

Copy change reads well and matches actual behavior (login re-adds; provider revoke disables). The rbac_up.sh fix using ${arr[@]+"${arr[@]}"} is the right idiom for bash 3.2 under set -u. LGTM.

@mythical-fred mythical-fred 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.

APPROVE.

The Feldera-Tenant header fix (86ac9f8) is a real one and worth the churn. Three auth paths silently substituted a caller's only match when it named a tenant it wasn't trusted in — the branch that reads the selector was skipped whenever the selector was redundant, which is precisely when the check matters. The rewrite makes the rule uniform across all three: a header is always resolved and checked against the caller's authorized set, its absence keeps the previous "one match is unambiguous" semantics, and the derived-tenant path (issuer/sub fallback) now refuses a header that names anything other than what it derived. The .filter(|s| !s.is_empty()) on the header is quiet good hygiene — an empty header behaves like no header, not like a request to enter the empty-string tenant.

The test module in be1cdb3 is a big net gain — the previous single-manager 18-row script really could not have exercised most of what RBAC does, and the commit message honestly explains why (a token whose iss matches the login provider is verified as a login and never reaches the trust path). Three distinct issuers plus a rogue signer make wrong-key and wrong-issuer distinguishable, the module ordering by filename with serial execution is called out because pytest guarantees exactly that ordering, and the claim-pattern subject prefixing is a nice touch — the previous unfalsifiable-strict-case failure mode is a subtle bug that would only surface in review as "the strict test never runs and always reports the matcher as too permissive."

test_tenant_the_token_does_not_name_is_refused in test_3_negative.py directly pins the behavior the auth.rs commit fixes, which is the ideal shape.

The auto-fmt commit a7fce0d is noise (line-break rewraps in auth.rs and test_5_owner_revocation.py), zero semantic content.

Signed-off-by on both human commits, no AI trailers, mergeable clean.

@mythical-fred mythical-fred 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.

LGTM

gz and others added 6 commits July 31, 2026 08:58
The matrix asserts one bit per route and role, and "not denied" is also what a
path that reaches no route returns. A mistyped probe path passes the whole
module while testing nothing, which this suite has already done once, when the
probes carried a doubled /v0 prefix.

Which statuses carry evidence depends on where authentication and authorization
sit relative to routing, so measure it rather than assume it. Authentication
precedes routing: a path no route serves answers 401 without a token, so the
unauthenticated sweep says nothing about whether a probe reached a route.
Authorization does not: the same path answers 404 to a reader, which is what
makes 403 evidence that a probe resolved to a real gated route.

That leaves the routes requiring `read`, where no role is ever denied and 404
would satisfy every assertion. A parameterless route answers 200 through the
same call path the matrix uses, which covers the URL composition that failed
before; a route with a parameter cannot show it, since it answers 404 whether
the route is missing or only the resource is.

The comment claiming a denied caller is refused ahead of routing was wrong.

Signed-off-by: Gerd Zellweger <mail@gerdzellweger.com>
Signed-off-by: feldera-bot <feldera-bot@feldera.com>
Federated authentication fetches a trust's discovery document and JWKS before it
verifies the token's signature, so an attacker who can register a trust chose an
outbound destination and learned the outcome from the 401 body.

Fetches now carry a destination policy. An issuer the operator named at deploy
time, as the login provider or in an owner trust, may sit on a private network.
A tenant-registered issuer resolves through a filtering DNS resolver that drops
loopback, private, carrier-grade NAT, link-local, and the IPv6 equivalents.
Filtering during resolution rather than before the request leaves no window in
which a hostname resolves to a public address for the check and a private one
for the connection. This is what registration could not do: it validates a URL
once, and DNS moves afterwards.

The discovery document names the second destination, so a tenant-registered
issuer's jwks_uri is held to the same policy.

--allow-internal-tenant-trust-issuers travels with the destination rather than
being read again here. Registration and fetching then apply one value, so the
flag cannot admit an issuer that the fetch would refuse, which would leave an
installation configured and broken.

The RBAC suite runs its issuers on localhost, which is the installation the flag
describes, so it sets the flag, and every trust the scenarios register exercises
that path. The two registration cases asserting an internal address is refused
go: they contradict this configuration, and the default they were checking is
covered against the validator directly in the unit tests.

Reaching an issuer is now `oidc::fetch`, beside the rule for where it may point.
A database that cannot answer the trusted-issuer or trust-matching query keeps
its own status rather than becoming 401: it says nothing about the caller's
credentials, and reporting it as one sends users to re-authenticate during an
outage.

Signed-off-by: Gerd Zellweger <mail@gerdzellweger.com>
…oldown

An unverified token chooses the key ID, and a cache miss fetched discovery and
JWKS on every request. Repeating unknown key IDs therefore turned one inbound
request into two outbound ones, and concurrent misses for the same issuer each
fetched independently. No credential was needed: the token only had to name a
registered issuer.

Refreshes for an issuer now serialize on a gate, so concurrent misses cost one
fetch, and a refresh that did not produce the requested key ID blocks further
fetches for thirty seconds. The cooldown starts before the fetch, so a failing
issuer is retried on the cooldown rather than on every request.

A genuine key rollover still resolves on its first miss, because the cooldown
only applies once a refresh has already run and come back without the key.

Signed-off-by: Gerd Zellweger <mail@gerdzellweger.com>
The route table classified validate_program and the pipeline diff by whether
they change durable state, and neither does, so both sat at read. Both take a
caller-supplied program and hand it to the shared compiler, which spawns a Java
process per request. A key labelled read-only could therefore consume compute
shared across tenants.

Both now require write. Compiling a supplied program is write authority even
when nothing is persisted, and the realistic caller is someone about to deploy.
No web-console flow calls either route, so this costs no console behaviour.

The role is not the whole control: a write user can saturate the compiler just
as easily. Admission control at the compiler remains to be added.

Profiling stays at read. It acts on a pipeline the reader can already observe,
and a bounded duration rather than the role is what limits its cost.

Signed-off-by: Gerd Zellweger <mail@gerdzellweger.com>
The demo runs its issuer on localhost, which a trust registered through the API may not name by default. The flag is what such an installation sets.

Signed-off-by: Gerd Zellweger <mail@gerdzellweger.com>
MonacoEditorRunes awaits the Monaco loader, and the json contribution module
when `jsonSchemas` is set, before it creates the editor. A consumer that
unmounts inside that window leaves Svelte to null the bound container, and
`editor.create` then walks up from null looking for a shadow root and throws
"Cannot read properties of null (reading 'parentNode')".

Nothing awaits the `onMount` promise, so the throw surfaces as an unhandled
rejection after the test that triggered it has already passed. Vitest reports
every test file green and then exits non-zero, which reads as a failure with no
failing test. Record the teardown in `onDestroy` and check it before touching
the DOM.

Unit tests reach the editor only through JSONDialog and MultiJSONDialog, whose
specs this branch adds, so the run fails here and not on main. The new test
closes the dialog while the loader is still in flight; it fails with three
rejections when the guard is removed.

Signed-off-by: Gerd Zellweger <mail@gerdzellweger.com>
A failed assertion in this suite reports what the API answered and nothing
about the manager that answered it. The workflow's `always()` step means to
cover that by dumping `docker logs`, but the manager fixture removes its
container when it tears down, so the step runs after the evidence is gone and
prints nothing. CI showed this on `test_02_pipeline_survives_enabling_auth`:
a 401 where 200 was expected, with no way to tell whether the token, the key
fetch or the trust lookup was at fault.

Capture the manager and issuer logs from a report hook instead. The hook runs
while the test's own fixtures are still alive, which is the last moment those
logs exist, and it attaches them to the failing test rather than to the job.

Raise `RUST_LOG` for `auth` and `oidc` to debug as well. Those modules log the
reason a token is refused at debug, so at info the captured log would have
recorded the 401 and still not explained it.

Signed-off-by: Gerd Zellweger <mail@gerdzellweger.com>

@mythical-fred mythical-fred 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.

Re-approve at 5225e97 (five new commits since a7fce0d).

  • ddc1066e (route matrix): the previous matrix could pass with a mistyped probe path because "not denied" is also what an unrouted path returns, and authentication precedes routing so the unauthenticated sweep is silent about whether a probe reached a route. Split into three tiers keyed to where auth/authz sit relative to routing: 401 has no routing evidence, 403 is evidence for authorized routes with denial-capable roles, and 200 via a parameterless route (GET /pipelines) covers URL composition for the read-only band where no role is denied and 404 would satisfy every assertion. Correcting the earlier comment about denial preceding routing is right — that mismatch was what let the doubled /v0 slip through last time. Nice recovery.

  • a15706d7: fmt.

  • 83701cb5: 5-line --allow-internal addition to rbac_up.sh so the demo registered trust can point at localhost. Matches the "restrict where a registered OIDC trust may point" hardening from 79688122.

  • fac36bea (Monaco unmount race): real bug. MonacoEditorRunes.onMount awaits the loader (and the json contrib when jsonSchemas is set); a consumer that unmounts inside that window leaves the bound container null, and editor.create walks up from null and throws. Nothing awaits the onMount promise so the throw surfaces as an unhandled rejection after the test has already reported green — that's the "vitest all-green then exits non-zero" mode. Fix records teardown in onDestroy and checks it before touching the DOM. The new JSONDialog.svelte.spec.ts closes the dialog while the loader is in flight and confirms three rejections without the guard. Correct diagnosis, minimal fix.

  • 5225e977: diagnostics. Previous CI failure on test_02_pipeline_survives_enabling_auth was a bare 401-vs-200 with no manager logs because the workflow's always() step dumped docker logs after the manager fixture removed its container. Moving the capture to a pytest report hook runs it while the fixture is still alive and attaches logs to the failing test rather than the job; raising RUST_LOG=auth,oidc=debug ensures the 401's actual refusal reason is in the captured log. Good instrumentation.

All five have Signed-off-by, no AI trailers. Single-purpose commits with prose that reads like the author already argued with themselves before writing.

APPROVE.

The manager in CI refuses every token because its JWKS fetch cannot verify the
issuer: `InvalidCertificate(UnknownIssuer)`. Two causes produce that identically,
and they need different fixes: `SSL_CERT_FILE` never reaching the process, so the
image's own root store answers instead, or a bundle that arrives and does not
chain to the issuer's certificate.

Dump the container's view on a failing test: the user it runs as, the value of
`SSL_CERT_FILE`, whether that path is readable there, how many certificates it
holds, and `curl` against the issuer both with the bundle and with the ambient
roots. The `curl --cacert` result is the one that separates the two causes.

Local runs are unaffected: the probe only has something to say when the manager
is a container.

Signed-off-by: Gerd Zellweger <mail@gerdzellweger.com>

@mythical-fred mythical-fred 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.

Two new commits since prior APPROVE @ 5225e977 (07-31 18:57Z).

db8fa85d (Gerd) adds a container-side TLS probe to platform_rbac/manager.py: on failure, pytest_runtest_makereport runs docker exec inside the manager container to print id, the SSL_CERT_FILE path, ls -l of the bundle, and two curls against the primary IdP's discovery URL — one with --cacert, one without. The point is that InvalidCertificate(UnknownIssuer) looks identical whether the bundle never reached the container or reached it and doesn't chain, and curl --cacert is the one output that separates them. Local runs get an empty string (no _container), so this is CI-only diagnostic weight — worth the ~30 lines. Probe URL comes from primary_idp fixture through the report hook, which is the right shape.

12c8e843 (Gerd) is the actual fix that motivated db8fa85d: --private-ca-cert-path didn't reach either OIDC fetch path. Two independent bugs behind that:

  1. The federated JWKS path (oidc_http_client) was already on reqwest but only trusted the platform's rustls roots — the deployment's own CA had nowhere to go. Now CommonConfig::configured_root_certificates() reads the configured CA bundles once at ServerState::new (so authentication never touches the filesystem), and every fetch site threads &[Certificate] through: fetch_jwks_uri_from_discovery, fetch_issuer_jwks, generic_oidc_auth_config, JwkCache::get, fetch_jwk_keys, fetch_jwk_oidc_keys. oidc_http_client calls add_root_certificate for each — reqwest's rustls path merges rather than replacing, so a public IdP still verifies via the platform roots.

  2. The login provider's JWKS fetch was on awc::Client::new(), which — as the commit message explains — silently loses SSL_CERT_FILE and the deployment's roots whenever pipeline-manager is co-built with a crate asking for rustls-0_23-webpki-roots (adapters does), because awc picks one root source at compile time and webpki-roots wins the feature vote. Building pipeline-manager alone kept native roots, so the two builds disagreed — exactly the kind of "works in dev, breaks in the release image" bug that eats afternoons. Fix: fetch_jwk_oidc_keys now goes through oidc_http_client(OidcDestination::OperatorConfigured, extra_roots), sharing the federated path's client. awc and its JsonPayloadError branches drop entirely from this file. Correct move — one client, one root policy, one place to reason about.

AuthError sheds JwkPayload and JwkContentType (awc-shaped) and JwkFetch swaps to reqwest::Error. A malformed key set now surfaces as JwkShape, a failed request as JwkFetch — clean rename. The existing invalid_url test passes &[] for extra roots, correct.

The RBAC suite now names the CA through --private-ca-cert-path (moved into _flags()) instead of SSL_CERT_FILE, which is what a real deployment with a private CA would do and which is what makes the suite regress if this fix is reverted. _ca_bundle() and its SYSTEM_CA_BUNDLES fallback are gone — they existed only because SSL_CERT_FILE replaces rather than adds, which is exactly the semantics --private-ca-cert-path now avoids.

Both commits Signed-off-by, no AI trailers. HTTPS, RBAC and OIDC trust job is green — this fix is exactly what makes it green. The failing Platform Integration Tests job on the tip is unrelated to the auth changes (I can't fetch the log from here to confirm, but the diff has no plausible interaction with the OSS Docker integration matrix; treating as flake unless it recurs). Full APPROVE.

An issuer behind a private CA could not be reached, so no token from it could be
verified and every authenticated request answered 401. `--private-ca-cert-path`
did not help: it reaches the clients built from `CommonConfig`, and neither OIDC
fetch used those.

The login provider's JWKS fetch made this worse by using a default `awc` client.
awc picks one root source at compile time and `rustls-0_23-webpki-roots` wins
over `rustls-0_23-native-roots` whenever both are enabled. `adapters` asks for
the former and `pipeline-manager` for the latter, so building them together, as
the release image does, left the login provider trusting the public web PKI and
nothing else. `SSL_CERT_FILE` was silently ignored, contrary to what this crate's
manifest says it is for. Building `pipeline-manager` alone kept native roots, so
the two builds disagreed.

Fetch the login provider's keys with the same reqwest client the federated path
uses, and give that client the deployment's configured roots on top of the
platform's. reqwest merges root sources instead of choosing between them, so the
result no longer depends on which crates share the build. Startup discovery takes
the same roots, since it runs before any of this and fails the same way.

`AuthError` loses `JwkPayload` and `JwkContentType`, which only the awc path
produced; a malformed key set now reports `JwkShape` and a failed request
`JwkFetch`.

The RBAC suite names the CA through `--private-ca-cert-path` rather than
`SSL_CERT_FILE`, which is what a deployment with a private CA does and what makes
the suite fail when this regresses: reverting the change stops the manager from
starting, because discovery cannot verify the issuer.

Signed-off-by: Gerd Zellweger <mail@gerdzellweger.com>
A failing job in test-integration-platform.yml left its siblings running. The
HTTPS, RBAC and OIDC trust suite could fail in minutes and oss-platform-tests
would still hold a GKE runner for hours.

ci.yml's cancel-if-tests-integration-platform-failed cannot prevent that: it
watches the caller-side invoke-tests-integration-platform job, which stands for
the whole called workflow and reports failure only once every job in it has
finished. build-rust.yml was the only called workflow carrying a sentinel next
to its own job, which is why the scheme looked like it covered everything.

Give each job in test-integration-platform.yml its own sentinel. github.run_id
inside a called workflow names the caller's run, so the cancel takes down the
whole run rather than this workflow alone. Record the rule where ci.yml
documents the scheme, since that is where someone adding a job will look.

Signed-off-by: Gerd Zellweger <mail@gerdzellweger.com>
@gz
gz enabled auto-merge July 31, 2026 23:22
@gz
gz added this pull request to the merge queue Jul 31, 2026

@mythical-fred mythical-fred 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.

Two new commits since prior APPROVE at 12c8e84 (07-31 21:55).

a1222ba (gz) supersedes the previous "Trust the configured CA when fetching OIDC keys" commit with the same title. The Rust code (api/main.rs, auth.rs, config.rs, oidc/fetch.rs) is byte-identical to the version I already approved. The only substantive delta is in python/tests/platform_rbac/manager.py: the _ca_bundle() helper and its SYSTEM_CA_BUNDLES constant are gone (they merged the suite's CA with the system store into a bundle passed via SSL_CERT_FILE), the manager is now pointed at the private CA directly via --private-ca-cert-path={self.ca_cert}, and the SSL_CERT_FILE env entry is dropped from start(). container_tls_view is retargeted to what the container sees of the CA it was pointed at (path + ls -l + curl --cacert) rather than what it sees of a bundle stitched into SSL_CERT_FILE. This is the right cleanup — it matches how a real private-CA deployment configures the manager and keeps the regression signal you built in earlier: revert the fix and discovery stops verifying, the suite fails.

22f9d9f (gz) is a CI-only reliability fix. .github/workflows/ci.yml's single cancel-if-tests-integration-platform-failed sentinel watches the caller-side invoke-tests-integration-platform job, and that stands in for the whole called workflow — it reports failure only after every job inside finishes. So a fast-failing HTTPS/RBAC job would leave oss-platform-tests holding a GKE runner for hours. Adds three per-job sentinels inside test-integration-platform.yml (cancel-if-manager-no-network-failed, cancel-if-manager-https-rbac-failed, cancel-if-oss-platform-tests-failed) that each needs: exactly one target job, if: failure(), and POST a cancel against github.run_id — which inside a called workflow names the caller's run, so the cancel drops the whole run rather than just this file. The scheme is now documented next to ci.yml's existing sentinel comment so anyone adding a job in a called workflow knows to add one here too. Sensible, minimal, well-commented.

Both Signed-off-by, no AI trailers.

Fresh APPROVE (upgrading prior APPROVE @ 12c8e84).

@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Aug 1, 2026
@gz
gz added this pull request to the merge queue Aug 1, 2026
Merged via the queue into main with commit 1876273 Aug 1, 2026
1 check passed
@gz
gz deleted the openid branch August 1, 2026 03:47
@gz

gz commented Aug 1, 2026

Copy link
Copy Markdown
Contributor Author

@ryanjdillon hi thanks for the feedback

We'd also love to see per-pipeline permissions. We have pipelines with different sensitivity levels sharing a tenant, and currently any user with Write access can modify any pipeline. Ideally:
We understand this is a bigger lift than tenant-level RBAC, but it would be valuable for teams running shared tenants with mixed workloads. Happy to discuss our use case in more detail if helpful.

As you point out the roles are currently tied to a tenant. We may be able to relax this in the future so the ask does make sense.

For the scenario you're describing, for now you could also solve it with multiple, separate tenants (e.g., prod, staging, dev etc.). In this case you can give a user different permissions for each tenant. Note that handling tenants also got easier to operate with the operator being able to create/delete/rename tenants.

saxena-dev pushed a commit to saxena-dev/feldera that referenced this pull request Aug 4, 2026
Lalith's review of feldera#6729. The steps to take before switching authentication on
now open the section as a warning, rather than trailing it; the tenant takeover
is explained in two sentences instead of walking through what the id turns into;
and the surviving-a-provider-change advice states the recommendation outright.

Two departures from the suggested text. The opening no longer says the changes
"move users between tenants", which the review itself catches as contradicting
"users cannot survive a provider change": what moves is the tenant a login lands
in, not the identities. And the web console does not delete the tenant a rename
displaced, so the claim that it does is left out; the list has its own delete.

Signed-off-by: Gerd Zellweger <mail@gerdzellweger.com>
saxena-dev pushed a commit to saxena-dev/feldera that referenced this pull request Aug 4, 2026
Mihai's review of feldera#6729.

`run_auth_token_command` ran `sh -c`, which Windows has no answer for, though
the release builds ship `fda.exe`. Pick the platform's shell instead, and mark
the three tests that drive POSIX syntax as Unix-only.

The rest is prose. The `--auth-token-command` examples are backquoted, and each
now reads as one line: clap joins a paragraph's lines, so a shell comment above
a command came out run together with it. The tenant help drops "asserts" and the
rename caveat, and says what `--displace-existing` is for, which is making the
rename atomic. A comment records what a `displaced` tenant in the response is.

Signed-off-by: Gerd Zellweger <mail@gerdzellweger.com>
saxena-dev pushed a commit to saxena-dev/feldera that referenced this pull request Aug 4, 2026
Mihai's review of feldera#6729.

No route sits at the "any authenticated principal" tier, so `ROUTE_MIN_ROLE`
holds a `Role` rather than an `Option<Role>`: the lookup, the middleware and the
OpenAPI annotation all lose an arm they never took. A new test pins the one
invariant the methods really carry, that a route which can change something
never admits `read`, with the compile-only and profiling POSTs listed as what
they are.

Admin and owner requests are audited at info now. Those are the requests an
operator has to be able to account for afterwards: admin manages a tenant's
members and trusts, owner acts across tenants including ones it does not belong
to. Read and write traffic is one line per request and stays at debug.

The rest is prose. Implementation notes move from doc comments into the bodies
they describe, HTTP status codes leave the database layer, which has no such
thing, the migration header says what the migration does rather than what the
schema used to lack, and the comment on the trust table's CHECK sits on the
CHECK. `displaced` and the scope `Option` say what they mean, and the API
reference for renaming a tenant is down to what a caller needs.

Signed-off-by: Gerd Zellweger <mail@gerdzellweger.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.

8 participants