Unbound Release / Check Preconditions (push) Successful in 27s
Unbound Release / Create Tag (push) Skipped
Unbound Release / Create Release (push) Successful in 27s
authz_client / test (push) Successful in 1m10s
authz_client / vulnerabilities (push) Successful in 53s
Unbound Release / Generate Changelog and Handle PR (push) Successful in 41s
Release / release (push) Successful in 1m10s
pre-commit / pre-commit (push) Successful in 3m12s
## Why The privilege cache could keep a grant authz-service had revoked: - **Unordered keys:** each routing key has its own transient queue, so a late `Privilege.Added`/`User.Added` resurrected a revoked grant. - **Startup gap:** services fetched `/authz` before binding their queues, so revocations published in between were lost until restart. Design: ADR-0015 (docs PR, Proposed). ## What - `Process` orders events by authz-service's global `sequenceNo` per (email, company). All four events come from the Company aggregate, so seq order equals commit order. An event only overrides older facts, and `User.Removed` stamps every privilege. - Events without a sequence number fail closed: additions are dropped, and removals hold until the next snapshot. Negative or huge sequence numbers are dropped. - `Fetch` checks the status and retries 503 (60×1 s, 30 s HTTP timeout). It reads `X-Authz-Sequence`, merges the snapshot as facts at that position, and raises a floor; snapshots older than the floor are ignored. A missing header merges at 0 with a warning (rollout window only). - `CompaniesByUser` returns `[]`, and unknown privileges create no state. CLAUDE.md is rewritten. **BREAKING:** `Process` without `SequenceNo` no longer grants, so service tests must set it. Services must call `Fetch()` after `conn.Start`. ## Verification - `go test -race`: 98.3% coverage, including table-driven reorderings, snapshot-merge cases and a revocation-during-Fetch race test. - 26 mutants on the ordering, merge and retry checks: all killed (each compiled and produced `--- FAIL`). - prek passes. **Expert review:** two rounds. Round 1: Security, Go Backend, Event Sourcing and Database experts reviewed both diffs. Round 2: Security and Event Sourcing re-reviewed the fixes. A final Event Sourcing review covered the committed-events wrapper. Fixed from the reviews: older snapshots merged after newer ones (floor check); a lagging read view serving snapshots that miss revocations (503 plus catch-up); a reset's TRUNCATE emptying a REPEATABLE READ snapshot (LOCK TABLE privileges, verified on PostgreSQL); seq-0 removals undone by older additions (pending stamp); late-committing events skipped by read view backfills (CommittedEventStore with LOCK TABLE events IN SHARE MODE, verified on PostgreSQL 18); an unbounded lock wait (lock_timeout plus retries); catch-up firing on ordinary lag (5 s stall, 10 s cooldown, 10 min deadline); plus smaller items (unknown privileges, invalid seqs, `require` in a goroutine, wrapped errors, `[]` not nil). **Deliberately deferred (tracked in Ambix):** stored-but-unpublished revocations (user decision: authz-service outbox); the readview library's commit-order gap for other services (upstream); removing the missing-header fallback and alerting on /authz 503s; moving Fetch after conn.Start in the 13 consumers (separate bump PRs). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01DVGsVQ8AMFR4NZoxyCoEqS Reviewed-on: https://gitea.unbound.se/shiny/authz_client/pulls/333
70 lines
1.8 KiB
Go
70 lines
1.8 KiB
Go
package client
|
|
|
|
// UserAdded is the event sent when a new user is added to a company.
|
|
// SequenceNo is authz-service's global event sequence number; it orders the events.
|
|
type UserAdded struct {
|
|
Email string `json:"email"`
|
|
CompanyID string `json:"companyId"`
|
|
SequenceNo int `json:"sequenceNo"`
|
|
}
|
|
|
|
// UserRemoved is the event sent when a user is removed from a company
|
|
type UserRemoved struct {
|
|
Email string `json:"email"`
|
|
CompanyID string `json:"companyId"`
|
|
SequenceNo int `json:"sequenceNo"`
|
|
}
|
|
|
|
// Privilege is an enumeration of all available privileges
|
|
type Privilege string
|
|
|
|
const (
|
|
PrivilegeAdmin = "ADMIN"
|
|
PrivilegeCompany = "COMPANY"
|
|
PrivilegeConsumer = "CONSUMER"
|
|
PrivilegeTime = "TIME"
|
|
PrivilegeInvoicing = "INVOICING"
|
|
PrivilegeAccounting = "ACCOUNTING"
|
|
PrivilegeSupplier = "SUPPLIER"
|
|
PrivilegeSalary = "SALARY"
|
|
)
|
|
|
|
var AllPrivilege = []Privilege{
|
|
PrivilegeAdmin,
|
|
PrivilegeCompany,
|
|
PrivilegeConsumer,
|
|
PrivilegeTime,
|
|
PrivilegeInvoicing,
|
|
PrivilegeAccounting,
|
|
PrivilegeSupplier,
|
|
PrivilegeSalary,
|
|
}
|
|
|
|
func (e Privilege) IsValid() bool {
|
|
switch e {
|
|
case PrivilegeAdmin, PrivilegeCompany, PrivilegeConsumer, PrivilegeTime, PrivilegeInvoicing, PrivilegeAccounting, PrivilegeSupplier, PrivilegeSalary:
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (e Privilege) String() string {
|
|
return string(e)
|
|
}
|
|
|
|
// PrivilegeAdded is the event sent when a new privilege is added
|
|
type PrivilegeAdded struct {
|
|
Email string `json:"email"`
|
|
CompanyID string `json:"companyId"`
|
|
Privilege Privilege `json:"privilege"`
|
|
SequenceNo int `json:"sequenceNo"`
|
|
}
|
|
|
|
// PrivilegeRemoved is the event sent when a privilege is removed
|
|
type PrivilegeRemoved struct {
|
|
Email string `json:"email"`
|
|
CompanyID string `json:"companyId"`
|
|
Privilege Privilege `json:"privilege"`
|
|
SequenceNo int `json:"sequenceNo"`
|
|
}
|