spetz commented on code in PR #4550:
URL: https://github.com/apache/iggy/pull/4550#discussion_r4276910068


##########
core/metadata/src/stm/user.rs:
##########
@@ -478,6 +546,9 @@ impl StateHandler for CreateUserRequest {
         }
 
         state.index.insert(username_arc, id as UserId);
+        // A token create admitted before the delete of the last holder of this
+        // id can commit after it. Its tokens must not resolve to the new 
holder.
+        state.remove_personal_access_tokens(id as UserId);

Review Comment:
   This sweep covers only a token create that commits before the `CreateUser` 
that takes the freed id. The opposite commit order is also valid. With two 
admin connections, alice's `CreatePersonalAccessToken` and another admin's 
`CreateUser` can both be prepared while `DeleteUser(alice)` is still in the 
pipeline, because admission checks only the client table session and that 
session ends at the delete's commit 
(`core/metadata/src/impls/metadata.rs:858`). 
`CreatePersonalAccessTokenRequest::apply` (`core/metadata/src/stm/user.rs:802`) 
then inserts the hash under the reused id without any owner check, 
`core/server/src/dispatch/mod.rs:1024` splices the raw token into alice's 
reply, and PAT login resolves it to the new holder and returns that holder's 
`created_at` (`core/server/src/dispatch/session_ops.rs:200`), so the 
`created_at` binding this PR adds does not catch it. Alice ends up with a 
working token for bob's account. A unit test in this module that applies 
`DeleteUser(alice)`, `Cre
 ateUser(bob)` at the freed id and then alice's `CreatePersonalAccessToken` 
gets code 0, and the token index resolves the hash to bob. The token is one 
case of a wider gap. `authorize` at apply reads only `header.user_id` 
(`core/metadata/src/stm/authz.rs:156`), so any control-plane op alice had 
admitted in that window, for example a `CreateStream` she holds no grant for, 
commits under bob's grants. The comment above says such ops must not resolve to 
the new holder, but only the token case in one commit order is handled. Both 
orders existed before this PR.



##########
core/server/src/dispatch/authz.rs:
##########
@@ -49,6 +50,22 @@ use server_common::Message;
 
 use crate::shell::{ShellBus, ShellShard};
 
+/// Run a read's `rule` only while the connection's session is still attached.
+///
+/// The funnel resolves the acting user before the read starts, and the session
+/// can end during any await of the read. A read runs this before its first
+/// await and again after its last one, before it builds the reply.
+pub(in crate::dispatch) fn authorize_attached(

Review Comment:
   Binary `POLL_MESSAGES` never goes through `authorize_attached`, although 
this doc and the PR description say binary reads run the permission rule again 
after each wait. `handle_poll_messages` checks the poll rule once 
(`core/server/src/dispatch/partition.rs:552`), then `read_polled_messages` 
awaits the owner shard (`core/server/src/dispatch/partition.rs:703`) and builds 
the reply with no second check. The owner completion at 
`core/shard/src/poll.rs:421` checks the session and the metadata incarnation 
but not grants. A consumer whose poll grant is revoked during that wait still 
receives the messages, and with `auto_commit` the owner has already stored the 
offset by then. `shard_waiting_reads` (`core/server/src/dispatch/mod.rs:1446`) 
leaves out `POLL_MESSAGES_CODE`, so no test covers the poll.



##########
core/server/src/http/handlers.rs:
##########
@@ -1331,6 +1354,7 @@ pub(in crate::http) async fn poll_messages(
     {
         return Ok(response);
     }
+    authorize_read(&state, &identity, consistency, rule)?;

Review Comment:
   This is the last `authorize_read` in `poll_messages`, and it runs before the 
`partition_read` await below, not after it. The PR description says HTTP reads 
run the rule again after their last wait, and the `authorize_read` doc 
(`core/server/src/http/reads.rs:54`) says the account can change during any 
await. `partition_read` always crosses a shard channel and waits up to 
`PARTITION_READ_TIMEOUT` (`core/shard/src/lib.rs:2184`), so a `DeleteUser` or 
`UpdatePermissions` can commit during it, and `build_polled_messages_body` 
still returns the messages to the revoked account.



##########
core/server/src/http/handlers.rs:
##########
@@ -1445,6 +1471,7 @@ pub(in crate::http) async fn get_consumer_offset(
     {
         return Ok(response);
     }
+    authorize_read(&state, &identity, consistency, rule)?;

Review Comment:
   `get_consumer_offset` has the same gap as `poll_messages`. It runs 
`authorize_read` before the `partition_read` await at line 1484 and builds the 
reply after it with no further check. The binary twin checks again after its 
read (`core/server/src/dispatch/partition.rs:858`), so a replaced or revoked 
account is refused over TCP but still gets the offset over HTTP.



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