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:
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
Middleware — assembled by business apps: Collector(rec *Recorder, opts ...CollectorOption) middleware.Middleware WithSpanEvents(enabled bool) CollectorOption // default true
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 → ratelimit → validate → metrics → audit.Collector → authn → authz → handler
^^^^^^^^^^^^^^^
outer to authn/authz; trailing position
aligns with transport/server/middleware
ChainBuilder.Build output
本顺序与 transport/server/middleware.ChainBuilder 的 Build 输出对齐;调用 WithAudit(rec) 即自动落到该位置,无需业务方手记 outer/inner。
Example ¶
推荐:通过 ChainBuilder.WithAudit 一行装配(无需手记 outer/inner 顺序):
recorder := audit.NewRecorder(emitter, "iam")
mw := middleware.NewChainBuilder(l).
WithTrace(trace).
WithMetrics(mtc).
WithAudit(recorder).
Build()
mw = append(mw,
authn.Server(jwtAuth),
authz.Server(fgaAuth, authz.WithRulesFunc(iampb.AuthzRules)),
)
如果不用 ChainBuilder 也可以手写——注意 audit.Collector 必须 OUTER 于 authn/authz:
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
- func Audit(opts ...AuditMiddlewareOption) middleware.Middleware
- func AuthnResultFrom(ctx context.Context) (*auditpb.AuthnDetail, bool)
- func AuthzResultFrom(ctx context.Context) (*auditpb.AuthzDetail, bool)
- func Collector(rec *Recorder, opts ...CollectorOption) middleware.Middleware
- func InstallHolder(ctx context.Context) context.Context
- func WithAuthnResult(ctx context.Context, d *auditpb.AuthnDetail) context.Context
- func WithAuthzResult(ctx context.Context, d *auditpb.AuthzDetail) context.Context
- type AuditMiddlewareOption
- type AuthzDecision
- type BrokerEmitter
- type CollectorOption
- type Emitter
- type EventType
- type LogEmitter
- type NoopEmitter
- type Recorder
- func (r *Recorder) Close() error
- func (r *Recorder) Emit(ctx context.Context, event *auditpb.AuditEvent) error
- func (r *Recorder) RecordAuthnResult(ctx context.Context, operation string, a actor.Actor, ...)
- func (r *Recorder) RecordAuthzDecision(ctx context.Context, operation string, a actor.Actor, ...)
- func (r *Recorder) RecordResourceMutation(ctx context.Context, operation string, a actor.Actor, ...)
- func (r *Recorder) RecordTupleChange(ctx context.Context, operation string, a actor.Actor, ...)
- type ResourceMutationType
- type Rule
- type TupleMutationType
Constants ¶
const DefaultAuditTopic = "servora.audit.events"
Variables ¶
This section is empty.
Functions ¶
func Audit ¶
func Audit(opts ...AuditMiddlewareOption) middleware.Middleware
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
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
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
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.
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 ¶
func (n *NoopEmitter) Emit(_ context.Context, _ *auditpb.AuditEvent) error
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 ¶
NewRecorder creates a Recorder backed by the given Emitter.
func NewRecorderOptional ¶
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) Emit ¶ added in v0.4.4
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" )