audit

package
v0.4.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 7, 2026 License: MIT Imports: 15 Imported by: 0

Documentation

Overview

Package audit records cross-cutting audit events (authentication outcomes, authorization decisions, OpenFGA tuple changes, business resource mutations) into a unified auditpb.AuditEvent envelope and delivers them through a pluggable Emitter (broker, log, noop). Schema is sourced exclusively from api/gen/go/servora/audit/v1 — there is no runtime↔proto mapper.

API surface

Three groups, top-down by typical caller:

  1. Ctx helpers — written by security middleware, read by Collector: WithAuthnResult(ctx, *auditpb.AuthnDetail) context.Context AuthnResultFrom(ctx) (*auditpb.AuthnDetail, bool) WithAuthzResult(ctx, *auditpb.AuthzDetail) context.Context AuthzResultFrom(ctx) (*auditpb.AuthzDetail, bool) InstallHolder(ctx) context.Context // auto-called by Collector; rarely needed directly

  2. Middleware — assembled by business apps: Collector(rec *Recorder, opts ...CollectorOption) middleware.Middleware WithSpanEvents(enabled bool) CollectorOption // default true

  3. Recorder — direct event emission for non-middleware paths: NewRecorder(emitter Emitter, serviceName string) *Recorder (*Recorder).Emit(ctx, *auditpb.AuditEvent) error (*Recorder).RecordResourceMutation(...) // proto-annotation-driven middleware uses this (*Recorder).RecordTupleChange(...) // infra/openfga write path uses this

Audit event sources topology

The four auditpb.AuditEventType values originate from different modules:

AUTHN_RESULT       ← security/authn middleware writes ctx, Collector emits
AUTHZ_DECISION    ← security/authz middleware writes ctx, Collector emits
RESOURCE_MUTATION ← obs/audit middleware (proto-annotation driven, separate path)
TUPLE_CHANGED     ← infra/openfga write path (calls Recorder.RecordTupleChange directly)

Each EventType's Result reflects only its own layer's outcome. Handler business errors are recorded via RESOURCE_MUTATION, never leak into AUTHN_RESULT or AUTHZ_DECISION — consumers can distinguish "authn failed" from "authn ok but business failed" by EventType alone.

Mounting rule (CRITICAL)

Collector MUST be the OUTER-most middleware relative to authn / authz. Kratos middleware chains wrap inner LIFO; if authn fails and short-circuits, only an outer Collector reaches its post-phase to read ctx and emit. Listing Collector inner to authn silently drops failure events (no panic, no error).

Recommended chain order:

recovery → tracing → logging → audit.Collector → authn → authz → handler
                               ^^^^^^^^^^^^^^^
                               outer to authn/authz; after tracing for trace_id capture

Example

recorder := audit.NewRecorder(emitter, "iam")
mw := []middleware.Middleware{
    recovery.Recovery(),
    tracing.Server(),
    logging.Server(l),
    audit.Collector(recorder),
    authn.Server(jwtAuth),
    authz.Server(fgaAuth, authz.WithRulesFunc(iampb.AuthzRules)),
}

See AGENTS.md in this package for the full mounting contract, push-ctx rationale, and emit pipeline.

Package audit provides Servora's audit event runtime: emitter interface, recorder, and Kratos middleware skeleton. Schema is sourced from api/protos/servora/audit/v1/audit.proto (auditpb).

本文件仅保留 codegen-friendly 的 Go 字符串枚举, 它们用于 audit middleware 的 Rule struct(基于 proto 注解生成)。 事件本体与各 detail 均使用 auditpb.* 而非 runtime struct(schema 单源 = proto)。

Index

Constants

View Source
const DefaultAuditTopic = "servora.audit.events"

Variables

This section is empty.

Functions

func Audit

Audit returns a Kratos middleware that records audit events based on configured rules. Operations with no matching rule are passed through silently.

This is a skeleton — full implementation follows in phase 2 when audit middleware is integrated with real service handlers.

func AuthnResultFrom added in v0.4.4

func AuthnResultFrom(ctx context.Context) (*auditpb.AuthnDetail, bool)

