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


##########
core/metadata/src/stm/authz.rs:
##########
@@ -78,7 +87,33 @@ where
     if let Some(denied) = authorize(&prepare, mux.users(), mux.streams()) {
         return Ok(denied);
     }
-    mux.update(prepare)
+    if let Some(rejected) = reject_dangling_grant(&prepare, mux.users(), 
mux.streams()) {
+        return Ok(rejected);
+    }
+    let header = prepare.header();
+    let timestamp = IggyTimestamp::from(header.timestamp);
+    let context = ApplyContext::from(header);
+    let reply = mux.update(prepare)?;
+    if let Some(deleted) = reply.deleted_resource {
+        // Publish the pruned grants before a later op can reuse the resource 
ID.
+        // Peer shards may see the deletion first, but cannot see its 
replacement yet.
+        mux.users().remove_grants(deleted, timestamp, context);
+    }
+    Ok(reply)
+}
+
+/// An admitted request can commit after its user was deleted or disabled.
+/// Check the replicated session table before resolving the reused user ID.
+/// Partition completions use a synthetic session without a client-table entry.
+pub(crate) fn reject_stale_session(
+    header: &PrepareHeader,
+    client_table: &ClientTable,
+) -> Option<ApplyReply> {
+    if message_bus::is_server_internal_client(header.client) {

Review Comment:
   This exemption trusts an id the client picks. 
`handle_login_register_request` takes `request.header().client` verbatim 
(`core/server/src/dispatch/session_ops.rs:1101`), `validate_request_fields` 
only refuses 0 (`core/binary_protocol/src/consensus/header.rs:507`), and every 
later op is stamped with that id (`core/server/src/dispatch/mod.rs:968-971`). 
`ConsensusSession::with_client_id` is public, so a client can log in with an id 
in `u128::MAX << 64`.
   Such a session skips this check on every op, which reopens the takeover this 
PR closes: alice's `CreatePersonalAccessToken` queued behind 
`DeleteUser(alice)` and a `CreateUser(bob)` that reuses her id commits as bob 
(`core/metadata/src/stm/user.rs:836-858`), and alice gets back a token that 
logs in as bob.
   The previous allowlist (`CompleteConsumerGroupRevocation`, 
`CompleteLifecycle`) could not be forged, since clients cannot send either op 
(`core/metadata/src/impls/metadata.rs:4010`). My earlier comment here suggested 
the client-id exemption; that was wrong without an ingress guard, sorry.
   Fix: refuse internal-range ids at every Register entry (see 
`register_preflight`), and exempt only an explicit list of server-only 
operations. "Not client-allowed" is not enough, `TruncatePartition` is admitted 
from clients with the caller's session. A regression test for a Register plus a 
stale op from an internal-range id would pin it.



##########
core/consensus/src/metadata_helpers.rs:
##########
@@ -258,11 +260,14 @@ where
     if !is_caught_up_primary(consensus) {
         return PreflightOutcome::NotReady;
     }
-    let Ok(verifier) = verifier_body.try_into() else {
+    let Ok(registration) = RegisterRequest::decode_from(verifier_body) else {
         return PreflightOutcome::Reject(IggyError::InvalidCommand.as_code());
     };
+    if !account_is_active(registration.user_created_at) {

Review Comment:
   This is one of the places to refuse 
`message_bus::is_server_internal_client(client_id)` with a terminal error, 
together with the login handler 
(`core/server/src/dispatch/session_ops.rs:1101`) and 
`submit_register_in_process` (`core/metadata/src/impls/metadata.rs:2414`). 
Every later op inherits the id from the bound session, so a Register gate 
covers them all at no per-op cost.
   Not in `validate_request_fields`: the server builds its own request frames 
with `AUTO_COMMIT_CLIENT_ID`, and those go through it too.
   The same gate also closes two holes that are on master already. A login with 
`u128::MAX` is treated as the auto-commit client and skips the consumer group 
owner/session match (`core/partitions/src/iggy_partition.rs:1092-1096`), so it 
can store or delete offsets of groups it is not a member of, within its own 
grants. A login with the exact `server_internal_client_id(key)` of a completion 
keeps `pipeline_has_message_from_client` true and stalls that completion with 
`InProgress` (`core/metadata/src/impls/metadata.rs:2987`).
   SDKs that mint random ids (C#, Java, Node, Go) should then clear the top bit 
so an honest client never hits the refusal; Rust, Python and C++ use UUIDv4 and 
are already outside the range.



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