Skip to content

Fix blank externalId filter and no-op object-group removal events - #77

Merged
ianmuchyri merged 2 commits into
edgefrom
fix-external-id-filter-and-noop-group-events
Aug 12, 2026
Merged

Fix blank externalId filter and no-op object-group removal events#77
ianmuchyri merged 2 commits into
edgefrom
fix-external-id-filter-and-noop-group-events

Conversation

@ianmuchyri

Copy link
Copy Markdown
Contributor

Summary

Two P2 findings from review of #69, both confirmed real and fixed.

1. A blank externalId filter matched the unfiltered set, not nothing.

normalize_external_id collapsed both "filter omitted" (None) and
"filter explicitly blank/whitespace" (Some(" ")) down to None.
The SQL callers (authorized_object_ids's entity path, and
identity::repo::list_entities) bind that as col IS NULL OR col = $n, so a caller who passed a blank externalId — a mistake, or an
attempt to match nothing — got back the full unfiltered
authorized/listed set instead of zero rows.

Fix: every Some(_) input now stays Some(_). Blank, NUL-bearing,
and over-cap values normalize to a sentinel longer than
MAX_EXTERNAL_ID_LEN, which chk_entities_external_id_length
guarantees no stored row can equal — the same "unmatchable value"
approach the function already used for NUL bytes. Only a genuinely
omitted None still means "no filter".

2. No-op object-group removals published membership-change events anyway.

remove_entity_from_object_group, clear_entity_object_groups, and
their resource counterparts (authz::repo) discarded the DELETE's
affected-row count and unconditionally published
entity.object_group.remove / entity.object_groups.clear (and the
resource equivalents), even when zero rows were deleted — e.g.
removing a membership that never existed, or clearing an
already-empty set. Event consumers were told a change happened when
nothing changed.

Fix: delete_entity_object_groups_in_tx /
delete_resource_object_groups_in_tx now return the deleted row
count, and the four *_with_audit callers skip the event publish
(but still commit) when it's zero — the same shape already used by
add_entity_to_object_group_with_audit /
add_resource_to_object_group_with_audit on the additive side, and
by remove_group_member_with_audit for principal-group membership.

Test plan

  • cargo build
  • cargo test --lib (unit tests, no DB)
  • cargo clippy --all-targets -- -D warnings
  • cargo fmt --check
  • Full DB-gated suite run the way CI runs it — each test binary
    against its own freshly migrated database, single-threaded
    (.github/workflows/rust.yml's run_one loop) — all green,
    including the new tests:
    - models::external_id::tests::normalize_keeps_omitted_and_blank_distinct
    - m31_entity_external_id::a_blank_external_id_filter_matches_nothing_via_authorized_listing
    - m31_entity_external_id::a_blank_external_id_filter_matches_nothing_via_list_entities
    - m32_authorized_object_ids_filters::blank_external_id_matches_nothing_not_everything
    - m30_group_membership_many_to_many::removing_a_membership_that_never_existed_publishes_no_event
    - m30_group_membership_many_to_many::removing_an_existing_membership_still_publishes_its_event
    - m30_group_membership_many_to_many::clearing_an_already_empty_entity_membership_set_publishes_no_event
    - m30_group_membership_many_to_many::removing_a_resource_membership_that_never_existed_publishes_no_event
    - m30_group_membership_many_to_many::clearing_an_already_empty_resource_membership_set_publishes_no_event

normalize_external_id collapsed both "filter omitted" and "filter
explicitly blank/whitespace" to None. The SQL callers bind that as
`col IS NULL OR col = $n`, so a caller who passed a blank externalId
filter (a mistake, or an attempt to match nothing) got back the full
unfiltered authorized/listed set instead of zero rows.

Every Some(_) input now stays Some(_): blank, NUL-bearing, and
over-cap values normalize to a sentinel longer than
MAX_EXTERNAL_ID_LEN, which the length CHECK constraint guarantees no
stored row can equal. Only a genuinely omitted None still means "no
filter".

Signed-off-by: ianmuchyri <ianmuchiri8@gmail.com>
remove_entity_from_object_group, clear_entity_object_groups, and
their resource counterparts discarded the DELETE's affected-row
count and always published entity.object_group.remove /
entity.object_groups.clear (and the resource equivalents), even when
zero rows were deleted — e.g. removing a membership that never
existed, or clearing an already-empty set. Event consumers were told
a change happened when nothing changed.

delete_entity_object_groups_in_tx and
delete_resource_object_groups_in_tx now return the deleted row
count, and the four *_with_audit callers skip the event publish (but
still commit) when it's zero — the same shape already used by
add_entity_to_object_group_with_audit / add_resource_to_object_group_with_audit
for the additive side, and by remove_group_member_with_audit for
principal-group membership.

Signed-off-by: ianmuchyri <ianmuchiri8@gmail.com>
@ianmuchyri
ianmuchyri merged commit c87d2ca into edge Aug 12, 2026
@ianmuchyri
ianmuchyri deleted the fix-external-id-filter-and-noop-group-events branch August 12, 2026 11:30
ianmuchyri added a commit that referenced this pull request Aug 13, 2026
* Keep a blank externalId filter distinct from an omitted one

normalize_external_id collapsed both "filter omitted" and "filter
explicitly blank/whitespace" to None. The SQL callers bind that as
`col IS NULL OR col = $n`, so a caller who passed a blank externalId
filter (a mistake, or an attempt to match nothing) got back the full
unfiltered authorized/listed set instead of zero rows.

Every Some(_) input now stays Some(_): blank, NUL-bearing, and
over-cap values normalize to a sentinel longer than
MAX_EXTERNAL_ID_LEN, which the length CHECK constraint guarantees no
stored row can equal. Only a genuinely omitted None still means "no
filter".

Signed-off-by: ianmuchyri <ianmuchiri8@gmail.com>

* Suppress object-group membership events on no-op removals

remove_entity_from_object_group, clear_entity_object_groups, and
their resource counterparts discarded the DELETE's affected-row
count and always published entity.object_group.remove /
entity.object_groups.clear (and the resource equivalents), even when
zero rows were deleted — e.g. removing a membership that never
existed, or clearing an already-empty set. Event consumers were told
a change happened when nothing changed.

delete_entity_object_groups_in_tx and
delete_resource_object_groups_in_tx now return the deleted row
count, and the four *_with_audit callers skip the event publish (but
still commit) when it's zero — the same shape already used by
add_entity_to_object_group_with_audit / add_resource_to_object_group_with_audit
for the additive side, and by remove_group_member_with_audit for
principal-group membership.

Signed-off-by: ianmuchyri <ianmuchiri8@gmail.com>

---------

Signed-off-by: ianmuchyri <ianmuchiri8@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant