activity

package
v0.27.6 Latest Latest
Warning

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

Go to latest
Published: Oct 29, 2026 License: MIT Imports: 17 Imported by: 5

README

Activity module

Audit logging helpers, a Bun-backed sink/repository, and query helpers for recent activity feeds and stats.

Components

  • BuildRecordFromActor and BuildRecordFromUUID convert request context into types.ActivityRecord with trimmed fields and cloned metadata.
  • Bun repository (activity.NewRepository) implements both types.ActivitySink and types.ActivityRepository for writes and reads.
  • Query handlers under query consume types.ActivityFilter/ActivityStatsFilter and respect tenant/org scope.

Constructing records

Use BuildRecordFromActor when you have a go-auth ActorContext (HTTP middleware); use BuildRecordFromUUID when you only have an actor UUID (background jobs, message handlers).

// From a request with go-auth metadata.
rec, err := activity.BuildRecordFromActor(actorCtx,
    "settings.updated",
    "settings",
    "global",
    map[string]any{"path": "ui.theme", "from": "light", "to": "dark"},
    activity.WithChannel("settings"),
)

// From a background worker with only an actor UUID.
rec, err := activity.BuildRecordFromUUID(actorID,
    "export.completed",
    "export.job",
    jobID,
    map[string]any{"format": "csv", "count": 120},
    activity.WithTenant(tenantID),
    activity.WithOrg(orgID),
    activity.WithOccurredAt(startedAt),
)

Record options:

  • WithChannel(string): module-level filter tag.
  • WithTenant(uuid.UUID): override tenant scope when not present in the actor context.
  • WithOrg(uuid.UUID): override org scope.
  • WithOccurredAt(time.Time): set a deterministic timestamp (defaults to time.Now().UTC()).

Wiring the sink/repository

store, err := activity.NewRepository(activity.RepositoryConfig{
    DB:    bunDB,
    Clock: types.SystemClock{},
    IDGen: types.UUIDGenerator{},
})
if err != nil {
    return err
}

svc := users.New(users.Config{
    ActivitySink:       store,
    ActivityRepository: store,
    // other dependencies...
})

if err := svc.ActivitySink().Log(ctx, rec); err != nil {
    return err
}

Log fills ID/OccurredAt when missing and persists to the user_activity table created by migration 000003_user_activity.sql.

Exact JSON and trusted machine classification

LogEntry.Data scans JSON numbers as json.Number in ordinary reads and INSERT RETURNING, including LogTx. Large integers, exact decimals and JSON numbers outside float64 range retain their value. Callers that previously asserted float64 must handle json.Number and explicitly choose any lossy conversion. This model-level setting does not alter Bun's global JSON provider or impose new size/depth limits on existing rows. Access policies still sanitize the decoded data.

The default PostgreSQL machine predicate treats missing JSON aliases as false; human rows with absent machine keys remain visible when machines are excluded. Existing machine actor/data aliases and SQLite legacy filtering are retained.

RepositoryConfig.MachineActivityFilter optionally supplies a trusted host-owned SQL classifier. It runs only when machine activity is disabled and before limits, counts, cursors and statistics. Add grouped predicates to the supplied query; preserve its scope/channel/user constraints. Never accept SQL or the classifier from request input. ExcludeMachineActivity(query, actorTypes, dataKeys) applies the default metadata predicate when a host needs a legacy fallback. An example consumer is go-admin: receipt-associated events use their canonical actor kind, while rows without a receipt continue using configured metadata aliases. A nil hook preserves the default behavior; no database schema change is needed.

Host-owned record policy and authentication events

RecordPolicy is an opt-in boundary for audit disclosure. Its zero configuration removes metadata and IP; it does not change DefaultMasker or access-policy behavior. Supply the same policy to ordinary writes, hooks, transactional writes and reads. The metadata callback owns the application's allowlist/redaction.

policy := activity.NewRecordPolicy(activity.RecordPolicyConfig{
    DataSanitizer: func(record types.ActivityRecord) map[string]any {
        // Return only the host's approved metadata. Do not forward arbitrary input.
        outcome, _ := record.Data["outcome"].(string)
        return map[string]any{"outcome": outcome}
    },
    RetainFailedLoginIdentifier: true, // explicit disclosure decision
})
sink := activity.SanitizingSink{Next: store, Policy: policy}
hooks = policy.SanitizeHooks(hooks) // only AfterActivity is decorated

// adapter/goauth imports as goauth; no identity lookup is performed.
authSink, err := goauth.NewActivitySink(goauth.ActivitySinkConfig{
    Sink: sink,
    Scope: types.ScopeFilter{TenantID: tenantID, OrgID: orgID},
    AnonymousActorID: authenticationGatewayID,
    RetainFailedLoginIdentifier: true,
})
if err != nil { return err }
authenticator.WithActivitySink(authSink)

The adapter requires nonzero tenant, organization and anonymous-actor UUIDs. It maps known event IDs and canonical actions, outcome, target and status fields, without copying arbitrary event metadata or provider errors. Identifier capture is disabled by default. Enabling it on both boundaries preserves only attempted_identifier for auth.login.failure in authentication: string input, controls/format characters removed, trimmed, case preserved and at most 254 Unicode code points. Render it as escaped, unverified attempted input.

