oscerd opened a new pull request, #26974: URL: https://github.com/apache/camel/pull/26974
Fixes [CAMEL-25028](https://issues.apache.org/jira/browse/CAMEL-25028). ## Why Camel has two of the three pieces of an authorization story: `camel-spiffe` establishes **who the caller is**, and `camel-opa` evaluates **what the rules say**. The missing piece is **what the caller's relationship to the resource is**. Policy-as-code is a poor fit for that question — expressing "anne may read `document:budget` because she owns the folder it lives in" in Rego means shipping the whole relationship graph into the policy input on every message — so routes that need it hand-roll an SDK call in a `.process()` block, which is the situation `camel-opa` was created to end. This adds `camel-openfga`, which delegates the question to [OpenFGA](https://openfga.dev) (CNCF, an implementation of Google's Zanzibar paper) and records the answer on the Exchange. It is deliberately built to `camel-opa`'s shape, so an operator who knows one knows the other. ## What **Producer** — `openfga:<operation>`: | Operation | Purpose | |---|---| | `check` | the authorization decision; verdict on `CamelOpenFgaAllowed`, body untouched | | `batchCheck` | filters the body's object identifiers down to the ones the check allowed | | `listObjects` | which objects of a type a subject can reach through a relation | | `listRelations` | which of a set of relations a subject holds on one object | | `listUsers` | which subjects hold a relation on one object | | `writeTuples` / `deleteTuples` | grant and revoke, so a route that creates a resource can grant access to it | **Route enforcement** — `OpenFgaSecurityPolicy`, an `AuthorizationPolicy`, the direct counterpart of `OpaSecurityPolicy`: ```java from("platform-http:/documents") .policy(openFgaPolicy) // a deny throws CamelAuthorizationException .to("direct:serveDocument"); ``` `user`, `object` and `relation` are Simple expressions evaluated per Exchange, so a plain `to()` is enough — no `toD()` and the endpoint-cache churn it brings. ## Security posture - **Fails closed.** An unreachable or erroring server denies. `failOpen` is off by default and annotated `security = "insecure:dev"`. It applies only to `check` and the security policy; `batchCheck`/`listObjects` ignore it, because "proceed" for a filter would mean handing back the objects it never managed to filter. - **The question is not the message's to choose.** `storeId`, `authorizationModelId`, `relation` and the operation come from the endpoint only. An inbound message cannot point the check at another store, pin an older model revision, downgrade the relation demanded from `owner` to `reader`, or turn a check into a tuple write. - **Decision headers are cleared on entry**, before the expressions are evaluated, written on every evaluation, and never read back as inputs — so a verdict a message arrived with never survives, including down the paths that throw (the CAMEL-24754 lesson). - **A missing identity is a deny, not a failure.** If `user` or `object` resolves to blank — or to a bare `user:` prefix, which is what a configured prefix plus an unresolved expression leaves — the Exchange is denied and `failOpen` does not reach it. OpenFGA would answer HTTP 400, which *is* a failure, and `failOpen` would then read it as an allow. - **A wildcard subject is refused.** Verified against OpenFGA 1.21.0: `check(user:*, reader, document:public)` returns `allowed: true` wherever a public-access tuple exists. `user:*` is a legitimate *tuple* subject but never a legitimate *checking* subject, so a `user` expression resolving to a typed wildcard is denied rather than sent. The IT asserts both halves of this — that the server really would say yes, and that the component says no. - **No contextual tuples or condition context in this first cut.** A contextual tuple derived from a message is a self-authorization primitive (`(user:me, owner, document:secret)`), so it is left out entirely rather than shipped with a gate that has not been reviewed. Follow-up issue. - `apiToken` and `clientSecret` are marked secret and already match `SensitiveUtils` keywords, so no core change was needed for URI masking. - `sslContextParameters` / `useGlobalSslContextParameters` for TLS, including presenting a SPIFFE X.509-SVID to a server requiring mutual TLS — which closes the loop with `camel-spiffe`. ## Two things found while writing this - **`openfga-sdk` 0.10.1 accepts a `connectTimeout` and never reads it.** `getConnectTimeout()` is called nowhere in the jar, and the `HttpClient.Builder` the SDK uses by default sets none, so the connect phase would be bounded only by the OS. The component therefore supplies its own `ApiClient(HttpClient.Builder)` — which is also the only seam an `SSLContext` can go through, since it can only be set while an `HttpClient` is being built. - **`clientBatchCheck` does not preserve input order** — it fans out in parallel and returns completion order. The producer filters the request list against the allowed set instead of building the result from the responses, so the body comes back in the order the route asked in. Caught by the IT, which failed intermittently until it was fixed. Also: the probe requires the server to report `SERVING`, matched as a whole value, because `"NOT_SERVING".contains("SERVING")` is `true` — a unit test caught that before it could report an unhealthy server as ready. ## Testing - 77 unit tests with a mocked client, covering the verdict paths, every deny reason, stale-header clearing, the `failOpen` boundary, the identifier guards, and all seven operations. - 10 end-to-end integration tests against a real OpenFGA server via a new `camel-test-infra-openfga` module (`mirror.gcr.io/openfga/openfga`, which publishes `amd64` and `arm64` only, so `skipITs.ppc64le` and `skipITs.s390x` are set as for `camel-opa`). The store, model and starting tuples are created through OpenFGA's HTTP API, so what the component does is measured against a graph it did not build. - Documentation in `src/main/docs/openfga-component.adoc`. No upgrade-guide entry: this is a new component, and the upgrade guide is for migration only. One generated change may look out of place: the full reactor adds `apitoken` to `SensitiveUtils` (and to `sensitive-keys.json`) from this component's `secret` metadata, and re-indents the `SENSITIVE-PATTERN: END` marker while rewriting that block. Both come straight from the generator — excluding the re-indent would leave the tree dirty after any build and fail CI's uncommitted-changes check. ## Follow-ups Separate issues once this lands: contextual tuples and condition context on `check` with a reviewed trust model; the `expand`, `readTuples` and `readChanges` operations; store and authorization-model management. A third, unrelated one: `camel-opa`'s documentation and `OpaConfiguration.getIncludeProperties()` both state that `camel-keycloak` and `camel-oauth` "record the identity they verified as an exchange property". They do not — `camel-keycloak` has only a `CamelKeycloakSubjectToken` *header* and sets no exchange properties, and `CamelKeycloakSubject` does not exist anywhere in the codebase. This PR's docs say "a property your authentication step has to set" instead; `camel-opa`'s wording should be corrected separately. --- _Claude Code on behalf of @oscerd_ 🤖 Generated with [Claude Code](https://claude.com/claude-code) -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
