Setup() registers one transient consumer per routing key, and go-messaging-amqp mints a separate randomly named queue per consumer, each drained by its own goroutine. User.Added and Privilege.Added for the same company therefore arrive in any order. Process(*UserAdded) replaced privileges[email][companyID] with an empty CompanyPrivileges{}, so a Privilege.Added processed first lost its privilege — permanently, since Fetch() only runs at service start.
This surfaced as authz-service #826 acceptance-test failures: a newly created company's Admin grant vanished, company-service's CreateCompany completion callback waited out its full 30s HasCompanyPrivilege timeout, and the company page never rendered. Two runs failed on that same 30s wait, in different suites.
Fix
User.Added creates the company entry only when it is missing. This matches authz-service's own aggregate (domain/aggregates.go creates the user entry only when absent) and its read view (on conflict (email, company_id) do nothing) — before this change the cache disagreed with the authority. It also makes a User.Added redelivery harmless.
Verification
CGO_ENABLED=1 go test -race ./... passes; prek run --all-files clean.
Mutation-checked: restoring the old User.Added body makes TestPrivilegeHandler_Process_UserAdded_Keeps_Existing_Privileges fail, so the test pins the behaviour rather than restating it.
New tests cover both delivery orders, and that a re-add after User.Removed restores membership without the revoked privileges.
Review
Go Backend and Security experts reviewed the diff. Both cleared it with no Critical or High findings against the change; Security confirmed it cannot fail open, because only Privilege.Added ever sets a flag. Pre-existing hazards they found are tracked in Ambix rather than folded in here: the four-queue design still allows a stale Privilege.Added after User.Removed to resurrect a grant, revocations published during startup are lost for the process lifetime (Fetch() runs before the consumers exist), and Fetch() merges into the privilege map instead of replacing it. The CLAUDE.md note records the ordering invariant and is explicit that the converse does not hold yet.
Rollout
No wire-format or signature change, so mixed versions run side by side safely. After release, 11 services on v0.6.0 take a patch bump; accounting-service and supplier-invoice-service are still on v0.5.1 and only get it with their Codeberg migration.
## Problem
`Setup()` registers one transient consumer per routing key, and go-messaging-amqp mints a separate randomly named queue per consumer, each drained by its own goroutine. `User.Added` and `Privilege.Added` for the same company therefore arrive in any order. `Process(*UserAdded)` replaced `privileges[email][companyID]` with an empty `CompanyPrivileges{}`, so a `Privilege.Added` processed first lost its privilege — permanently, since `Fetch()` only runs at service start.
This surfaced as authz-service #826 acceptance-test failures: a newly created company's Admin grant vanished, company-service's `CreateCompany` completion callback waited out its full 30s `HasCompanyPrivilege` timeout, and the company page never rendered. Two runs failed on that same 30s wait, in different suites.
## Fix
`User.Added` creates the company entry only when it is missing. This matches authz-service's own aggregate (`domain/aggregates.go` creates the user entry only when absent) and its read view (`on conflict (email, company_id) do nothing`) — before this change the cache disagreed with the authority. It also makes a `User.Added` redelivery harmless.
## Verification
- `CGO_ENABLED=1 go test -race ./...` passes; `prek run --all-files` clean.
- Mutation-checked: restoring the old `User.Added` body makes `TestPrivilegeHandler_Process_UserAdded_Keeps_Existing_Privileges` fail, so the test pins the behaviour rather than restating it.
- New tests cover both delivery orders, and that a re-add after `User.Removed` restores membership without the revoked privileges.
## Review
Go Backend and Security experts reviewed the diff. Both cleared it with no Critical or High findings against the change; Security confirmed it cannot fail open, because only `Privilege.Added` ever sets a flag. Pre-existing hazards they found are tracked in Ambix rather than folded in here: the four-queue design still allows a stale `Privilege.Added` after `User.Removed` to resurrect a grant, revocations published during startup are lost for the process lifetime (`Fetch()` runs before the consumers exist), and `Fetch()` merges into the privilege map instead of replacing it. The CLAUDE.md note records the ordering invariant and is explicit that the converse does not hold yet.
## Rollout
No wire-format or signature change, so mixed versions run side by side safely. After release, 11 services on v0.6.0 take a patch bump; accounting-service and supplier-invoice-service are still on v0.5.1 and only get it with their Codeberg migration.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01XMvdB7bcwn1CrQKM4dCshM
Setup() registers one transient consumer per routing key, and each mints its
own queue drained by its own goroutine, so User.Added and Privilege.Added for the
same company can be processed in either order. Process(*UserAdded) replaced the
company entry with empty privileges, so a Privilege.Added handled first lost its
privilege until the next Fetch(), which only runs at service start.
Create the entry only when it is missing instead, matching authz-service's own
aggregate and read view, which both leave an existing user entry alone. This also
makes a User.Added redelivery harmless.
Seen as authz-service #826 acceptance-test failures: a new company's Admin grant
vanished, so company-service's CreateCompany callback waited out its 30s timeout
and the company page stayed empty.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XMvdB7bcwn1CrQKM4dCshM
govulncheck reports GO-2026-6372 against amqp091-go v1.12.0, pulled in
indirectly: a broker-controlled oversized payload can exhaust memory. Fixed in
v1.13.0; take v1.15.0. The advisory fails CI on main too, and this is the first
build since it was published.
go-messaging-amqp goes to v0.0.5 at the same time, which is what every migrated
service already runs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XMvdB7bcwn1CrQKM4dCshM
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Problem
Setup()registers one transient consumer per routing key, and go-messaging-amqp mints a separate randomly named queue per consumer, each drained by its own goroutine.User.AddedandPrivilege.Addedfor the same company therefore arrive in any order.Process(*UserAdded)replacedprivileges[email][companyID]with an emptyCompanyPrivileges{}, so aPrivilege.Addedprocessed first lost its privilege — permanently, sinceFetch()only runs at service start.This surfaced as authz-service #826 acceptance-test failures: a newly created company's Admin grant vanished, company-service's
CreateCompanycompletion callback waited out its full 30sHasCompanyPrivilegetimeout, and the company page never rendered. Two runs failed on that same 30s wait, in different suites.Fix
User.Addedcreates the company entry only when it is missing. This matches authz-service's own aggregate (domain/aggregates.gocreates the user entry only when absent) and its read view (on conflict (email, company_id) do nothing) — before this change the cache disagreed with the authority. It also makes aUser.Addedredelivery harmless.Verification
CGO_ENABLED=1 go test -race ./...passes;prek run --all-filesclean.User.Addedbody makesTestPrivilegeHandler_Process_UserAdded_Keeps_Existing_Privilegesfail, so the test pins the behaviour rather than restating it.User.Removedrestores membership without the revoked privileges.Review
Go Backend and Security experts reviewed the diff. Both cleared it with no Critical or High findings against the change; Security confirmed it cannot fail open, because only
Privilege.Addedever sets a flag. Pre-existing hazards they found are tracked in Ambix rather than folded in here: the four-queue design still allows a stalePrivilege.AddedafterUser.Removedto resurrect a grant, revocations published during startup are lost for the process lifetime (Fetch()runs before the consumers exist), andFetch()merges into the privilege map instead of replacing it. The CLAUDE.md note records the ordering invariant and is explicit that the converse does not hold yet.Rollout
No wire-format or signature change, so mixed versions run side by side safely. After release, 11 services on v0.6.0 take a patch bump; accounting-service and supplier-invoice-service are still on v0.5.1 and only get it with their Codeberg migration.
🤖 Generated with Claude Code
https://claude.ai/code/session_01XMvdB7bcwn1CrQKM4dCshM
Coverage Report
Total coverage: 98%
Coverage Report
Total coverage: 98%