For required transactional audit, call repo.LogTx(ctx, tx, record, policy) and propagate its error to the mutation transaction owner. Policy runs after enrichment; nil policy drops metadata/IP. The method never starts or commits a transaction. Direct CreateTx remains available for caller-managed mapping and disclosure policy. For reads, call policy.Sanitize(record) after access and scope enforcement. A separate read policy may use SanitizeRecord as its base callback while enabling the same identifier exception. Callbacks receive detached JSON metadata; returned data is detached again before forwarding. Nil policies fail closed; missing sink destinations return errors and downstream errors propagate. No retries or deduplication are added.

These helpers do not authorize readers. Hosts must retain trusted scope and permission checks, lifecycle/logger redaction, response cache policy and their own retention decision. Repository methods remain persistence-only and do not implicitly sanitize arbitrary direct writes.

Queries

feed, _ := svc.Queries().ActivityFeed.Query(ctx, types.ActivityFilter{
    Scope:      types.ScopeFilter{TenantID: tenantID},
    Channel:    "settings",
    Pagination: types.Pagination{Limit: 20},
})

stats, _ := svc.Queries().ActivityStats.Query(ctx, types.ActivityStatsFilter{
    Scope: types.ScopeFilter{TenantID: tenantID},
})

Filters include optional ActorID, UserID, ObjectType, ObjectID, Verb, Channel, Channels, ChannelDenylist, Since, and Until.

Role-aware filtering & sanitization

Use BuildFilterFromActor for role-aware filters or attach the default access policy to activity queries:

policy := activity.NewDefaultAccessPolicy(
    activity.WithPolicyFilterOptions(
        activity.WithChannelAllowlist("settings", "roles"),
        activity.WithMachineActivityEnabled(false),
        activity.WithSuperadminScope(true),
    ),
)

feedQuery := query.NewActivityFeedQuery(store, scopeGuard, query.WithActivityAccessPolicy(policy))
feed, _ := feedQuery.Query(ctx, types.ActivityFilter{
    Actor:      actorRef,
    Pagination: types.Pagination{Limit: 25},
})

Defaults treat system_admin/superadmin as superadmins and tenant_admin/admin/org_admin as admins; you can override with WithRoleAliases. Sanitization uses an independent frozen go-masker security profile: credentials and session IDs are fully redacted at every length, actor email is hashed, and IPs are removed for non-superadmins. Customize masking with WithPolicyMasker; changing the global masker.Default no longer changes Activity disclosure.

Optional cursor pagination

For high-volume feeds, the cursor helper can paginate by created_at and id:

cursor := &activity.ActivityCursor{
    OccurredAt: lastRecord.OccurredAt,
    ID:         lastRecord.ID,
}
query := activity.ApplyCursorPagination(db.NewSelect().Model(&rows), cursor, 50)

Conventions

  • Verbs/objects: settings.updated (settings), export.completed (export.job), bulk.users.updated (bulk.job), media.uploaded (media.asset).
  • Channels: lowercase module names (settings, export, bulk, media) for dashboard filtering.
  • Metadata: flat, JSON-serializable keys; include counts and scope hints when relevant.

See docs/ACTIVITY.md for deeper guidance, indexes, and schema details.

Legacy command sinks are best effort; failures reach the configured Logger. Required audit, version advancement and durable publication belong in the host transaction.

Documentation

Overview

Package activity provides default persistence helpers for the go-users ActivitySink. The Repository implements both the sink (writes) and the ActivityRepository read-side contract so transports can log lifecycle events and later query them for dashboards. The ActivitySink interface lives in pkg/types and is intentionally minimal (`Log(ctx, ActivityRecord) error`) so hosts can swap sinks without breaking changes.

Index

Constants

View Source
const (
	DataKeyActorDisplay    = "actor_display"
	DataKeyActorEmail      = "actor_email"
	DataKeyActorID         = "actor_id"
	DataKeyActorType       = "actor_type"
	DataKeyObjectDisplay   = "object_display"
	DataKeyObjectType      = "object_type"
	DataKeyObjectID        = "object_id"
	DataKeyObjectDeleted   = "object_deleted"
	DataKeyActionDisplay   = "action_display"
	DataKeySessionID       = "session_id"
	DataKeyEnrichedAt      = "enriched_at"
	DataKeyEnricherVersion = "enricher_version"
)

Enrichment metadata keys. These are flat JSONB keys and should remain stable.

View Source
const (
	// DataKeyAttemptedIdentifier contains unverified submitted login input, never actor identity.
	DataKeyAttemptedIdentifier  = "attempted_identifier"
	AuthenticationChannel       = "authentication"
	LoginFailureVerb            = "auth.login.failure"
	AttemptedIdentifierMaxRunes = 254
)
View Source
const DefaultEnricherVersion = "v1"

DefaultEnricherVersion is the built-in fallback version string when enrichment is enabled.

Variables

