Documentation
¶
Overview ¶
Package audit is the durable record of everything the server did: an append-and-prune store of projected events, with the in-memory and SQL backends behind one contract.
Why ¶
The threat model's repudiation control states that every state change emits an event and that subscribers persist them. Phase 9 made the first half true with event/sink, which decides what an event may say. This package is the second half: the place those records go so a question can be asked afterwards. An slog sink is attributable but only as durable as the log stream someone remembered to ship; proving who erased a device three weeks ago needs a table.
The interface is deliberately append-and-prune. There is no update and no delete by id, because a trail whose rows can be edited answers no question worth asking. The only way a record leaves is age, so retention is a stated policy rather than a way to lose one inconvenient row.
What is stored is the projection from event/sink, never event.Data. The payload of a TokenUpdate carries UnlockToken, the secret that clears a device passcode, so a trail that persisted raw payloads would be a long-lived copy of every device's secrets. Fields holds what the projection allowed and nothing else.
This package owns the contract and the domain type. Persistence is audit/inmem and audit/sqlstore, which keeps its own migration set, and audit/audittest is the suite both must pass.
References ¶
- Decision record 0038: docs/research/decisions/0038-persisted-audit-trail.md
- Decision record 0037: docs/research/decisions/0037-event-sinks-and-redaction.md
- Plan of record: docs/research/implementation_plan.md (phase 9)
- Threat model: docs/security/threat-model.md (repudiation)
- Apple documents no audit surface; what is recorded is the protocol vocabulary cited by service and ddm.
Index ¶
Constants ¶
const DefaultPageSize = 100
DefaultPageSize applies when Page.Limit is not positive.
const MaxPageSize = 1000
MaxPageSize bounds one page. A caller asking for more gets this many, so a cursor is always cheap to serve.
Variables ¶
var ( // ErrNotFound is a record that does not exist. ErrNotFound = errors.New("audit: not found") // ErrInvalid is a malformed argument: a bad cursor, an empty record, a // nil database. ErrInvalid = errors.New("audit: invalid argument") )
Sentinel errors. They mirror the shapes storage and adminauth use so a handler can map them to a status without knowing which store answered.
Functions ¶
This section is empty.
Types ¶
type Page ¶
Page requests one page of records. An empty Cursor starts from the newest; Limit <= 0 uses DefaultPageSize.
type Query ¶
type Query struct {
// Type restricts to one event type.
Type string
// Actor restricts to one actor, which is how "what did this admin do"
// and "what did break-glass do" are asked.
Actor string
// Enrollment restricts to one enrollment id.
Enrollment string
// Since and Until bound the record time. Zero means unbounded.
Since, Until time.Time
}
Query filters a listing. The zero Query matches everything.
type Record ¶
type Record struct {
// ID orders the trail and is the pagination cursor. It is assigned by
// the store, ascending, so the newest record has the highest id.
ID int64
// At is when the event happened, from the publisher's clock.
At time.Time
// Type is the event type, for example "command-queued".
Type string
// Actor is who caused it: "device", "admin", a principal name, or
// "break-glass".
Actor string
// Enrollment is the enrollment the event concerned, zero for events with
// no enrollment such as an admin action.
Enrollment mdm.EnrollmentID
// Fields is the projected payload, stored as JSON.
Fields map[string]any
}
Record is one persisted event. It is the projection from event/sink after it has been reduced to what may leave the process, never the raw payload: a TokenUpdate's payload carries the device unlock token, so the trail stores what the projection allowed and nothing else.
type Store ¶
type Store interface {
// Append writes one record and returns it with its assigned ID.
// ErrInvalid for a record with no type.
Append(ctx context.Context, rec Record) (Record, error)
// List pages the trail newest first, filtered by q. An unparsable
// cursor is ErrInvalid.
List(ctx context.Context, q Query, p Page) (Result[Record], error)
// Get returns one record. ErrNotFound when it does not exist.
Get(ctx context.Context, id int64) (Record, error)
// Prune removes records older than before and returns how many went.
// Retention is the only way a record leaves the trail.
Prune(ctx context.Context, before time.Time) (int, error)
}
Store persists the audit trail.
It is deliberately append-and-prune: there is no update and no delete by id. A trail whose rows can be edited answers no question worth asking, and the only removal is by age, so retention is a policy rather than a way to lose one inconvenient record.
Directories
¶
| Path | Synopsis |
|---|---|
|
Package audittest is the contract every audit backend must satisfy, and a store that fails on demand.
|
Package audittest is the contract every audit backend must satisfy, and a store that fails on demand. |
|
Package inmem is the in-memory audit trail: the backend every unit test uses and the one a deployment without a database falls back to.
|
Package inmem is the in-memory audit trail: the backend every unit test uses and the one a deployment without a database falls back to. |