fix!: order privilege events by sequence number and merge snapshots by position
authz_client / test (push) Skipped
authz_client / vulnerabilities (push) Skipped
pre-commit / pre-commit (push) Skipped
authz_client / vulnerabilities (pull_request) Successful in 1m0s
authz_client / test (pull_request) Successful in 1m10s
pre-commit / pre-commit (pull_request) Successful in 3m47s
authz_client / test (push) Skipped
authz_client / vulnerabilities (push) Skipped
pre-commit / pre-commit (push) Skipped
authz_client / vulnerabilities (pull_request) Successful in 1m0s
authz_client / test (pull_request) Successful in 1m10s
pre-commit / pre-commit (pull_request) Successful in 3m47s
The four privilege keys arrive on separate transient queues, so a late Privilege.Added or User.Added could resurrect a revoked grant. Services also fetched /authz before binding their queues, losing revocations published in between. Process now orders events by authz-service's global sequenceNo per (email, company): an event only overrides older facts, User.Removed stamps every privilege, and events without a sequence number fail closed (additions dropped, removals held until the next snapshot). Fetch checks the status, retries 503 while authz-service's read view is behind, reads the X-Authz-Sequence header and merges the snapshot as facts at that position, ignoring snapshots older than one already merged. CompaniesByUser returns [] instead of nil. BREAKING CHANGE: events without SequenceNo no longer grant anything; tests that seed the handler through Process must set SequenceNo. Call Fetch() after conn.Start (ADR-0015). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DVGsVQ8AMFR4NZoxyCoEqS
This commit is contained in:
4 files changed
+693
-142
No files matched your search
@@ -23,10 +23,9 @@ import client "gitea.unbound.se/shiny/authz_client"
|
||||
handler := client.New(client.WithBaseURL("http://authz-service"))
|
||||
|
||||
// Check user privileges
|
||||
privileges := handler.Get(email, companyID)
|
||||
if privileges.Invoicing {
|
||||
// User has invoicing privileges
|
||||
}
|
||||
allowed := handler.IsAllowed(email, companyID, func(p client.CompanyPrivileges) bool {
|
||||
return p.Invoicing
|
||||
})
|
||||
```
|
||||
|
||||
### Privileges
|
||||
@@ -43,4 +42,10 @@ The `CompanyPrivileges` struct contains permission flags:
|
||||
|
||||
### Event Handling
|
||||
|
||||
Registers per-replica (transient) go-messaging-amqp consumers for privilege update events from the authz-service (`Setup()`), keeping the local privilege cache up-to-date. Each routing key gets its own queue, so events for one company arrive in any order — `Process` must never discard state it didn't itself record. `User.Added` therefore creates the company entry only when it is missing: a `Privilege.Added` processed ahead of it would otherwise be overwritten, and nothing re-reads the privilege until the next `Fetch()`. The converse doesn't hold yet — `setPrivileges` still creates an entry for a company it never saw a `User.Added` for, so a `Privilege.*` delivered after `User.Removed` resurrects an all-false membership. Don't combine `Setup()` with go-messaging-amqp's `WithReconnect`: a reconnect declares new per-replica queues, so revocations published during the outage are lost unless `Fetch()` runs again. Services exit on connection loss (`CloseListener`) and re-fetch on start.
|
||||
Registers per-replica (transient) go-messaging-amqp consumers for privilege events from authz-service (`Setup()`). Each routing key gets its own queue, so events arrive in any order. `Process` orders them by the `sequenceNo` authz-service's event store stamps on every event: all four events come from authz-service's `Company` aggregate, so the global sequence number orders them within a company. Each (email, company) keeps the sequence number of the last fact about membership and about each privilege; an event only overrides older facts. `User.Removed` stamps every privilege, so a late grant can't come back. An event without `sequenceNo` fails closed: additions are dropped, and removals apply and block every later event for those facts until the next snapshot. An event with a negative or implausibly large `sequenceNo` is dropped. Tests that seed the handler must set `SequenceNo`.
|
||||
|
||||
### Startup
|
||||
|
||||
Call `Fetch()` **after** `conn.Start` (consumers bound), never before: events published between the snapshot and the binding would otherwise be lost. authz-service only serves a snapshot whose read view has applied every stored event. It sends that position in the `X-Authz-Sequence` header and answers 503 while the read view lags (backlog, backfill, reset); `Fetch` retries 503 for about a minute. `Fetch` merges the snapshot as facts at that sequence number (newer event facts win, pairs missing from the snapshot are removed) and drops later events at or below it. A snapshot older than one already merged is ignored, so concurrent or repeated fetches are safe. A snapshot without the header merges at 0 and logs a warning (rollout window only).
|
||||
|
||||
Don't combine `Setup()` with go-messaging-amqp's `WithReconnect`: a reconnect declares new per-replica queues, so revocations published during the outage are lost unless `Fetch()` runs again. Services exit on connection loss (`CloseListener`) and re-fetch on start.
|
||||
Reference in new issue
Block a user