View Source
var (
	// ErrMissingActivityMapperRegistry indicates registry-backed mapper has no registry.
	ErrMissingActivityMapperRegistry = errors.New("go-users: missing activity mapper registry")
	// ErrActivityMapperNameRequired indicates mapper registration/query lacked a name.
	ErrActivityMapperNameRequired = errors.New("go-users: activity mapper name required")
	// ErrActivityMapperExists indicates the mapper name is already registered.
	ErrActivityMapperExists = errors.New("go-users: activity mapper already registered")
	// ErrActivityMapperNotFound indicates the named mapper is not registered.
	ErrActivityMapperNotFound = errors.New("go-users: activity mapper not found")
)
View Source
var (
	// ErrMissingActivityMapper indicates hook execution lacks a configured mapper.
	ErrMissingActivityMapper = errors.New("go-users: missing activity event mapper")
	// ErrUnsupportedActivityEvent indicates the mapper cannot interpret the event.
	ErrUnsupportedActivityEvent = errors.New("go-users: unsupported activity event")
)

Functions

func ApplyCursorPagination added in v0.10.0

func ApplyCursorPagination(q *bun.SelectQuery, cursor *ActivityCursor, limit int) *bun.SelectQuery

ApplyCursorPagination applies cursor pagination using created_at/id ordering. Results are ordered by created_at DESC, id DESC, and filtered to items older than the supplied cursor.

func AttachSessionID added in v0.14.0

func AttachSessionID(ctx context.Context, record types.ActivityRecord, provider SessionIDProvider, key string) types.ActivityRecord

AttachSessionID adds a session identifier to the record data if available.

func AttachSessionIDValue added in v0.14.0

func AttachSessionIDValue(record types.ActivityRecord, sessionID, key string) types.ActivityRecord

AttachSessionIDValue adds a session identifier to the record data if missing.

func BuildFilterFromActor added in v0.10.0

func BuildFilterFromActor(actor *auth.ActorContext, role string, req types.ActivityFilter, opts ...FilterOption) (types.ActivityFilter, error)

BuildFilterFromActor constructs a safe ActivityFilter using the auth actor context plus role-aware constraints and optional channel rules.

func BuildRecordFromActor

func BuildRecordFromActor(actor *auth.ActorContext, verb, objectType, objectID string, metadata map[string]any, opts ...RecordOption) (types.ActivityRecord, error)

BuildRecordFromActor constructs an ActivityRecord using the actor metadata supplied by go-auth middleware plus verb/object details and optional metadata. It normalizes actor, tenant, and org identifiers into UUIDs and defensively copies metadata to avoid caller mutation.

func BuildRecordFromUUID added in v0.3.0

func BuildRecordFromUUID(actorID uuid.UUID, verb, objectType, objectID string, metadata map[string]any, opts ...RecordOption) (types.ActivityRecord, error)

BuildRecordFromUUID constructs an ActivityRecord when only the actor UUID is available. It trims verb/object fields, validates required values, copies metadata defensively, and applies RecordOptions.

func DefaultAdminRoleAliases added in v0.10.0

func DefaultAdminRoleAliases() []string

DefaultAdminRoleAliases returns the default admin role aliases.

func DefaultMachineActorTypes added in v0.10.0

func DefaultMachineActorTypes() []string

DefaultMachineActorTypes returns the default machine actor type identifiers.

func DefaultMachineDataKeys added in v0.10.0

func DefaultMachineDataKeys() []string

DefaultMachineDataKeys returns the default machine data keys.

func DefaultMasker added in v0.10.0

func DefaultMasker() *masker.Masker

DefaultMasker returns an independent, frozen Activity masker. Credentials and session identifiers are fully redacted regardless of length; host changes to the package-global compatibility masker cannot weaken this boundary.

func DefaultSuperadminRoleAliases added in v0.10.0

func DefaultSuperadminRoleAliases() []string

DefaultSuperadminRoleAliases returns the default superadmin role aliases.

func ExcludeMachineActivity added in v0.27.6

func ExcludeMachineActivity(q *bun.SelectQuery, actorTypes, dataKeys []string) *bun.SelectQuery

ExcludeMachineActivity applies the default legacy metadata exclusion predicate. Hosts composing trusted identity filters may use it inside a grouped fallback branch. Values are normalized and parameterized; scope remains caller-owned.

func SanitizeAttemptedIdentifier added in v0.26.0

func SanitizeAttemptedIdentifier(value any) string

SanitizeAttemptedIdentifier bounds unverified input without normalizing case or changing the identifier used by authentication. Markup remains plain text; renderers must escape it for their output context.

func SanitizeRecord added in v0.10.0

func SanitizeRecord(mask *masker.Masker, record types.ActivityRecord) types.ActivityRecord

SanitizeRecord masks sensitive values in the activity record data payload.

func SanitizeRecords added in v0.10.0

func SanitizeRecords(mask *masker.Masker, records []types.ActivityRecord) []types.ActivityRecord

SanitizeRecords masks sensitive values for every record in the slice.

func StampEnrichment added in v0.14.0

func StampEnrichment(record types.ActivityRecord, now time.Time, version string) types.ActivityRecord

StampEnrichment sets enriched_at and enricher_version on the activity record.

func ToActivityRecord

func ToActivityRecord(entry *LogEntry) types.ActivityRecord

ToActivityRecord converts the Bun model into the domain activity record.

Types

type AccessPolicyOption added in v0.10.0

type AccessPolicyOption func(*DefaultAccessPolicy)

AccessPolicyOption customizes the default activity access policy.

func WithIPRedaction added in v0.10.0

func WithIPRedaction(enabled bool) AccessPolicyOption

WithIPRedaction toggles IP redaction for non-superadmin roles.

func WithMetadataExposure added in v0.15.0

func WithMetadataExposure(strategy MetadataExposureStrategy) AccessPolicyOption

WithMetadataExposure configures how activity metadata is exposed for support roles.

func WithMetadataSanitizer added in v0.15.0

func WithMetadataSanitizer(sanitizer MetadataSanitizer) AccessPolicyOption

WithMetadataSanitizer overrides the metadata sanitizer for sanitized exposure mode.

func WithPolicyFilterOptions added in v0.10.0

func WithPolicyFilterOptions(opts ...FilterOption) AccessPolicyOption

WithPolicyFilterOptions configures filter options applied during policy enforcement.

func WithPolicyMasker added in v0.10.0

func WithPolicyMasker(masker *masker.Masker) AccessPolicyOption

WithPolicyMasker overrides the masker used for sanitization.

func WithPolicyStatsSelfOnly added in v0.10.0

func WithPolicyStatsSelfOnly(enabled bool) AccessPolicyOption

WithPolicyStatsSelfOnly toggles self-only stats for non-admin roles.

type ActivityAccessPolicy added in v0.10.0

type ActivityAccessPolicy interface {
	Apply(actor *auth.ActorContext, role string, req types.ActivityFilter) (types.ActivityFilter, error)
	Sanitize(actor *auth.ActorContext, role string, records []types.ActivityRecord) []types.ActivityRecord
}

ActivityAccessPolicy applies role-aware constraints and sanitization to activity feeds.

type ActivityCursor added in v0.10.0

type ActivityCursor struct {
	OccurredAt time.Time
	ID         uuid.UUID
}

ActivityCursor defines the cursor shape for activity feeds.

type ActivityEnricher added in v0.14.0

type ActivityEnricher interface {
	Enrich(ctx context.Context, record types.ActivityRecord) (types.ActivityRecord, error)
}

ActivityEnricher mutates or returns an enriched ActivityRecord.

type ActivityEnrichmentFilter added in v0.14.0

type ActivityEnrichmentFilter struct {
	Scope          types.ScopeFilter
	MissingKeys    []string
	EnrichedBefore *time.Time
	Cursor         *ActivityCursor
	Limit          int
}

ActivityEnrichmentFilter narrows enrichment backfill selection.

type ActivityEnrichmentPage added in v0.14.0

type ActivityEnrichmentPage struct {
	Records    []types.ActivityRecord
	NextCursor *ActivityCursor
}

ActivityEnrichmentPage returns selected activity records and the next cursor.

type ActivityEnrichmentQuery added in v0.14.0

type ActivityEnrichmentQuery interface {
	ListActivityForEnrichment(ctx context.Context, filter ActivityEnrichmentFilter) (ActivityEnrichmentPage, error)
}

ActivityEnrichmentQuery exposes helper queries for missing/stale enrichment.

type ActivityEnrichmentStore added in v0.14.0

type ActivityEnrichmentStore interface {
	UpdateActivityData(ctx context.Context, id uuid.UUID, data map[string]any) error
}

ActivityEnrichmentStore updates activity metadata for backfill jobs.

type ActivityEnrichmentStoreWithOptions added in v0.14.0

type ActivityEnrichmentStoreWithOptions interface {
	UpdateActivityDataWithOptions(ctx context.Context, id uuid.UUID, data map[string]any, opts ActivityEnrichmentUpdateOptions) error
}

ActivityEnrichmentStoreWithOptions supports optional forced updates.

type ActivityEnrichmentUpdateOptions added in v0.14.0

type ActivityEnrichmentUpdateOptions struct {
	// ForceKeys allows overwriting specific keys (e.g., enriched_at) when needed.
	ForceKeys []string
}

ActivityEnrichmentUpdateOptions controls missing-key update behavior.

type ActivityEnvelope added in v0.17.0

type ActivityEnvelope struct {
	Channel    string
	Verb       string
	ObjectType string
	ObjectID   string
	ActorID    string
	TenantID   string
	OccurredAt time.Time
	Metadata   map[string]any
}

ActivityEnvelope is a generic envelope shape for low-coupling integrations.

func (ActivityEnvelope) ActivityActorID added in v0.17.0

func (e ActivityEnvelope) ActivityActorID() string

func (ActivityEnvelope) ActivityChannel added in v0.17.0

func (e ActivityEnvelope) ActivityChannel() string

func (ActivityEnvelope) ActivityMetadata added in v0.17.0

func (e ActivityEnvelope) ActivityMetadata() map[string]any

func (ActivityEnvelope) ActivityObjectID added in v0.17.0

func (e ActivityEnvelope) ActivityObjectID() string

func (ActivityEnvelope) ActivityObjectType added in v0.17.0

func (e ActivityEnvelope) ActivityObjectType() string

func (ActivityEnvelope) ActivityOccurredAt added in v0.17.0

func (e ActivityEnvelope) ActivityOccurredAt() time.Time

func (ActivityEnvelope) ActivityTenantID added in v0.17.0

func (e ActivityEnvelope) ActivityTenantID() string

func (ActivityEnvelope) ActivityVerb added in v0.17.0

func (e ActivityEnvelope) ActivityVerb() string

type ActivityEnvelopeCarrier added in v0.17.0

type ActivityEnvelopeCarrier interface {
	ActivityChannel() string
	ActivityVerb() string
	ActivityObjectType() string
	ActivityObjectID() string
	ActivityActorID() string
	ActivityTenantID() string
	ActivityOccurredAt() time.Time
	ActivityMetadata() map[string]any
}

ActivityEnvelopeCarrier exposes envelope fields used by EnvelopeMapper.

type ActivityEventMapper added in v0.17.0

type ActivityEventMapper interface {
	Map(ctx context.Context, evt any) (types.ActivityRecord, error)
}

ActivityEventMapper maps arbitrary events into ActivityRecord payloads.

type ActivityEventMapperFunc added in v0.17.0

type ActivityEventMapperFunc func(ctx context.Context, evt any) (types.ActivityRecord, error)

ActivityEventMapperFunc adapts a function into ActivityEventMapper.

func (ActivityEventMapperFunc) Map added in v0.17.0

Map executes the mapper function.

type ActivityMapperRegistry added in v0.17.0

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

ActivityMapperRegistry stores named event mappers for runtime composition.

func NewActivityMapperRegistry added in v0.17.0

func NewActivityMapperRegistry() *ActivityMapperRegistry

NewActivityMapperRegistry constructs an empty mapper registry.

func (*ActivityMapperRegistry) Lookup added in v0.17.0

Lookup returns the named mapper when present.

func (*ActivityMapperRegistry) Names added in v0.17.0

func (r *ActivityMapperRegistry) Names() []string

Names returns sorted mapper names.

func (*ActivityMapperRegistry) Register added in v0.17.0

func (r *ActivityMapperRegistry) Register(name string, mapper ActivityEventMapper) error

Register adds a named mapper. Names are normalized to lower-case.

type ActivityStatsPolicy added in v0.10.0

type ActivityStatsPolicy interface {
	ApplyStats(actor *auth.ActorContext, role string, req types.ActivityStatsFilter) (types.ActivityStatsFilter, error)
}

ActivityStatsPolicy applies role-aware constraints to activity stats.

type ActorInfo added in v0.14.0

type ActorInfo struct {
	ID      uuid.UUID
	Type    string
	Display string
	Email   string
}

ActorInfo defines enrichment details for an actor.

type ActorResolver added in v0.14.0

type ActorResolver interface {
	ResolveActors(ctx context.Context, ids []uuid.UUID, meta ResolveContext) (map[uuid.UUID]ActorInfo, error)
}

ActorResolver resolves actor enrichment details in batch.

type DefaultAccessPolicy added in v0.10.0

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

DefaultAccessPolicy applies BuildFilterFromActor and sanitizes records on read.

func NewDefaultAccessPolicy added in v0.10.0

func NewDefaultAccessPolicy(opts ...AccessPolicyOption) *DefaultAccessPolicy

NewDefaultAccessPolicy returns the default policy implementation.

func (*DefaultAccessPolicy) Apply added in v0.10.0

Apply enforces role-aware scope/visibility rules on the requested filter.

func (*DefaultAccessPolicy) ApplyStats added in v0.10.0

ApplyStats enforces role-aware scope/visibility rules on stats filters.

func (*DefaultAccessPolicy) Sanitize added in v0.10.0

func (p *DefaultAccessPolicy) Sanitize(actor *auth.ActorContext, role string, records []types.ActivityRecord) []types.ActivityRecord

Sanitize applies masking rules and IP redaction to activity records.

type EnrichedSink added in v0.14.0

type EnrichedSink struct {
	Sink         types.ActivitySink
	Enricher     ActivityEnricher
	ErrorHandler EnrichmentErrorHandler
}

EnrichedSink enriches activity records before logging them to a sink.

func (*EnrichedSink) Log added in v0.14.0

func (s *EnrichedSink) Log(ctx context.Context, record types.ActivityRecord) error

Log enriches the record (if configured) and forwards it to the sink.

type EnricherChain added in v0.14.0

type EnricherChain []ActivityEnricher

EnricherChain composes multiple enrichers in order.

func (EnricherChain) Enrich added in v0.14.0

Enrich applies the chain sequentially and stops on the first error.

func (EnricherChain) EnrichWithHandler added in v0.14.0

func (c EnricherChain) EnrichWithHandler(ctx context.Context, record types.ActivityRecord, handler EnrichmentErrorHandler) (types.ActivityRecord, error)

EnrichWithHandler applies the chain and delegates error handling when provided.

type EnricherFunc added in v0.14.0

type EnricherFunc func(ctx context.Context, record types.ActivityRecord) (types.ActivityRecord, error)

EnricherFunc adapts a function into an ActivityEnricher.

func (EnricherFunc) Enrich added in v0.14.0

Enrich executes the function and satisfies ActivityEnricher.

type EnricherWithHandler added in v0.14.0

type EnricherWithHandler interface {
	EnrichWithHandler(ctx context.Context, record types.ActivityRecord, handler EnrichmentErrorHandler) (types.ActivityRecord, error)
}

EnricherWithHandler allows enrichment with an explicit error handler.

type EnrichmentErrorHandler added in v0.14.0

type EnrichmentErrorHandler func(ctx context.Context, err error, enricher ActivityEnricher, current types.ActivityRecord, original types.ActivityRecord) (types.ActivityRecord, error)

EnrichmentErrorHandler decides how to handle errors during enrichment. Return a non-nil error to stop the chain. Return nil to continue using the returned record. Best-effort handlers should return the last successful record to allow partial enrichment.

func DefaultEnrichmentErrorHandler added in v0.14.0

func DefaultEnrichmentErrorHandler(strategy EnrichmentErrorStrategy) EnrichmentErrorHandler

DefaultEnrichmentErrorHandler returns a handler for the chosen strategy.

type EnrichmentErrorStrategy added in v0.14.0

type EnrichmentErrorStrategy int

EnrichmentErrorStrategy chooses how enrichment errors are handled.

const (
	// EnrichmentFailFast stops on the first error and returns the original record.
	EnrichmentFailFast EnrichmentErrorStrategy = iota
	// EnrichmentBestEffort keeps the last successful record and continues the chain.
	EnrichmentBestEffort
)

type EnrichmentScope added in v0.14.0

type EnrichmentScope string

EnrichmentScope determines when enrichment should apply.

const (
	EnrichmentScopeWrite    EnrichmentScope = "write"
	EnrichmentScopeBackfill EnrichmentScope = "backfill"
	EnrichmentScopeBoth     EnrichmentScope = "both"
)

type EnrichmentWriteMode added in v0.14.0

type EnrichmentWriteMode string

EnrichmentWriteMode determines how write-time enrichment is applied.

const (
	EnrichmentWriteModeNone    EnrichmentWriteMode = "none"
	EnrichmentWriteModeWrapper EnrichmentWriteMode = "wrapper"
	EnrichmentWriteModeRepo    EnrichmentWriteMode = "repo"
	EnrichmentWriteModeHybrid  EnrichmentWriteMode = "hybrid"
)

type EnvelopeMapper added in v0.17.0

type EnvelopeMapper struct {
	DefaultChannel    string
	DefaultObjectType string
}

EnvelopeMapper maps ActivityEnvelopeCarrier events to ActivityRecord.

func (EnvelopeMapper) Map added in v0.17.0

Map converts a generic envelope event into ActivityRecord.

type FilterConfig added in v0.10.0

type FilterConfig struct {
	ChannelAllowlist []string
	ChannelDenylist  []string

	// Machine activity settings are reserved for policy layers that can inspect
	// records (actor type/data); BuildFilterFromActor does not use them directly.
	MachineActivityEnabled bool
	MachineActorTypes      []string
	MachineDataKeys        []string

	SuperadminScope bool

	AdminRoleAliases      []string
	SuperadminRoleAliases []string
}

FilterConfig controls how BuildFilterFromActor applies role and channel rules.

type FilterOption added in v0.10.0

type FilterOption func(*FilterConfig)

FilterOption mutates the filter configuration.

func WithChannelAllowlist added in v0.10.0

func WithChannelAllowlist(channels ...string) FilterOption

WithChannelAllowlist restricts results to the provided channels.

func WithChannelDenylist added in v0.10.0

func WithChannelDenylist(channels ...string) FilterOption

WithChannelDenylist excludes the provided channels.

func WithMachineActivityEnabled added in v0.10.0

func WithMachineActivityEnabled(enabled bool) FilterOption

WithMachineActivityEnabled toggles machine activity visibility.

func WithMachineActorTypes added in v0.10.0

func WithMachineActorTypes(types ...string) FilterOption

WithMachineActorTypes overrides the machine actor type identifiers.

func WithMachineDataKeys added in v0.10.0

func WithMachineDataKeys(keys ...string) FilterOption

WithMachineDataKeys overrides the data keys used to flag machine activity.

func WithRoleAliases added in v0.10.0

func WithRoleAliases(adminAliases, superadminAliases []string) FilterOption

WithRoleAliases overrides the admin/superadmin role alias lists.

func WithSuperadminScope added in v0.10.0

func WithSuperadminScope(enabled bool) FilterOption

WithSuperadminScope allows superadmins to widen scope beyond actor context.

type LogEntry

