Documentation
¶
Overview ¶
Package retention defines retention policy and archive entities.
Index ¶
Constants ¶
const DefaultPurgeBatchSize = 5000
DefaultPurgeBatchSize bounds how many events one retention pass loads into memory, so purging a large backlog does not have to fit in RAM all at once.
Variables ¶
var ErrNoChainRecorder = errors.New(
"retention: no chain recorder configured; purging would leave gaps verification " +
"reports as deletions (use WithChainRecorder, or WithUnrecordedPurge to accept that)",
)
ErrNoChainRecorder is returned when an Enforcer would purge events with no ChainRecorder to record the purge in the hash chain.
Purging without a record leaves holes that verification cannot tell apart from an attacker deleting events, so every stream a policy touches reports as broken from then on, and a real deletion hides among them. An Enforcer refuses to do that unless it was built with WithUnrecordedPurge.
var ErrPolicyWithoutApp = errors.New("retention: policy has no app ID; refusing to enforce it")
ErrPolicyWithoutApp is returned when an Enforcer is handed a policy with no AppID. It purges nothing for that policy.
Every list filter reads an empty AppID as every app, so a policy that reaches the enforcer without one is a row nothing should have produced. Guessing what it was meant to cover is not worth an unrecoverable purge.
Functions ¶
This section is empty.
Types ¶
type Archive ¶
type Archive struct {
chronicle.Entity
ID id.ID `json:"id"`
PolicyID id.ID `json:"policy_id"`
Category string `json:"category"`
EventCount int64 `json:"event_count"`
FromTimestamp time.Time `json:"from_timestamp"`
ToTimestamp time.Time `json:"to_timestamp"`
SinkName string `json:"sink_name"`
SinkRef string `json:"sink_ref"` // e.g. S3 key
AppID string `json:"app_id"`
TenantID string `json:"tenant_id,omitempty"`
}
Archive records that a batch of events was archived to cold storage.
type ChainRecorder ¶
type ChainRecorder interface {
// RecordRetention appends a record listing the given events to each of
// their streams and returns the IDs it recorded. Only those may be
// purged. It may return recorded IDs alongside an error, and purging
// those is still correct.
RecordRetention(ctx context.Context, ref audit.RetentionRef, events []*audit.Event) ([]id.ID, error)
}
ChainRecorder records a retention purge in the hash chain before the events are deleted. *chronicle.Chronicle implements it.
type EnforceResult ¶
type EnforceResult struct {
Archived int64 `json:"archived"`
Purged int64 `json:"purged"`
Retained int64 `json:"retained"`
}
EnforceResult contains the results of a retention enforcement run.
type Enforcer ¶
type Enforcer struct {
// contains filtered or unexported fields
}
Enforcer runs retention policies: finds old events, optionally archives them, then purges.
func NewEnforcer ¶
func NewEnforcer(store Store, archiveSink sink.Sink, logger log.Logger, opts ...EnforcerOption) *Enforcer
NewEnforcer creates a retention enforcer. archiveSink may be nil if no archiving is needed.
Without WithChainRecorder or WithUnrecordedPurge, enforcing a policy that selects any events fails with ErrNoChainRecorder and purges nothing.
func (*Enforcer) Enforce ¶
func (e *Enforcer) Enforce(ctx context.Context) (*EnforceResult, error)
Enforce runs every retention policy once, each against its own scope.
This is the entry point for the background scheduler, which legitimately covers all apps and tenants. Each policy still only purges the events its own (AppID, TenantID) owns. Request handlers must call EnforceScope instead, so a caller cannot trigger another tenant's retention.
func (*Enforcer) EnforceScope ¶
EnforceScope runs only the policies owned by the given scope.
A policy the store returns from outside s is skipped, so a store whose ListPolicies ignores the scope still cannot make one caller purge another's events.
func (*Enforcer) EnforceScopeFunc ¶
func (e *Enforcer) EnforceScopeFunc( ctx context.Context, s Scope, keep func(*Policy) bool, ) (*EnforceResult, error)
EnforceScopeFunc is EnforceScope with the caller's own ownership check: only the policies keep reports true for are run. It is applied before anything is read or purged, on top of the scope check EnforceScope already does. A nil keep runs every policy in s.
Use it when the caller's idea of what a viewer owns is stricter than the listing scope, so the policies it enforces are the same ones it shows.
type EnforcerOption ¶
type EnforcerOption func(*Enforcer)
EnforcerOption configures an Enforcer.
func WithChainRecorder ¶
func WithChainRecorder(r ChainRecorder) EnforcerOption
WithChainRecorder records every purge in the hash chain first, so verification reports the removed sequences as retained rather than missing. Pass the *chronicle.Chronicle that writes the streams being purged.
func WithMetrics ¶
func WithMetrics(m gometrics.Metrics) EnforcerOption
WithMetrics counts the events every policy archives and purges on m. In a Forge app pass app.Metrics(), and the series land on /_/metrics as forge_chronicle_events_archived_total and forge_chronicle_events_purged_total. The Forge extension does this for you. A nil m records nothing.
func WithUnrecordedPurge ¶
func WithUnrecordedPurge() EnforcerOption
WithUnrecordedPurge lets an Enforcer with no ChainRecorder purge anyway.
Every purge then leaves gaps that verification reports exactly as it would an attacker's deletion. Use it only for a store whose events are not hash-chained, or where the chain is knowingly not verified.
type ListOpts ¶
ListOpts defines pagination and scope options for listing archives.
Scope is applied by the store, not by the caller after the fact: filtering after LIMIT/OFFSET would silently drop a tenant's rows whenever another tenant's archives fill the first page.
type ListPoliciesOpts ¶
ListPoliciesOpts scopes and bounds a policy listing.
type Policy ¶
type Policy struct {
chronicle.Entity
ID id.ID `json:"id"`
Category string `json:"category"` // "*" for default
Duration time.Duration `json:"duration"`
Archive bool `json:"archive"` // archive before purge?
AppID string `json:"app_id"`
TenantID string `json:"tenant_id,omitempty"`
}
Policy defines how long events of a given category are retained.
A policy is owned by exactly one (AppID, TenantID) pair and may only ever purge events belonging to that pair. An empty TenantID means the policy covers the app's untenanted events, and nothing recorded under any tenant. Tenants do not inherit an app-level policy: only a tenant's own policies purge its events.
type PurgeQuery ¶
type PurgeQuery struct {
// Scope is the policy's owner, and it matches exactly. An empty TenantID
// selects the app's untenanted events only, and an empty AppID selects
// events recorded with no app. Neither is a wildcard, unlike the list
// filters, because a purge cannot be undone.
Scope
// Category is the event category the policy governs.
Category string `json:"category"`
// Before selects events with a timestamp strictly older than this.
Before time.Time `json:"before"`
// Limit bounds how many events are loaded in one pass. Zero means
// DefaultPurgeBatchSize; a negative value means unbounded.
Limit int `json:"limit,omitempty"`
}
PurgeQuery selects the events a retention policy may act on.
func (PurgeQuery) EffectiveLimit ¶
func (q PurgeQuery) EffectiveLimit() int
EffectiveLimit resolves 0 to DefaultPurgeBatchSize and negatives to 0 (meaning "no bound").
type Scope ¶
type Scope struct {
AppID string `json:"app_id,omitempty"`
TenantID string `json:"tenant_id,omitempty"`
}
Scope identifies the owner of retention policies, archives, and the events a policy is allowed to purge.
func (Scope) IsZero ¶
IsZero reports whether the scope names no app and no tenant. When listing policies or archives a zero scope matches everything, so it must only be used by trusted in-process callers such as the background retention scheduler. A PurgeQuery's scope never works that way: see PurgeQuery.
type Store ¶
type Store interface {
// SavePolicy persists a retention policy. A policy is unique per
// (AppID, TenantID, Category); saving one must never overwrite or take
// ownership of another scope's policy for the same category.
SavePolicy(ctx context.Context, p *Policy) error
// GetPolicy returns a retention policy by ID. Callers serving a request
// must check the returned policy's scope before acting on it.
GetPolicy(ctx context.Context, policyID id.ID) (*Policy, error)
// ListPolicies returns retention policies matching opts. Scope filtering is
// applied before pagination.
ListPolicies(ctx context.Context, opts ListPoliciesOpts) ([]*Policy, error)
// DeletePolicy removes a retention policy by ID.
DeletePolicy(ctx context.Context, policyID id.ID) error
// EventsOlderThan returns events the given query selects: older than
// q.Before, in q.Category, and belonging to q.Scope. Implementations must
// match the scope exactly, comparing both AppID and TenantID even when
// they are empty. An empty TenantID selects the app's untenanted events,
// never every tenant's, and an empty AppID selects events with no app.
// Treating an empty value as a wildcard here, the way ListPolicies does,
// lets one policy purge history it does not own.
EventsOlderThan(ctx context.Context, q PurgeQuery) ([]*audit.Event, error)
// PurgeEvents permanently deletes events by IDs (only used by retention enforcer).
PurgeEvents(ctx context.Context, eventIDs []id.ID) (int64, error)
// RecordArchive records that a batch of events was archived.
RecordArchive(ctx context.Context, a *Archive) error
// ListArchives returns archive records matching opts, scoped before
// pagination.
ListArchives(ctx context.Context, opts ListOpts) ([]*Archive, error)
}
Store manages retention policies and archives.