AuthnResultFrom returns the authn detail recorded by the matching WithAuthnResult call within this request, if any. Returns (nil, false) when no holder is installed or no detail has been written.

func AuthzResultFrom added in v0.4.4

func AuthzResultFrom(ctx context.Context) (*auditpb.AuthzDetail, bool)

AuthzResultFrom mirrors AuthnResultFrom.

func Collector added in v0.4.4

func Collector(rec *Recorder, opts ...CollectorOption) middleware.Middleware

Collector is a transport middleware that runs at the chain OUTERMOST position (it must wrap authn/authz, not be wrapped by them). At request entry it installs a per-request detail holder into ctx. Inner middleware writes (WithAuthnResult / WithAuthzResult) mutate that holder. After the inner handler returns, Collector reads the holder and emits one *auditpb.AuditEvent per detail present.

Mounting position (CRITICAL): Collector must be the OUTER-most middleware relative to authn/authz. In Kratos middleware chains, the slice order is execution order — list Collector FIRST so authn/authz are wrapped by it. If listed after authn/authz, the holder isn't installed when authn writes, the writes are silently dropped, and no audit events emit.

Why outer (not inner) install: Go's context.WithValue is immutable — child ctx writes don't propagate up. Inner-write/outer-read requires a shared mutable holder threaded through the original ctx. See context.go's detailHolder docstring for full rationale.

Result semantics: each emitted AuditEvent.Result reflects ONLY its own layer's outcome. AUTHN_RESULT.Result mirrors AuthnDetail.Success / FailureReason; AUTHZ_DECISION.Result mirrors Decision / ErrorReason. The handler's business error is propagated via the middleware return value but never leaks into AuditEvent.Result — RPC-layer errors are recorded by RESOURCE_MUTATION events (proto-annotation driven), not by authn/authz audit records. This lets consumers distinguish "authn failed" from "authn ok but business failed" by EventType alone.

func InstallHolder added in v0.4.4

func InstallHolder(ctx context.Context) context.Context

InstallHolder mounts a fresh detail holder into ctx. Called by audit.Collector at the outermost position of the middleware chain. Must be called before any inner middleware invokes WithAuthnResult / WithAuthzResult; otherwise those writes are silently dropped.

Idempotent: a second call on a ctx that already carries a holder is a no-op (returns the same ctx). This prevents a nested Collector from orphaning the outer holder's already-written details.

func WithAuthnResult added in v0.4.4

func WithAuthnResult(ctx context.Context, d *auditpb.AuthnDetail) context.Context

WithAuthnResult records an authentication outcome into the per-request holder previously installed by audit.Collector. Idempotent: latest call wins (a single request only authenticates once). Returns the same ctx (no child created); the holder mutation is what propagates upward.

nil detail makes this a no-op. If the holder hasn't been installed (i.e. Collector isn't mounted as outer), this silently drops the write — the audit event won't be emitted. See obs/audit/AGENTS.md for the mounting contract.

Also attaches an "audit.authn.recorded" event to the active OTel span (noop when no span is active). Span events fire only when the holder is present, so misconfigured chains don't pollute traces with phantom audit markers.

func WithAuthzResult added in v0.4.4

func WithAuthzResult(ctx context.Context, d *auditpb.AuthzDetail) context.Context

WithAuthzResult mirrors WithAuthnResult for the authz layer. See WithAuthnResult docstring for semantics & no-holder behavior.

Types

type AuditMiddlewareOption

type AuditMiddlewareOption func(*auditMiddlewareConfig)

AuditMiddlewareOption configures the audit middleware.

func WithRecorder

func WithRecorder(r *Recorder) AuditMiddlewareOption

WithRecorder sets the Recorder to use for emitting events.

func WithRules

func WithRules(rules map[string]Rule) AuditMiddlewareOption

WithRules sets the per-operation audit rules directly.

func WithRulesFunc

func WithRulesFunc(fn func() map[string]Rule) AuditMiddlewareOption