type LogEntry struct {
	bun.BaseModel `bun:"table:user_activity"`

	ID         uuid.UUID            `bun:",pk,type:uuid"`
	UserID     uuid.UUID            `bun:"user_id,type:uuid"`
	ActorID    uuid.UUID            `bun:"actor_id,type:uuid"`
	TenantID   uuid.UUID            `bun:"tenant_id,type:uuid"`
	OrgID      uuid.UUID            `bun:"org_id,type:uuid"`
	ScopeKey   string               `bun:"scope_key,notnull,default:''"`
	Labels     map[string]uuid.UUID `bun:"labels,type:jsonb,notnull,default:'{}'"`
	Verb       string               `bun:"verb"`
	ObjectType string               `bun:"object_type"`
	ObjectID   string               `bun:"object_id"`
	Channel    string               `bun:"channel"`
	IP         string               `bun:"ip"`
	Data       map[string]any       `bun:"data,type:jsonb,json_use_number"`
	CreatedAt  time.Time            `bun:"created_at"`
}

LogEntry models the persisted row in user_activity.

func FromActivityRecord

func FromActivityRecord(record types.ActivityRecord) *LogEntry

FromActivityRecord converts a domain activity record into the Bun model so it can be reused by transports without duplicating conversion logic.

type MachineActivityFilter added in v0.27.6

type MachineActivityFilter func(*bun.SelectQuery, []string, []string) *bun.SelectQuery

MachineActivityFilter adds SQL exclusion predicates using trusted host-owned classification. It is called only when machine activity is disabled, before pagination/counting, for feeds, cursors and statistics. Preserve the existing query predicates; use ExcludeMachineActivity for legacy rows. Never derive this callback from request input. A nil callback uses the default metadata filter.

type MetadataExposureStrategy added in v0.15.0

type MetadataExposureStrategy int

MetadataExposureStrategy controls how activity metadata is exposed on read.

const (
	// MetadataExposeNone returns no metadata.
	MetadataExposeNone MetadataExposureStrategy = iota
	// MetadataExposeSanitized returns metadata after sanitization.
	MetadataExposeSanitized
	// MetadataExposeAll returns raw metadata (intended for development/debug).
	MetadataExposeAll
)

type MetadataSanitizer added in v0.15.0

type MetadataSanitizer func(actor *auth.ActorContext, role string, record types.ActivityRecord) map[string]any

MetadataSanitizer customizes sanitized metadata exposure.

type ObjectInfo added in v0.14.0

type ObjectInfo struct {
	ID      string
	Type    string
	Display string
	Deleted bool
}

ObjectInfo defines enrichment details for an object.

type ObjectResolver added in v0.14.0

type ObjectResolver interface {
	ResolveObjects(ctx context.Context, objectType string, ids []string, meta ResolveContext) (map[string]ObjectInfo, error)
}

ObjectResolver resolves object enrichment details in batch.

type RecordOption

type RecordOption func(*types.ActivityRecord)

RecordOption mutates the ActivityRecord produced by BuildRecordFromActor.

func WithChannel

func WithChannel(channel string) RecordOption

WithChannel sets the channel/module field used for downstream filtering.

func WithOccurredAt added in v0.3.0

func WithOccurredAt(occurredAt time.Time) RecordOption

WithOccurredAt overrides the default occurrence timestamp.

func WithOrg added in v0.3.0

func WithOrg(orgID uuid.UUID) RecordOption

WithOrg sets the organization identifier for the record.

func WithTenant added in v0.3.0

func WithTenant(tenantID uuid.UUID) RecordOption

WithTenant sets the tenant identifier for the record.

type RecordPolicy added in v0.26.0

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

RecordPolicy applies the same disclosure rules to sinks, hooks, reads and caller-owned transactions. It never changes canonical identity or scope and does not authorize access. Configuration is immutable; callbacks must be safe for concurrent use when the policy is shared.

func NewRecordPolicy added in v0.26.0

func NewRecordPolicy(config RecordPolicyConfig) *RecordPolicy

func (*RecordPolicy) Sanitize added in v0.26.0

Sanitize returns a detached record. A nil policy is fail-closed like the zero configuration. The attempted-identifier exception is enforced even if a host callback returns that field for an unrelated event.

func (*RecordPolicy) SanitizeHooks added in v0.26.0

func (p *RecordPolicy) SanitizeHooks(hooks types.Hooks) types.Hooks

SanitizeHooks decorates only AfterActivity. Other hooks retain host policy.

type RecordPolicyConfig added in v0.26.0

type RecordPolicyConfig struct {
	// DataSanitizer owns general metadata redaction. It receives a detached copy
	// of JSON-decoded maps/slices and must return only fields safe to disclose.
	DataSanitizer func(types.ActivityRecord) map[string]any
	// RetainFailedLoginIdentifier permits the bounded, event-specific exception.
	RetainFailedLoginIdentifier bool
	PreserveIP                  bool
}

RecordPolicyConfig defines host-owned disclosure policy. Zero configuration drops all metadata and IP. It does not change the package's global masker.

type RegistryMapper added in v0.17.0

type RegistryMapper struct {
	Registry *ActivityMapperRegistry
	Name     string
}

RegistryMapper resolves and delegates mapping to a named registry mapper.

func (*RegistryMapper) Map added in v0.17.0

Map resolves the named mapper and delegates event mapping.

type Repository

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

Repository persists activity logs and exposes query helpers.

func NewRepository

func NewRepository(cfg RepositoryConfig) (*Repository, error)

NewRepository constructs a repository that implements both ActivitySink and ActivityRepository interfaces.

func (*Repository) ActivityStats

func (r *Repository) ActivityStats(ctx context.Context, filter types.ActivityStatsFilter) (types.ActivityStats, error)

ActivityStats aggregates counts grouped by verb.

func (*Repository) ListActivity

func (r *Repository) ListActivity(ctx context.Context, filter types.ActivityFilter) (types.ActivityPage, error)

ListActivity returns a paginated feed filtered by the supplied criteria.

func (*Repository) ListActivityCursor added in v0.27.0

func (r *Repository) ListActivityCursor(ctx context.Context, filter types.ActivityFilter, cursor *ActivityCursor) ([]types.ActivityRecord, error)

ListActivityCursor applies exact scope and cursor predicates before SQL limit. It is a persistence API; callers must authorize the filter scope first.

func (*Repository) ListActivityForEnrichment added in v0.14.0

func (r *Repository) ListActivityForEnrichment(ctx context.Context, filter ActivityEnrichmentFilter) (ActivityEnrichmentPage, error)

ListActivityForEnrichment returns activity records missing enrichment keys or stale by cutoff.

func (*Repository) Log

func (r *Repository) Log(ctx context.Context, record types.ActivityRecord) error

Log persists an activity record into the database.

func (*Repository) LogTx added in v0.27.0

func (r *Repository) LogTx(ctx context.Context, tx bun.Tx, record types.ActivityRecord, policy *RecordPolicy) error

LogTx persists required audit in the caller's transaction and returns failures to its owner. Disclosure policy is applied after enrichment; nil policy drops metadata and IP. Callers must propagate this error to roll back; legacy command Activity sinks remain best effort and cannot make a mutation transactional.

func (*Repository) UpdateActivityData added in v0.14.0

func (r *Repository) UpdateActivityData(ctx context.Context, id uuid.UUID, data map[string]any) error

UpdateActivityData merges missing keys into the activity data payload.

func (*Repository) UpdateActivityDataWithOptions added in v0.14.0

func (r *Repository) UpdateActivityDataWithOptions(ctx context.Context, id uuid.UUID, data map[string]any, opts ActivityEnrichmentUpdateOptions) error

UpdateActivityDataWithOptions merges missing keys and optionally forces updates for specific keys.

type RepositoryConfig

type RepositoryConfig struct {
	MachineActivityFilter MachineActivityFilter
	DB                    *bun.DB
	Repository            repository.Repository[*LogEntry]
	Clock                 types.Clock
	IDGen                 types.IDGenerator
	Enricher              ActivityEnricher
	// EnrichmentErrorHandler controls fail-fast vs best-effort behavior.
	EnrichmentErrorHandler EnrichmentErrorHandler
}

RepositoryConfig wires the Bun-backed activity repository.

type ResolveContext added in v0.14.0

type ResolveContext struct {
	TenantID       uuid.UUID
	OrganizationID uuid.UUID
	ActorID        uuid.UUID
	Verb           string
	Source         string
	Metadata       map[string]any
}

ResolveContext provides request-scoped data used by resolvers.

type RoleEventMapper added in v0.27.0

type RoleEventMapper struct{}

RoleEventMapper preserves catalog scope for definition events and exact assignment scope for grant events. Attach through the existing activity hook.

func (RoleEventMapper) Map added in v0.27.0

type SanitizerConfig added in v0.10.0

type SanitizerConfig struct {
	Masker *masker.Masker
}

SanitizerConfig controls the masker used for activity sanitization.

type SanitizingSink added in v0.26.0

type SanitizingSink struct {
	Next   types.ActivitySink
	Policy *RecordPolicy
}

SanitizingSink applies an explicit policy before forwarding an audit record. Missing destinations return an error instead of silently dropping audit data.

func (SanitizingSink) Log added in v0.26.0

type SessionIDProvider added in v0.14.0

type SessionIDProvider interface {
	SessionID(ctx context.Context) (string, bool)
}

SessionIDProvider extracts a session identifier from the request context.

type SessionIDProviderFunc added in v0.14.0

type SessionIDProviderFunc func(ctx context.Context) (string, bool)

SessionIDProviderFunc adapts a function into a SessionIDProvider.

func (SessionIDProviderFunc) SessionID added in v0.14.0

func (f SessionIDProviderFunc) SessionID(ctx context.Context) (string, bool)

SessionID returns the session identifier and satisfies SessionIDProvider.

type UsersActivityHook added in v0.17.0

type UsersActivityHook struct {
	Sink            types.ActivitySink
	Mapper          ActivityEventMapper
	SessionProvider SessionIDProvider
	Enricher        ActivityEnricher
	ErrorHandler    EnrichmentErrorHandler
}

UsersActivityHook maps events into go-users activity records using a mapper.

func (*UsersActivityHook) Notify added in v0.17.0

func (h *UsersActivityHook) Notify(ctx context.Context, evt any) error

Notify maps and persists one event through the configured sink.

Jump to

Keyboard shortcuts

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