davsclaus commented on code in PR #27195:
URL: https://github.com/apache/camel/pull/27195#discussion_r4155221089


##########
components/camel-openfga/src/main/java/org/apache/camel/component/openfga/OpenFgaConstants.java:
##########
@@ -74,6 +74,14 @@ public final class OpenFgaConstants {
               javaType = "String")
     public static final String STORE_ID = HEADER_PREFIX + "StoreId";
 
+    @Metadata(label = "producer",
+              description = "The continuation token the readTuples or 
readChanges page came back with, or absent when"

Review Comment:
   For `readChanges` this isn't quite right: when there are no further changes, 
OpenFGA returns the request's continuation token back (the `ErrNotFound` branch 
in `pkg/server/commands/read_changes.go`), so once a token has been sent the 
header is never absent. Maybe: "absent on the last readTuples page; for 
readChanges the token is always returned, and an empty page means the log has 
been read up to date".



##########
components/camel-openfga/src/main/docs/openfga-component.adoc:
##########
@@ -320,12 +320,68 @@ 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.
 
+=== Reading the graph
+
+`readTuples` answers "what access exists", as opposed to `check`'s "may this 
subject do this". Here `user`, `relation`
+and `object` are a *filter* rather than a subject, and the body comes back as 
a list of maps keyed `user`, `relation`,
+`object` and `timestamp` — deliberately the keys `writeTuples` and 
`deleteTuples` accept, so a route can revoke what
+it just read without reshaping anything:
+
+[source,java]
+------------------------------------------------------------
+from("direct:revokeEverythingBobHas")
+        
.to("openfga:readTuples?storeId={{fga.store}}&object=document:&user=user:bob")
+        .to("openfga:deleteTuples?storeId={{fga.store}}");
+------------------------------------------------------------
+
+[NOTE]
+.What OpenFGA accepts as a read filter
+====
+The filter is narrower than "every part is optional". Measured against OpenFGA 
1.21.0:
+
+* no filter at all — reads the whole store, a page at a time;
+* `object=document:` *plus* a `user` — reads that user's tuples of that object 
type;
+* `object=document:budget` — reads that object's tuples, with or without a 
user or relation;
+* `user` alone, `relation` alone, or `object=document:` alone — **rejected**, 
because OpenFGA requires an object type
+  as soon as any filter is given and will not accept an empty object id and an 
empty user together.
+
+The component checks this before the call and names the option to change, 
rather than letting an opaque HTTP 400
+through. Note this is also why a type-only `document:` is accepted here but 
not as an `object` anywhere else: a read
+filter and an identifier have different rules.
+====
+
+`readChanges` reads the change log, which is the building block for keeping a 
cache or a projection in step with the
+graph. Each entry adds an `operation` of `WRITE` or `DELETE`.
+
+Both are paged. `pageSize` bounds one request, and the token the page returns 
arrives on
+`CamelOpenFgaContinuationToken`; feed it back through `continuationToken` for 
the next page:
+
+[source,java]
+------------------------------------------------------------
+from("timer:sync?period=30000")
+        .to("openfga:readChanges?storeId={{fga.store}}&type=document"
+            + "&continuationToken=${exchangeProperty.fgaToken}")

Review Comment:
   Each timer fire is a new exchange, so `${exchangeProperty.fgaToken}` is 
always empty here and every poll starts from the beginning (or from 
`startTime`). Something that survives between exchanges is needed, e.g. 
`.setVariable("global:fgaToken", header("CamelOpenFgaContinuationToken"))` with 
`continuationToken=${variable.global:fgaToken}` (please verify), or a state 
repository if it must survive a restart.



##########
components/camel-openfga/src/main/java/org/apache/camel/component/openfga/OpenFgaProducer.java:
##########
@@ -276,6 +292,207 @@ private void listUsers(Exchange exchange, 
OpenFgaAuthorizer authorizer) throws E
         exchange.getMessage().setBody(identifiers);
     }
 