WithRulesFunc sets the per-operation audit rules via a function (e.g. generated AuditRules()). The function is called once during middleware construction.

type AuthzDecision

type AuthzDecision string

AuthzDecision describes the outcome of an authorization check.

该 Go 字符串别名仅在 obs/audit 包内部用作可读语义, 不进入对外 schema;对外契约一律走 auditpb.AuthzDecision (proto enum)。

const (
	AuthzDecisionAllowed AuthzDecision = "allowed"
	AuthzDecisionDenied  AuthzDecision = "denied"
	AuthzDecisionNoRule  AuthzDecision = "no_rule"
	AuthzDecisionError   AuthzDecision = "error"
)

type BrokerEmitter

type BrokerEmitter struct {
	// contains filtered or unexported fields
}

BrokerEmitter sends audit events to a message broker topic (e.g. Kafka). 事件本体已是 *auditpb.AuditEvent,直接 proto.Marshal 即可,无需 runtime↔proto mapper。

func NewBrokerEmitter

func NewBrokerEmitter(b broker.Broker, topic string, l logger.Logger) *BrokerEmitter

func (*BrokerEmitter) Close

func (e *BrokerEmitter) Close() error

func (*BrokerEmitter) Emit

func (e *BrokerEmitter) Emit(ctx context.Context, event *auditpb.AuditEvent) error

type CollectorOption added in v0.4.4

type CollectorOption func(*collectorConfig)

CollectorOption configures Collector behaviour. Named with the Collector prefix for parallelism with AuditMiddlewareOption (middleware.go).

func WithSpanEvents added in v0.4.4

func WithSpanEvents(enabled bool) CollectorOption

WithSpanEvents toggles the trailing "audit.collected" span event emitted by Collector after it processes ctx detail. Default is true. Disabling this does NOT affect the write-time span events from WithAuthnResult / WithAuthzResult — those fire upstream before Collector ever runs.

type Emitter

type Emitter interface {
	// Emit sends the event to the backend. Errors are non-fatal (audit must not break business flow).
	Emit(ctx context.Context, event *auditpb.AuditEvent) error
	// Close releases any resources held by the emitter.
	Close() error
}

Emitter is the interface for sending audit events to a backend. Implementations: BrokerEmitter (→ Kafka), LogEmitter (→ logger), NoopEmitter (→ /dev/null).

事件本体直接使用 auditpb.AuditEvent(proto 为 schema 单源), 避免 runtime↔proto 双 schema 与手写 mapper。

type EventType

type EventType string

EventType categorizes audit events. Used by audit middleware Rule.

const (
	EventTypeAuthnResult      EventType = "authn.result"
	EventTypeAuthzDecision    EventType = "authz.decision"
	EventTypeTupleChanged     EventType = "tuple.changed"
	EventTypeResourceMutation EventType = "resource.mutation"
)

type LogEmitter

type LogEmitter struct {
	// contains filtered or unexported fields
}

LogEmitter serialises audit events as JSON (proto json) and writes them to the Servora logger. Intended for development and debug environments.

func NewLogEmitter

func NewLogEmitter(l logger.Logger) *LogEmitter

func (*LogEmitter) Close

func (e *LogEmitter) Close() error

func (*LogEmitter) Emit

func (e *LogEmitter) Emit(_ context.Context, event *auditpb.AuditEvent) error

type NoopEmitter

type NoopEmitter struct{}

NoopEmitter silently discards all events. Used in tests and when audit is disabled.

func NewNoopEmitter

func NewNoopEmitter() *NoopEmitter

func (*NoopEmitter) Close

func (n *NoopEmitter) Close() error

func (*NoopEmitter) Emit

type Recorder

type Recorder struct {
	// contains filtered or unexported fields
}

Recorder is the primary entrypoint for producing audit events. It wires actor extraction, auto-fills metadata, and delegates to an Emitter.

自 v0.4.4 起,所有 detail 参数与事件本体均使用 auditpb.* (proto 为 schema 单源); 不再维护手写 runtime↔proto mapper。

func NewRecorder

func NewRecorder(emitter Emitter, serviceName string) *Recorder

