oscerd commented on code in PR #27185:
URL: https://github.com/apache/camel/pull/27185#discussion_r4155944033


##########
components/camel-openfga/src/main/docs/openfga-component.adoc:
##########
@@ -320,11 +320,51 @@ No check is registered for an injected `openFgaClient`, 
which can point anywhere
 about. Set `healthCheckProducerEnabled=false` on the component, or 
`healthCheckEnabled=false` on the policy, to
 turn them off.
 
+=== Contextual tuples and condition context
+
+`contextualTuples` hands OpenFGA relationships that are true for one request 
and never stored — a group membership
+that lives in the token rather than in the graph, or a fact about the request 
such as which network it arrived on.
+It takes semicolon-separated `user,relation,object` triples, each part a 
Simple expression:
+
+[source,java]
+------------------------------------------------------------
+to("openfga:check?storeId={{fga.store}}&relation=reader"
+   + "&user=user:${exchangeProperty.CamelKeycloakTokenSubject}"
+   + "&object=document:${header.documentId}"
+   + 
"&contextualTuples=user:${exchangeProperty.CamelKeycloakTokenSubject},member,team:${header.team}");

Review Comment:
   You are right, and it is worse than an imperfect example: the doc 
contradicts itself. Three paragraphs below, the IMPORTANT block says "an 
expression here which reads an inbound header hands the caller that power 
anyway. Keep these literal, or derive them from something the route 
established" — and then the example above it does exactly the thing it forbids.
   
   Fixed in `77abcd745cd9`. The team now comes from an exchange property the 
route's own token-validating step set, with a comment drawing the line 
explicitly:
   
   ```java
   // both the subject and the team come from exchange properties the route's 
own token-validating step set.
   // the object may come from the request - asking about a document is the 
caller's prerogative - but the
   // granting tuple must not, which is what the IMPORTANT block below is about
   to("openfga:check?storeId={{fga.store}}&relation=reader"
      + "&user=user:${exchangeProperty.CamelKeycloakTokenSubject}"
      + "&object=document:${header.documentId}"
      + 
"&contextualTuples=user:${exchangeProperty.CamelKeycloakTokenSubject},member,"
      + "team:${exchangeProperty.tokenTeam}");
   ```
   
   I left `object=document:${header.documentId}` on a header deliberately. The 
object is the resource being asked about, and "may I read document X" is the 
caller's question to ask — the rule is about the tuple that *grants*, not about 
what is being checked. The comment says so, since the distinction is the thing 
a reader is most likely to flatten into "never use headers".
   
   I also swept the rest of the page and the ITs for other granting expressions 
fed from inbound headers and found none, so this was the only instance rather 
than a pattern.
   
   PR description updated to match.
   
   _Claude Code on behalf of oscerd_



##########
components/camel-openfga/src/test/java/org/apache/camel/component/openfga/OpenFgaContextualTuplesTest.java:
##########
@@ -0,0 +1,194 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.openfga;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+
+import dev.openfga.sdk.api.client.OpenFgaClient;
+import dev.openfga.sdk.api.client.model.ClientCheckRequest;
+import dev.openfga.sdk.api.client.model.ClientCheckResponse;
+import dev.openfga.sdk.api.client.model.ClientListObjectsResponse;
+import dev.openfga.sdk.api.client.model.ClientTupleKey;
+import org.apache.camel.BindToRegistry;
+import org.apache.camel.Exchange;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+class OpenFgaContextualTuplesTest extends CamelTestSupport {
+
+    private static final String STORE = "01HQMVAJXYZ0000000000000";
+    private static final String BASE = "?openFgaClient=#fgaClient&storeId=" + 
STORE;
+
+    @BindToRegistry("fgaClient")
+    private final OpenFgaClient client = mock(OpenFgaClient.class);
+
+    @BindToRegistry("myContext")
+    private final Map<String, Object> conditionContext = Map.of("hour", 14, 
"onCorpNetwork", true);
+
+    private void givenVerdict(Boolean allowed) throws Exception {
+        ClientCheckResponse response = mock(ClientCheckResponse.class);
+        when(response.getAllowed()).thenReturn(allowed);
+        when(client.check(any(ClientCheckRequest.class), 
any())).thenReturn(CompletableFuture.completedFuture(response));
+    }
+
+    private ClientCheckRequest captureCheck() throws Exception {
+        ArgumentCaptor<ClientCheckRequest> captor = 
ArgumentCaptor.forClass(ClientCheckRequest.class);
+        verify(client).check(captor.capture(), any());
+        return captor.getValue();
+    }
+
+    @Test
+    void sendsTheConfiguredContextualTuplesWithTheCheck() throws Exception {
+        givenVerdict(Boolean.TRUE);
+
+        Exchange out = template.request(
+                "openfga:check" + BASE + 
"&relation=reader&user=user:anne&object=document:budget"
+                                        + 
"&contextualTuples=user:anne,member,team:eng;user:anne,on_network,network:corp",
+                e -> {
+                });
+
+        assertThat(out.getException()).isNull();
+        assertThat(captureCheck().getContextualTuples())
+                .extracting(ClientTupleKey::getUser, 
ClientTupleKey::getRelation, ClientTupleKey::getObject)
+                .containsExactly(
+                        org.assertj.core.groups.Tuple.tuple("user:anne", 
"member", "team:eng"),

Review Comment:
   Fixed in `77abcd745cd9` — checked for a clash first and there is none, so 
both became plain imports: `import static org.assertj.core.groups.Tuple.tuple;` 
and `import dev.openfga.sdk.api.client.model.ClientListObjectsRequest;`. The 
file imports `ClientTupleKey` but nothing named `Tuple`, so the static import 
is unambiguous.
   
   Worth noting for anyone relying on the automated side of that rule: 
OpenRewrite's FQCN shortening did not flag either of these, even though the 
build runs it and CI fails on uncommitted shortenings. Introducing a *static* 
import is not a shortening it performs, and the 
`ArgumentCaptor.forClass(X.class)` form evidently did not trip it either. So 
the import-style rule still needs a human reading the diff — thanks for being 
that.
   
   106 unit + 13 integration tests green after the change.
   
   _Claude Code on behalf of oscerd_



-- 
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]

Reply via email to