+    /**
+     * Reads the stored relationship tuples, filtered by whichever of user, 
relation and object the endpoint configured.
+     * <p/>
+     * Unlike every other operation these three are optional here: an unset 
part means "do not filter on this", and
+     * leaving all three unset reads the whole store a page at a time. That is 
why the raw resolvers are used rather
+     * than the check-path ones, which treat a blank value as a missing 
identity and deny.
+     */
+    private void readTuples(Exchange exchange, OpenFgaAuthorizer authorizer) 
throws Exception {
+        String filterUser = trimmedOrNull(authorizer.rawUser(exchange));

Review Comment:
   `trimmedOrNull` makes "configured but resolved to blank" the same as "not 
configured". With `object=document:budget&user=${header.who}` and no `who` 
header, this reads every tuple on `document:budget`, and the documented 
`readTuples -> deleteTuples` route would then revoke all of them. Could 
"unfiltered" be limited to options that aren't set, and a set option that 
resolves blank throw? That matches what `validateReadFilter`'s comment says 
about the write path, and #27185's deny-on-unresolved choice.



##########
components/camel-openfga/src/main/docs/openfga-component.adoc:
##########
@@ -320,12 +320,68 @@ 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.
 
+=== Reading the graph
+
+`readTuples` answers "what access exists", as opposed to `check`'s "may this 
subject do this". Here `user`, `relation`
+and `object` are a *filter* rather than a subject, and the body comes back as 
a list of maps keyed `user`, `relation`,
+`object` and `timestamp` — deliberately the keys `writeTuples` and 
`deleteTuples` accept, so a route can revoke what
+it just read without reshaping anything:
+
+[source,java]
+------------------------------------------------------------
+from("direct:revokeEverythingBobHas")
+        
.to("openfga:readTuples?storeId={{fga.store}}&object=document:&user=user:bob")
+        .to("openfga:deleteTuples?storeId={{fga.store}}");
+------------------------------------------------------------
+
+[NOTE]
+.What OpenFGA accepts as a read filter
+====
+The filter is narrower than "every part is optional". Measured against OpenFGA 
1.21.0:
+
+* no filter at all — reads the whole store, a page at a time;
+* `object=document:` *plus* a `user` — reads that user's tuples of that object 
type;
+* `object=document:budget` — reads that object's tuples, with or without a 
user or relation;
+* `user` alone, `relation` alone, or `object=document:` alone — **rejected**, 
because OpenFGA requires an object type
+  as soon as any filter is given and will not accept an empty object id and an 
empty user together.
+
+The component checks this before the call and names the option to change, 
rather than letting an opaque HTTP 400
+through. Note this is also why a type-only `document:` is accepted here but 
not as an `object` anywhere else: a read
+filter and an identifier have different rules.
+====
+
+`readChanges` reads the change log, which is the building block for keeping a 
cache or a projection in step with the
+graph. Each entry adds an `operation` of `WRITE` or `DELETE`.
+
+Both are paged. `pageSize` bounds one request, and the token the page returns 
arrives on
+`CamelOpenFgaContinuationToken`; feed it back through `continuationToken` for 
the next page:
+
+[source,java]
+------------------------------------------------------------
+from("timer:sync?period=30000")
+        .to("openfga:readChanges?storeId={{fga.store}}&type=document"
+            + "&continuationToken=${exchangeProperty.fgaToken}")
+        .setProperty("fgaToken", header("CamelOpenFgaContinuationToken"))
+        .split(body()).to("direct:applyChange");
+------------------------------------------------------------
+
+The header is *removed* rather than left in place on the last page, so a route 
looping on it stops instead of

Review Comment:
   True for `readTuples`, but not for `readChanges` once a token has been fed 
back: OpenFGA returns the same token with an empty page when there is nothing 
new, so a loop on this header over `readChanges` would never end. Worth 
splitting the sentence by operation.



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