NewRecorder creates a Recorder backed by the given Emitter.

func NewRecorderOptional

func NewRecorderOptional(cfg *conf.App, b broker.Broker, l logger.Logger) *Recorder

NewRecorderOptional creates a Recorder from App config. When audit is disabled or emitter_type is unrecognised, a NoopEmitter-backed Recorder is returned so callers never need a nil check.

Follows the optional-initialisation pattern of pkg/openfga.NewClientOptional.

func (*Recorder) Close

func (r *Recorder) Close() error

Close releases the underlying emitter resources.

func (*Recorder) Emit added in v0.4.4

func (r *Recorder) Emit(ctx context.Context, event *auditpb.AuditEvent) error

Emit publishes a fully-built AuditEvent. Returns the underlying emitter error; callers should typically log-and-continue (audit failures must not break business flow). nil event makes this a no-op and returns nil.

This is the low-level entrypoint used by callers (e.g. audit.Collector) that have already assembled an *auditpb.AuditEvent and want to observe emission failures. The high-level Record* helpers internally swallow this error.

func (*Recorder) RecordAuthnResult

func (r *Recorder) RecordAuthnResult(ctx context.Context, operation string, a actor.Actor, detail *auditpb.AuthnDetail)

RecordAuthnResult records an authentication attempt result. nil detail makes this a no-op.

func (*Recorder) RecordAuthzDecision

func (r *Recorder) RecordAuthzDecision(ctx context.Context, operation string, a actor.Actor, detail *auditpb.AuthzDetail)

RecordAuthzDecision records an OpenFGA authorization check result. nil detail makes this a no-op.

func (*Recorder) RecordResourceMutation

func (r *Recorder) RecordResourceMutation(ctx context.Context, operation string, a actor.Actor, target *auditpb.AuditTarget, detail *auditpb.ResourceMutationDetail, err error)

RecordResourceMutation records a CRUD operation on a business resource. nil detail makes this a no-op (an event with a missing oneof Detail would be malformed).

func (*Recorder) RecordTupleChange

func (r *Recorder) RecordTupleChange(ctx context.Context, operation string, a actor.Actor, detail *auditpb.TupleMutationDetail)

RecordTupleChange records an OpenFGA tuple write or delete. nil detail makes this a no-op.

type ResourceMutationType

type ResourceMutationType string

ResourceMutationType describes the type of resource mutation. Used by Rule struct in audit middleware (codegen-friendly).

const (
	ResourceMutationCreate ResourceMutationType = "create"
	ResourceMutationUpdate ResourceMutationType = "update"
	ResourceMutationDelete ResourceMutationType = "delete"
)

type Rule

type Rule struct {
	// EventType is the audit event type to emit.
	EventType EventType
	// Operation override (defaults to the gRPC operation path).
	Operation string
	// TargetType is the resource type being operated on (e.g. "user").
	TargetType string
	// MutationType specifies the mutation kind (create/update/delete) for
	// EventTypeResourceMutation rules. Defaults to ResourceMutationUpdate if unset.
	MutationType ResourceMutationType
	// RecordOnError controls whether to emit events even when the handler fails.
	RecordOnError bool
	// TargetIDFunc extracts the target resource ID from the request/response.
	// Generated by protoc-gen-servora-audit for rules with target_id_field set.
	// Called after handler returns; req is the original request, resp is the handler response.
	TargetIDFunc func(req, resp any) string
}

Rule describes how a specific RPC operation should be audited.

字段类型保留 obs/audit 自有的 Go 字符串枚举, 因为 Rule 主要由 protoc-gen-servora-audit 在编译期生成,使用字符串枚举比 proto enum 更便于代码生成器拼装、阅读与版本演进;middleware 内部转换到 auditpb.* 后再 emit。

type TupleMutationType

type TupleMutationType string

TupleMutationType describes the type of OpenFGA tuple change. Used by Rule struct in audit middleware (codegen-friendly).

const (
	TupleMutationWrite  TupleMutationType = "write"
	TupleMutationDelete TupleMutationType = "delete"
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL