From 8576fbe58e8597ab0325d428440be83f5191bcc4 Mon Sep 17 00:00:00 2001 From: Dmitriy Matrenichev Date: Thu, 8 Aug 2024 18:26:50 +0300 Subject: [PATCH] chore: implement audit log for several types This commit implements session tracking and log audit for those types: - [x] auth.PublicKey - [x] auth.AccessPolicy - [x] auth.User - [x] auth.Identity - [x] omni.Machine - [x] omni.MachineLabels - [x] omni.Cluster - [x] omni.MachineSet (only empty owners for update, log create and delete in all cases) - [x] omni.MachineSetNode (only empty owners for update, log create and delete in all cases) - [x] omni.ConfigPatch - [x] Talos API Access - [x] Kubernetes API access Output example: ``` {"event_type":"update","resource_type":"Machines.omni.sidero.dev","event_ts":1723137771180,"event_data":{"session":{"user_agent":"Omni-Internal-Agent"},"machine":{"id":"18cec051-d975-483d-8d43-10ac6421648a","is_connected":true,"management_address":"fdae:41e4:649b:9303:da9b:1ed:a725:c3dd","labels":{"omni.sidero.dev/address":"fdae:41e4:649b:9303:da9b:1ed:a725:c3dd"}}}} {"event_type":"update","resource_type":"Machines.omni.sidero.dev","event_ts":1723137771180,"event_data":{"session":{"user_agent":"Omni-Internal-Agent"},"machine":{"id":"18cec051-d975-483d-8d43-10ac6421648a","is_connected":true,"management_address":"fdae:41e4:649b:9303:da9b:1ed:a725:c3dd","labels":{"omni.sidero.dev/address":"fdae:41e4:649b:9303:da9b:1ed:a725:c3dd"}}}} {"event_type":"update","resource_type":"Machines.omni.sidero.dev","event_ts":1723137771181,"event_data":{"session":{"user_agent":"Omni-Internal-Agent"},"machine":{"id":"18cec051-d975-483d-8d43-10ac6421648a","is_connected":true,"management_address":"fdae:41e4:649b:9303:da9b:1ed:a725:c3dd","labels":{"omni.sidero.dev/address":"fdae:41e4:649b:9303:da9b:1ed:a725:c3dd"}}}} {"event_type":"create","resource_type":"MachineLabels.omni.sidero.dev","event_ts":1723137787549,"event_data":{"session":{"user_agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36","ip_address":"","user_id":"ea002172-b9da-423f-bd1d-b443b8a7b43c","role":"Admin","email":"dmitry.matrenichev@siderolabs.com","fingerprint":"da7b997eb68449a12bebc6a3bf4f59beaf167209"},"machine_labels":{"id":"18cec051-d975-483d-8d43-10ac6421648a","labels":{"222":""}}}} {"event_type":"update","resource_type":"MachineLabels.omni.sidero.dev","event_ts":1723137787553,"event_data":{"session":{"user_agent":"Omni-Internal-Agent"},"machine_labels":{"id":"18cec051-d975-483d-8d43-10ac6421648a","labels":{"222":""}}}} {"event_type":"update","resource_type":"MachineLabels.omni.sidero.dev","event_ts":1723137811532,"event_data":{"session":{"user_agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36","ip_address":"","user_id":"ea002172-b9da-423f-bd1d-b443b8a7b43c","role":"Admin","email":"dmitry.matrenichev@siderolabs.com","fingerprint":"da7b997eb68449a12bebc6a3bf4f59beaf167209"},"machine_labels":{"id":"18cec051-d975-483d-8d43-10ac6421648a","labels":{"222":"","333":""}}}} {"event_type":"update","resource_type":"MachineLabels.omni.sidero.dev","event_ts":1723137811610,"event_data":{"session":{"user_agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36","ip_address":"","user_id":"ea002172-b9da-423f-bd1d-b443b8a7b43c","role":"Admin","email":"dmitry.matrenichev@siderolabs.com","fingerprint":"da7b997eb68449a12bebc6a3bf4f59beaf167209"},"machine_labels":{"id":"18cec051-d975-483d-8d43-10ac6421648a","labels":{"222":"","333":""}}}} {"event_type":"update","resource_type":"MachineLabels.omni.sidero.dev","event_ts":1723137811611,"event_data":{"session":{"user_agent":"Omni-Internal-Agent"},"machine_labels":{"id":"18cec051-d975-483d-8d43-10ac6421648a","labels":{"222":"","333":""}}}} {"event_type":"destroy","resource_type":"MachineLabels.omni.sidero.dev","event_ts":1723137811621,"event_data":{"session":{"user_agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36","ip_address":"","user_id":"ea002172-b9da-423f-bd1d-b443b8a7b43c","role":"Admin","email":"dmitry.matrenichev@siderolabs.com","fingerprint":"da7b997eb68449a12bebc6a3bf4f59beaf167209"},"machine_labels":{"id":"18cec051-d975-483d-8d43-10ac6421648a","labels":{"222":"","333":""}}}} {"event_type":"create","resource_type":"Users.omni.sidero.dev","event_ts":1723141793888,"event_data":{"new_user":{"role":"Admin","id":"7903a72c-87af-43b8-94dc-82bd961ab768"},"session":{"user_agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36","ip_address":"","user_id":"ea002172-b9da-423f-bd1d-b443b8a7b43c","role":"Admin","email":"dmitry.matrenichev@siderolabs.com","fingerprint":"da7b997eb68449a12bebc6a3bf4f59beaf167209"}}} {"event_type":"create","resource_type":"Identities.omni.sidero.dev","event_ts":1723141793981,"event_data":{"new_user":{"id":"7903a72c-87af-43b8-94dc-82bd961ab768","email":"some-user-email@email.com"},"session":{"user_agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36","ip_address":"","user_id":"ea002172-b9da-423f-bd1d-b443b8a7b43c","role":"Admin","email":"dmitry.matrenichev@siderolabs.com","fingerprint":"da7b997eb68449a12bebc6a3bf4f59beaf167209"}}} ``` Closes #37 Signed-off-by: Dmitriy Matrenichev --- client/go.mod | 2 +- cmd/omni/main.go | 8 + internal/backend/grpc/auth.go | 23 - internal/backend/grpc/router/router.go | 23 +- internal/backend/k8sproxy/k8sproxy.go | 9 +- internal/backend/k8sproxy/middleware.go | 20 + internal/backend/runtime/omni/audit/audit.go | 431 ++++++++++++++- .../backend/runtime/omni/audit/audit_test.go | 154 ++++++ internal/backend/runtime/omni/audit/data.go | 130 +++++ internal/backend/runtime/omni/audit/gate.go | 143 ----- .../backend/runtime/omni/audit/hooks/hooks.go | 388 ++++++++++++++ internal/backend/runtime/omni/audit/log.go | 76 --- .../runtime/omni/audit/log_file_test.go | 25 +- .../backend/runtime/omni/audit/log_test.go | 116 ---- internal/backend/runtime/omni/audit/state.go | 96 ++-- .../testdata/concurrent/2012-01-01.jsonlog | 500 +++++++++--------- .../testdata/currentday/2012-01-01.jsonlog | 6 +- .../audit/testdata/log/2012-01-01.jsonlog | 8 +- .../audit/testdata/nextday/2012-01-01.jsonlog | 4 +- .../audit/testdata/nextday/2012-01-02.jsonlog | 2 +- internal/backend/runtime/omni/state.go | 250 +++++---- internal/backend/server.go | 31 +- internal/pkg/auth/interceptor/auth_config.go | 14 +- internal/pkg/auth/interceptor/jwt.go | 4 +- internal/pkg/auth/interceptor/saml.go | 4 +- internal/pkg/auth/interceptor/signature.go | 5 +- internal/pkg/grpcutil/audit.go | 6 +- 27 files changed, 1683 insertions(+), 795 deletions(-) create mode 100644 internal/backend/runtime/omni/audit/audit_test.go create mode 100644 internal/backend/runtime/omni/audit/data.go delete mode 100644 internal/backend/runtime/omni/audit/gate.go create mode 100644 internal/backend/runtime/omni/audit/hooks/hooks.go delete mode 100644 internal/backend/runtime/omni/audit/log.go delete mode 100644 internal/backend/runtime/omni/audit/log_test.go diff --git a/client/go.mod b/client/go.mod index 5dfc39ff..14b33058 100644 --- a/client/go.mod +++ b/client/go.mod @@ -15,6 +15,7 @@ require ( github.com/blang/semver v3.5.1+incompatible github.com/cosi-project/runtime v0.5.5 github.com/fatih/color v1.17.0 + github.com/gertd/go-pluralize v0.2.1 github.com/google/uuid v1.6.0 github.com/gosuri/uiprogress v0.0.1 github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 @@ -50,7 +51,6 @@ require ( github.com/cpuguy83/go-md2man/v2 v2.0.4 //indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/gertd/go-pluralize v0.2.1 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/go-cmp v0.6.0 // indirect diff --git a/cmd/omni/main.go b/cmd/omni/main.go index cb1eb83c..5a3db918 100644 --- a/cmd/omni/main.go +++ b/cmd/omni/main.go @@ -151,6 +151,13 @@ var rootCmd = &cobra.Command{ //nolint:gocognit func runWithState(logger *zap.Logger) func(context.Context, state.State, *virtual.State) error { return func(ctx context.Context, resourceState state.State, virtualState *virtual.State) error { + auditWrap, auditErr := omni.NewAuditWrap(resourceState, config.Config, logger) + if auditErr != nil { + return auditErr + } + + resourceState = auditWrap.WrapState(resourceState) + talosClientFactory := talos.NewClientFactory(resourceState, logger) prometheus.MustRegister(talosClientFactory) @@ -260,6 +267,7 @@ func runWithState(logger *zap.Logger) func(context.Context, state.State, *virtua rootCmdArgs.keyFile, rootCmdArgs.certFile, backend.NewProxyServer(rootCmdArgs.frontendBind, handler, rootCmdArgs.keyFile, rootCmdArgs.certFile), + auditWrap, logger, ) if err != nil { diff --git a/internal/backend/grpc/auth.go b/internal/backend/grpc/auth.go index 62c588c5..08e2042a 100644 --- a/internal/backend/grpc/auth.go +++ b/internal/backend/grpc/auth.go @@ -29,13 +29,11 @@ import ( "github.com/siderolabs/omni/client/api/omni/specs" "github.com/siderolabs/omni/client/pkg/omni/resources" authres "github.com/siderolabs/omni/client/pkg/omni/resources/auth" - "github.com/siderolabs/omni/internal/backend/runtime/omni/audit" "github.com/siderolabs/omni/internal/backend/runtime/omni/controllers/omni" "github.com/siderolabs/omni/internal/pkg/auth" "github.com/siderolabs/omni/internal/pkg/auth/actor" "github.com/siderolabs/omni/internal/pkg/auth/role" "github.com/siderolabs/omni/internal/pkg/config" - "github.com/siderolabs/omni/internal/pkg/ctxstore" ) const ( @@ -145,17 +143,6 @@ func (s *authServer) RegisterPublicKey(ctx context.Context, request *authpb.Regi newPubKey := authres.NewPublicKey(resources.DefaultNamespace, pubKey.id) - auditData, ok := ctxstore.Value[*audit.Data](ctx) - if !ok { - return nil, errors.New("audit data not found") - } - - auditData.UserID = userID - auditData.Fingerprint = pubKey.id - auditData.PublicKeyExpiration = pubKey.expiration.Unix() - auditData.Role = pubKeyRole - auditData.Email = email - _, err = safe.StateGet[*authres.PublicKey](ctx, s.state, newPubKey.Metadata()) if state.IsNotFoundError(err) { setPubKeyAttributes(newPubKey) @@ -249,16 +236,6 @@ func (s *authServer) ConfirmPublicKey(ctx context.Context, request *authpb.Confi return nil, errors.New("public key <> id mismatch") } - auditData, ok := ctxstore.Value[*audit.Data](ctx) - if !ok { - return nil, errors.New("audit data not found") - } - - auditData.UserID = userID - auditData.Fingerprint = pubKey.Metadata().ID() - auditData.PublicKeyExpiration = pubKey.TypedSpec().Value.Expiration.Seconds - auditData.Role = role.Role(pubKey.TypedSpec().Value.GetRole()) - _, err = safe.StateUpdateWithConflicts(ctx, s.state, pubKey.Metadata(), func(pk *authres.PublicKey) error { pk.TypedSpec().Value.Confirmed = true diff --git a/internal/backend/grpc/router/router.go b/internal/backend/grpc/router/router.go index 5c96f00d..d44f88bb 100644 --- a/internal/backend/grpc/router/router.go +++ b/internal/backend/grpc/router/router.go @@ -56,6 +56,11 @@ const ( talosBackendTTL = time.Hour ) +// TalosAuditor is an interface for auditing Talos access. +type TalosAuditor interface { + AuditTalosAccess(context.Context, string, string, string) error +} + // Router wraps grpc-proxy StreamDirector. type Router struct { talosBackends *expirable.LRU[string, proxy.Backend] @@ -67,6 +72,7 @@ type Router struct { nodeResolver NodeResolver verifier grpc.UnaryServerInterceptor cosiState state.State + talosAuditor TalosAuditor authEnabled bool } @@ -76,6 +82,7 @@ func NewRouter( cosiState state.State, nodeResolver NodeResolver, authEnabled bool, + talosAuditor TalosAuditor, verifier grpc.UnaryServerInterceptor, ) (*Router, error) { omniConn, err := grpc.NewClient(transport.Address(), @@ -95,12 +102,6 @@ func NewRouter( r := &Router{ talosBackends: expirable.NewLRU[string, proxy.Backend](talosBackendLRUSize, nil, talosBackendTTL), - omniBackend: NewOmniBackend("omni", nodeResolver, omniConn), - cosiState: cosiState, - nodeResolver: nodeResolver, - authEnabled: authEnabled, - verifier: verifier, - metricCacheSize: prometheus.NewGauge(prometheus.GaugeOpts{ Name: "omni_grpc_proxy_talos_backend_cache_size", Help: "Number of Talos clients in the cache of gRPC Proxy.", @@ -117,6 +118,12 @@ func NewRouter( Name: "omni_grpc_proxy_talos_backend_cache_misses_total", Help: "Number of gRPC Proxy Talos client cache misses.", }), + omniBackend: NewOmniBackend("omni", nodeResolver, omniConn), + nodeResolver: nodeResolver, + verifier: verifier, + cosiState: cosiState, + talosAuditor: talosAuditor, + authEnabled: authEnabled, } return r, nil @@ -153,6 +160,10 @@ func (r *Router) Director(ctx context.Context, fullMethodName string) (proxy.Mod return proxy.One2One, nil, err } + if err = r.talosAuditor.AuditTalosAccess(ctx, fullMethodName, getClusterName(md), getNodeID(md)); err != nil { + return proxy.One2One, nil, err + } + return proxy.One2One, backends, nil } diff --git a/internal/backend/k8sproxy/k8sproxy.go b/internal/backend/k8sproxy/k8sproxy.go index 9e1900cc..ed318ebc 100644 --- a/internal/backend/k8sproxy/k8sproxy.go +++ b/internal/backend/k8sproxy/k8sproxy.go @@ -30,10 +30,15 @@ type Handler struct { chain http.Handler } +// MiddlewareWrapper is an interface for middleware wrappers. +type MiddlewareWrapper interface { + Wrap(http.Handler) http.Handler +} + // NewHandler creates a new Handler. -func NewHandler(keyFunc KeyProvider, clusterUUIDResolver ClusterUUIDResolver, logger *zap.Logger) (*Handler, error) { +func NewHandler(keyFunc KeyProvider, clusterUUIDResolver ClusterUUIDResolver, wrapper MiddlewareWrapper, logger *zap.Logger) (*Handler, error) { multiplexer := newMultiplexer() - proxy := newProxyHandler(multiplexer, logger) + proxy := wrapper.Wrap(newProxyHandler(multiplexer, logger)) handler := &Handler{ multiplexer: multiplexer, diff --git a/internal/backend/k8sproxy/middleware.go b/internal/backend/k8sproxy/middleware.go index 6c84d7e3..0cdbd809 100644 --- a/internal/backend/k8sproxy/middleware.go +++ b/internal/backend/k8sproxy/middleware.go @@ -19,6 +19,7 @@ import ( "go.uber.org/zap" "k8s.io/client-go/transport" + "github.com/siderolabs/omni/internal/backend/runtime/omni/audit" "github.com/siderolabs/omni/internal/pkg/ctxstore" ) @@ -118,6 +119,25 @@ func AuthorizeRequest(next http.Handler, keyFunc KeyProvider, clusterUUIDResolve req.Header.Add(transport.ImpersonateUserHeader, claims.Subject) + //nolint:contextcheck + req = req.WithContext(ctxstore.WithValue( + req.Context(), + &audit.Data{ + K8SAccess: &audit.K8SAccess{ + FullMethodName: req.Method + " " + req.URL.Path, + Command: req.Header.Get("Kubectl-Command"), + Session: req.Header.Get("Kubectl-Session"), + ClusterName: clusterName, + ClusterUUID: clusterUUID, + Body: "", + }, + Session: audit.Session{ + UserAgent: req.Header.Get("User-Agent"), + Email: claims.Subject, + }, + }, + )) + for _, group := range claims.Groups { req.Header.Add(transport.ImpersonateGroupHeader, group) } diff --git a/internal/backend/runtime/omni/audit/audit.go b/internal/backend/runtime/omni/audit/audit.go index bd2a66e9..147761de 100644 --- a/internal/backend/runtime/omni/audit/audit.go +++ b/internal/backend/runtime/omni/audit/audit.go @@ -7,24 +7,423 @@ package audit import ( - "github.com/siderolabs/omni/internal/pkg/auth/role" + "bytes" + "context" + "errors" + "fmt" + "io" + "net/http" + "os" + "reflect" + "sync" + "time" + + "github.com/cosi-project/runtime/pkg/resource" + "github.com/cosi-project/runtime/pkg/resource/meta" + "github.com/cosi-project/runtime/pkg/state" + "go.uber.org/zap" + + "github.com/siderolabs/omni/internal/pkg/ctxstore" ) -const ( - // Auth0 is auth0 confirmation type. - Auth0 = "auth0" - // SAML is SAML confirmation type. - SAML = "saml" +// NewLog creates a new audit logger. +func NewLog(auditLogDir string, logger *zap.Logger) (*Log, error) { + err := os.MkdirAll(auditLogDir, 0o755) + if err != nil { + return nil, fmt.Errorf("failed to create audit logger: %w", err) + } + + return &Log{ + logFile: NewLogFile(auditLogDir), + logger: logger, + mu: sync.RWMutex{}, + createHooks: map[resource.Type]CreateHook{}, + updateHooks: map[resource.Type]UpdateHook{}, + destroyHooks: map[resource.Type]DestroyHook{}, + updateWithConflictsHooks: map[resource.Type]UpdateWithConflictsHook{}, + }, nil +} + +// Log logs audit events. +// +//nolint:govet +type Log struct { + logFile *LogFile + logger *zap.Logger + + mu sync.RWMutex + createHooks map[resource.Type]CreateHook + updateHooks map[resource.Type]UpdateHook + destroyHooks map[resource.Type]DestroyHook + updateWithConflictsHooks map[resource.Type]UpdateWithConflictsHook +} + +// LogCreate logs the resource creation if there is a hook for this type. +func (l *Log) LogCreate(r resource.Resource) CreateHook { + l.mu.RLock() + defer l.mu.RUnlock() + + return l.createHooks[r.Metadata().Type()] +} + +// LogUpdate logs the resource update if there is a hook for this type. +func (l *Log) LogUpdate(newRes resource.Resource) UpdateHook { + l.mu.RLock() + defer l.mu.RUnlock() + + return l.updateHooks[newRes.Metadata().Type()] +} + +// LogDestroy logs the resource destruction if there is a hook for this type. +func (l *Log) LogDestroy(ptr resource.Pointer) DestroyHook { + l.mu.RLock() + defer l.mu.RUnlock() + + return l.destroyHooks[ptr.Type()] +} + +// LogUpdateWithConflicts logs the resource update with conflicts if there is a hook for this type. +func (l *Log) LogUpdateWithConflicts(ptr resource.Pointer) UpdateWithConflictsHook { + l.mu.RLock() + defer l.mu.RUnlock() + + return l.updateWithConflictsHooks[ptr.Type()] +} + +// AuditTalosAccess logs the talos access event. +func (l *Log) AuditTalosAccess(ctx context.Context, fullMethodName string, clusterID string, nodeID string) error { + data := extractData(ctx, options{ + userAgent: internalAgent, + newDataIfNone: true, + }) + if data == nil { + return nil + } + + if data.TalosAccess == nil { + data.TalosAccess = &TalosAccess{} + } + + data.TalosAccess.FullMethodName = fullMethodName + data.TalosAccess.ClusterName = clusterID + data.TalosAccess.MachineIP = nodeID + + return l.logFile.Dump(event{ + Type: "talos_access", + Time: time.Now().UnixMilli(), + Data: data, + }) +} + +// Wrap wraps the http.Handler with audit logging. +func (l *Log) Wrap(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + clonedReq := req.Clone(req.Context()) + + strData, body, err := duplicateReadCloser(req.Body) + if err != nil { + l.logger.Error("failed to clone request body", zap.Error(err)) + next.ServeHTTP(w, req) + + return + } + + clonedReq.Body = body + + data, ok := ctxstore.Value[*Data](req.Context()) + if !ok { + next.ServeHTTP(w, clonedReq) + + return + } + + if data.K8SAccess == nil { + data.K8SAccess = &K8SAccess{} + } + + data.K8SAccess.Body = strData + + err = l.logFile.Dump(event{ + Type: "k8s_access", + Time: time.Now().UnixMilli(), + Data: data, + }) + if err != nil { + l.logger.Error("failed to write audit log", zap.Error(err)) + } + + next.ServeHTTP(w, clonedReq) + }) +} + +func duplicateReadCloser(body io.ReadCloser) (string, io.ReadCloser, error) { + if body == nil { + return "", nil, nil + } + + var buf bytes.Buffer + + _, err := buf.ReadFrom(body) + if err != nil { + return "", nil, err + } + + return buf.String(), io.NopCloser(&buf), nil +} + +type ( + // CreateHook is a hook for specific type resource creation. + CreateHook = func(ctx context.Context, res resource.Resource, option ...state.CreateOption) error + // UpdateHook is a hook for specific type resource update. + UpdateHook = func(ctx context.Context, oldRes, newRes resource.Resource, opts ...state.UpdateOption) error + // UpdateWithConflictsHook is a hook for specific type resource update with conflicts. + UpdateWithConflictsHook = func(ctx context.Context, oldRes, newRes resource.Resource, option ...state.UpdateOption) error + // DestroyHook is a hook for specific type resource destruction. + DestroyHook = func(ctx context.Context, ptr resource.Pointer, option ...state.DestroyOption) error ) -// Data contains the audit data. -type Data struct { - UserAgent string `json:"user_agent,omitempty"` - IPAddress string `json:"ip_address,omitempty"` - UserID string `json:"user_id,omitempty"` - Role role.Role `json:"role,omitempty"` - Email string `json:"email,omitempty"` - ConfirmationType string `json:"confirmation_type,omitempty"` - Fingerprint string `json:"fingerprint,omitempty"` - PublicKeyExpiration int64 `json:"public_key_expiration,omitempty"` +//nolint:govet +type event struct { + Type string `json:"event_type,omitempty"` + ResourceType resource.Type `json:"resource_type,omitempty"` + Time int64 `json:"event_ts,omitempty"` + Data *Data `json:"event_data,omitempty"` } + +// Res is a resource type constraint. +type Res interface { + resource.Resource + meta.ResourceDefinitionProvider +} + +// ShouldLogCreate adds a creation hook to logger which informs what resource type should be logged and how to log it. +func ShouldLogCreate[T Res](l *Log, before func(context.Context, *Data, T, ...state.CreateOption) error, opts ...Option) { + resType, o := resourceType[T](), toOptions(opts...) + + setHook(l, l.createHooks, resType, func(ctx context.Context, res resource.Resource, option ...state.CreateOption) error { + data := extractData(ctx, o) + if data == nil { + return nil + } + + typedRes, ok := res.(T) + if !ok { + return fmt.Errorf("resource type %T != expected type %T passed to create hook", res, typedRes) + } + + if err := before(ctx, data, typedRes, option...); err != nil { + if errors.Is(err, ErrNoLog) { + return nil + } + + return err + } + + if err := l.logFile.Dump(makeEvent("create", resType, data)); err != nil { + return fmt.Errorf("failed to write audit log for create event: %w", err) + } + + return nil + }) +} + +// ShouldLogUpdate adds an update hook to logger which informs what resource type should be logged and how to log it. +func ShouldLogUpdate[T Res](l *Log, before func(context.Context, *Data, T, T, ...state.UpdateOption) error, opts ...Option) { + resType, o := resourceType[T](), toOptions(opts...) + + setHook(l, l.updateHooks, resType, func(ctx context.Context, oldRes, newRes resource.Resource, opts ...state.UpdateOption) error { + oldTypedRes, ok := oldRes.(T) + if !ok { + return fmt.Errorf("old resource type %T != expected type %T passed to update hook", oldRes, oldTypedRes) + } + + newTypesRed, ok := newRes.(T) + if !ok { + return fmt.Errorf("new resource type %T != expected type %T passed to update hook", newRes, newTypesRed) + } + + if isEqualResource(oldTypedRes, newTypesRed) { + return nil + } + + data := extractData(ctx, o) + if data == nil { + return nil + } + + eventType := "update" + if newTypesRed.Metadata().Phase() == resource.PhaseTearingDown { + eventType = "teardown" + } + + if err := before(ctx, data, oldTypedRes, newTypesRed, opts...); err != nil { + if errors.Is(err, ErrNoLog) { + return nil + } + + return err + } + + if err := l.logFile.Dump(makeEvent(eventType, resType, data)); err != nil { + return fmt.Errorf("failed to write audit log for update event: %w", err) + } + + return nil + }) +} + +// ShouldLogDestroy adds a destruction hook to logger which informs what resource type should be logged and how to log it. +func ShouldLogDestroy(l *Log, resType resource.Type, before func(context.Context, *Data, resource.Pointer, ...state.DestroyOption) error, opts ...Option) { + o := toOptions(opts...) + + setHook(l, l.destroyHooks, resType, func(ctx context.Context, ptr resource.Pointer, option ...state.DestroyOption) error { + data := extractData(ctx, o) + if data == nil { + return nil + } + + if err := before(ctx, data, ptr, option...); err != nil { + return err + } + + if err := l.logFile.Dump(makeEvent("destroy", resType, data)); err != nil { + if errors.Is(err, ErrNoLog) { + return nil + } + + return fmt.Errorf("failed to write audit log for destroy event: %w", err) + } + + return nil + }) +} + +// ShouldLogUpdateWithConflicts adds an update with conflicts hook to logger which informs what resource type should be logged and how to log it. +func ShouldLogUpdateWithConflicts[T Res](l *Log, before func(context.Context, *Data, T, T, ...state.UpdateOption) error, opts ...Option) { + resType, o := resourceType[T](), toOptions(opts...) + + setHook(l, l.updateWithConflictsHooks, resType, func(ctx context.Context, oldRes, newRes resource.Resource, option ...state.UpdateOption) error { + oldTypedRes, ok := oldRes.(T) + if !ok { + return fmt.Errorf("old resource type %T != expected type %T passed to update hook", oldRes, oldTypedRes) + } + + newTypesRed, ok := newRes.(T) + if !ok { + return fmt.Errorf("new resource type %T != expected type %T passed to update hook", newRes, newTypesRed) + } + + if isEqualResource(oldTypedRes, newTypesRed) { + return nil + } + + data := extractData(ctx, o) + if data == nil { + return nil + } + + if err := before(ctx, data, oldTypedRes, newTypesRed, option...); err != nil { + if errors.Is(err, ErrNoLog) { + return nil + } + + return err + } + + if err := l.logFile.Dump(makeEvent("update_with_conflicts", resType, data)); err != nil { + return fmt.Errorf("failed to write audit log for update with conflicts events: %w", err) + } + + return nil + }) +} + +func resourceType[T meta.ResourceDefinitionProvider]() resource.Type { + var zero T + + return zero.ResourceDefinition().Type +} + +func makeEvent(eventType string, resType resource.Type, data *Data) event { + return event{ + Type: eventType, + ResourceType: resType, + Time: time.Now().UnixMilli(), + Data: data, + } +} + +func setHook[T any](l *Log, hooks map[resource.Type]T, resType resource.Type, hook T) { + l.mu.Lock() + defer l.mu.Unlock() + + if _, ok := hooks[resType]; ok { + panic(fmt.Errorf("hook for type %s already exists", resType)) + } + + hooks[resType] = hook +} + +func extractData(ctx context.Context, opts options) *Data { + data, ok := ctxstore.Value[*Data](ctx) + if ok { + return data + } + + if !opts.newDataIfNone { + return nil + } + + result := &Data{} + + if opts.userAgent != "" { + result.Session.UserAgent = opts.userAgent + } + + return result +} + +func isEqualResource(oldRes, newRes resource.Resource) bool { + if oldRes.Metadata().ID() != newRes.Metadata().ID() { + return false + } + + oldSpec, newSpec := oldRes.Spec(), newRes.Spec() + + if equality, ok := oldSpec.(interface{ Equal(any) bool }); ok { + return equality.Equal(newSpec) + } + + return reflect.DeepEqual(oldSpec, newSpec) +} + +type options struct { + userAgent string + newDataIfNone bool +} + +const internalAgent = "Omni-Internal-Agent" + +// WithInternalAgent informs hook that if [audit.Data] is missing in context it should create new one with internal agent. +func WithInternalAgent() Option { + return func(o *options) { + o.newDataIfNone = true + o.userAgent = internalAgent + } +} + +// Option is a function that modifies options. +type Option func(*options) + +func toOptions(o ...Option) options { + var result options + + for _, v := range o { + v(&result) + } + + return result +} + +// ErrNoLog is returned by hooks to indicate that the event should be ignored. +var ErrNoLog = errors.New("ignore this event") diff --git a/internal/backend/runtime/omni/audit/audit_test.go b/internal/backend/runtime/omni/audit/audit_test.go new file mode 100644 index 00000000..9490b299 --- /dev/null +++ b/internal/backend/runtime/omni/audit/audit_test.go @@ -0,0 +1,154 @@ +// Copyright (c) 2024 Sidero Labs, Inc. +// +// Use of this software is governed by the Business Source License +// included in the LICENSE file. + +package audit_test + +import ( + "context" + "embed" + "encoding/json" + "errors" + "io" + "os" + "strings" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/siderolabs/gen/xtesting/must" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zaptest" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/siderolabs/omni/client/api/omni/specs" + "github.com/siderolabs/omni/client/pkg/omni/resources" + "github.com/siderolabs/omni/client/pkg/omni/resources/auth" + "github.com/siderolabs/omni/internal/backend/runtime/omni/audit" + "github.com/siderolabs/omni/internal/backend/runtime/omni/audit/hooks" + "github.com/siderolabs/omni/internal/pkg/ctxstore" +) + +//go:embed testdata/log +var logDir embed.FS + +func TestAudit(t *testing.T) { + tempDir := t.TempDir() + l := must.Value(audit.NewLog(tempDir, zaptest.NewLogger(t)))(t) + + hooks.Init(l) + + res := auth.NewPublicKey(resources.DefaultNamespace, "917e47635eb900d0ae66271dd1e06966e048c4f3") + + res.Metadata().Labels().Set(auth.LabelPublicKeyUserID, "002cf196-1767-43fd-8e3d-91241e2ce70c") + + res.TypedSpec().Value.Identity = &specs.Identity{Email: "dmitry.matrenichev@siderolabs.com"} + res.TypedSpec().Value.Role = "Admin" + res.TypedSpec().Value.PublicKey = nil + res.TypedSpec().Value.Expiration = timestamppb.New(time.Unix(1325587579, 0)) + + createCtx := func() context.Context { + ad := makeAuditData("Mozilla/5.0", "10.10.0.1", "") + + return ctxstore.WithValue(context.Background(), &ad) + } + + actions := []func(*testing.T){ + func(t *testing.T) { + fn := l.LogCreate(res) + + require.NoError(t, fn(createCtx(), res)) + }, + func(t *testing.T) { + newRes := res.DeepCopy().(*auth.PublicKey) //nolint:errcheck,forcetypeassert + newRes.TypedSpec().Value.Confirmed = true + fn := l.LogUpdate(res) + + require.NoError(t, fn(createCtx(), res, newRes)) + + res = newRes + }, + func(t *testing.T) { + newRes := res.DeepCopy().(*auth.PublicKey) //nolint:errcheck,forcetypeassert + newRes.TypedSpec().Value.Confirmed = false + fn := l.LogUpdateWithConflicts(res.Metadata()) + + require.NoError(t, fn(createCtx(), res, newRes)) + + res = newRes + }, + func(t *testing.T) { + fn := l.LogDestroy(res.Metadata()) + + require.NoError(t, fn(createCtx(), res.Metadata())) + }, + func(t *testing.T) { + fn := l.LogCreate(res) + + require.NoError(t, fn(createCtx(), res)) + }, + } + + for _, action := range actions { + action(t) + } + + equalDirs( + t, + &wrapFS{ + subFS: fsSub(t, logDir, "log"), + File: "2012-01-01.jsonlog", + }, + os.DirFS(tempDir).(subFS), //nolint:forcetypeassert + cmpIgnoreTime, + ) +} + +type wrapFS struct { + subFS + File string +} + +func (w *wrapFS) ReadFile(string) ([]byte, error) { + return w.subFS.ReadFile(w.File) +} + +func cmpIgnoreTime(t *testing.T, expected string, actual string) { + expectedEvents := loadEvents(t, expected) + actualEvents := loadEvents(t, actual) + + diff := cmp.Diff(expectedEvents, actualEvents, cmpopts.IgnoreMapEntries(func(k string, v any) bool { + _, ok := v.(json.Number) + + return ok && k == "event_ts" + })) + if diff != "" { + t.Fatalf("events mismatch (-want +got):\n%s", diff) + } +} + +func loadEvents(t *testing.T, events string) []any { + var result []any + + decoder := json.NewDecoder(strings.NewReader(events)) + decoder.UseNumber() + + for { + var event any + + err := decoder.Decode(&event) + if err != nil { + if errors.Is(err, io.EOF) { + break + } + + t.Fatalf("failed to decode event: %v", err) + } + + result = append(result, event) + } + + return result +} diff --git a/internal/backend/runtime/omni/audit/data.go b/internal/backend/runtime/omni/audit/data.go new file mode 100644 index 00000000..05768815 --- /dev/null +++ b/internal/backend/runtime/omni/audit/data.go @@ -0,0 +1,130 @@ +// Copyright (c) 2024 Sidero Labs, Inc. +// +// Use of this software is governed by the Business Source License +// included in the LICENSE file. + +package audit + +import ( + "github.com/cosi-project/runtime/pkg/resource" + + "github.com/siderolabs/omni/client/api/omni/specs" + "github.com/siderolabs/omni/internal/pkg/auth/role" +) + +const ( + // Auth0 is auth0 confirmation type. + Auth0 = "auth0" + // SAML is SAML confirmation type. + SAML = "saml" +) + +// Data contains the audit data. +type Data struct { + NewUser *NewUser `json:"new_user,omitempty"` + Machine *Machine `json:"machine,omitempty"` + MachineLabels *MachineLabels `json:"machine_labels,omitempty"` + AccessPolicy *AccessPolicy `json:"access_policy,omitempty"` + Cluster *Cluster `json:"cluster,omitempty"` + MachineSet *MachineSet `json:"machine_set,omitempty"` + MachineSetNode *MachineSetNode `json:"machine_set_node,omitempty"` + ConfigPatch *ConfigPatch `json:"config_patch,omitempty"` + TalosAccess *TalosAccess `json:"talos_access,omitempty"` + K8SAccess *K8SAccess `json:"k8s_access,omitempty"` + Session Session `json:"session,omitempty"` +} + +// Session contains information about the current session. +type Session struct { + UserAgent string `json:"user_agent,omitempty"` + IPAddress string `json:"ip_address,omitempty"` + UserID string `json:"user_id,omitempty"` + Role role.Role `json:"role,omitempty"` + Email string `json:"email,omitempty"` + Fingerprint string `json:"fingerprint,omitempty"` + ConfirmationType string `json:"confirmation_type,omitempty"` + PublicKeyExpiration int64 `json:"public_key_expiration,omitempty"` +} + +// NewUser contains information about the new user. +type NewUser struct { + Role role.Role `json:"role,omitempty"` + UserID string `json:"id,omitempty"` + Email string `json:"email,omitempty"` + IsServiceAccount bool `json:"is_service_account,omitempty"` +} + +// Machine contains information about the machine. +type Machine struct { + Labels map[string]string `json:"labels,omitempty"` + ID string `json:"id,omitempty"` + ManagementAddress string `json:"management_address,omitempty"` + IsConnected bool `json:"is_connected,omitempty"` +} + +// MachineLabels contains information about the machine labels. +type MachineLabels struct { + Labels map[string]string `json:"labels,omitempty"` + ID string `json:"id,omitempty"` +} + +// AccessPolicy contains information about the access policy. +type AccessPolicy struct { + ID resource.ID `json:"id,omitempty"` + ClusterGroups map[string]*specs.AccessPolicyClusterGroup `json:"cluster_groups,omitempty"` + UserGroups map[string]*specs.AccessPolicyUserGroup `json:"user_groups,omitempty"` + Rules []*specs.AccessPolicyRule `json:"rules,omitempty"` + Tests []*specs.AccessPolicyTest `json:"tests,omitempty"` +} + +// Cluster struct contains information about the cluster. +type Cluster struct { + ID string `json:"id,omitempty"` + Labels map[string]string `json:"labels,omitempty"` + BackupConfiguration *specs.EtcdBackupConf `json:"backup_configuration,omitempty"` + Features *specs.ClusterSpec_Features `json:"features,omitempty"` + KubernetesVersion string `json:"kubernetes_version,omitempty"` + TalosVersion string `json:"talos_version,omitempty"` +} + +// MachineSet struct contains information about the machine set. +type MachineSet struct { + Labels map[string]string `json:"labels,omitempty"` + MachineClass *specs.MachineSetSpec_MachineClass `json:"machine_class,omitempty"` + BootstrapSpec *specs.MachineSetSpec_BootstrapSpec `json:"bootstrap_spec,omitempty"` + UpdateStrategyConfig *specs.MachineSetSpec_UpdateStrategyConfig `json:"update_strategy_config,omitempty"` + DeleteStrategyConfig *specs.MachineSetSpec_UpdateStrategyConfig `json:"delete_strategy_config,omitempty"` + ID string `json:"id,omitempty"` + UpdateStrategy string `json:"update_strategy,omitempty"` + DeleteStrategy string `json:"delete_strategy,omitempty"` +} + +// MachineSetNode struct contains information about the machine set node. +type MachineSetNode struct { + Labels map[string]string `json:"labels,omitempty"` + ID string `json:"id,omitempty"` +} + +// ConfigPatch struct contains information about the config patch. +type ConfigPatch struct { + Labels map[string]string `json:"labels,omitempty"` + ID string `json:"id,omitempty"` + Data string `json:"data,omitempty"` +} + +// TalosAccess struct contains information about the access to the Talos node. +type TalosAccess struct { + FullMethodName string `json:"full_method_name,omitempty"` + ClusterName string `json:"cluster_name,omitempty"` + MachineIP string `json:"machine_ip,omitempty"` +} + +// K8SAccess struct contains information about the access to the Kubernetes cluster. +type K8SAccess struct { + FullMethodName string `json:"full_method_name,omitempty"` + Command string `json:"command,omitempty"` + Body string `json:"body,omitempty"` + Session string `json:"kube_session,omitempty"` + ClusterName string `json:"cluster_name,omitempty"` + ClusterUUID string `json:"cluster_uuid,omitempty"` +} diff --git a/internal/backend/runtime/omni/audit/gate.go b/internal/backend/runtime/omni/audit/gate.go deleted file mode 100644 index 1064df50..00000000 --- a/internal/backend/runtime/omni/audit/gate.go +++ /dev/null @@ -1,143 +0,0 @@ -// Copyright (c) 2024 Sidero Labs, Inc. -// -// Use of this software is governed by the Business Source License -// included in the LICENSE file. - -package audit - -import ( - "context" - "sync" - - "github.com/cosi-project/runtime/pkg/resource" - "github.com/siderolabs/gen/pair" -) - -// Check is a function that checks if the event is allowed. -type Check = func(ctx context.Context, eventType EventType, args ...any) bool - -// Gate is a gate that checks if the event is allowed. -// -//nolint:govet -type Gate struct { - mu sync.RWMutex - fns [10]map[resource.Type]Check -} - -// Check checks if the event is allowed. -func (g *Gate) Check(ctx context.Context, eventType EventType, typ resource.Type, args ...any) bool { - fn := g.check(eventType, typ) - if fn == nil { - return false - } - - return fn(ctx, eventType, args...) -} - -func (g *Gate) check(eventType EventType, typ resource.Type) Check { - g.mu.RLock() - defer g.mu.RUnlock() - - if g.fns[0] == nil { - return nil - } - - for i, e := range allEvents { - if eventType == e.typ { - return g.fns[i][typ] - } - } - - return nil -} - -// AddChecks adds checks for the event types. It's allowed to pass several at once using bitwise OR. -func (g *Gate) AddChecks(eventTypes EventType, pairs []pair.Pair[resource.Type, Check]) { - g.mu.Lock() - defer g.mu.Unlock() - - if g.fns[0] == nil { - for i := range g.fns { - g.fns[i] = map[resource.Type]Check{} - } - } - - for _, p := range pairs { - g.addCheck(eventTypes, p) - } -} - -func (g *Gate) addCheck(eventTypes EventType, p pair.Pair[resource.Type, Check]) { - for i, e := range allEvents { - if e.typ&eventTypes != 0 { - if _, ok := g.fns[i][p.F1]; ok { - panic("duplicate check") - } - - g.fns[i][p.F1] = p.F2 - } - } -} - -// AllowAll is a check that allows all events for certain event type. -func AllowAll(context.Context, EventType, ...any) bool { - return true -} - -const ( - // EventGet is the get event type. - EventGet EventType = 1 << iota - // EventList is the list event type. - EventList - // EventCreate is the create event type. - EventCreate - // EventUpdate is the update event type. - EventUpdate - // EventDestroy is the destroy event type. - EventDestroy - // EventWatch is the watch event type. - EventWatch - // EventWatchKind is the watch kind event type. - EventWatchKind - // EventWatchKindAggregated is the watch kind aggregated event type. - EventWatchKindAggregated - // EventUpdateWithConflicts is the update with conflicts event type. - EventUpdateWithConflicts - // EventWatchFor is the watch for event type. - EventWatchFor -) - -// EventType represents the type of event. -type EventType int - -// MarshalJSON marshals the event type to JSON. -func (e *EventType) MarshalJSON() ([]byte, error) { - return []byte(`"` + e.String() + `"`), nil -} - -// String returns the string representation of the event type. -func (e *EventType) String() string { - for _, ev := range allEvents { - if *e == ev.typ { - return ev.str - } - } - - return "" -} - -var allEvents = []struct { - str string - typ EventType -}{ - {"get", EventGet}, - {"list", EventList}, - {"create", EventCreate}, - {"update", EventUpdate}, - {"destroy", EventDestroy}, - {"watch", EventWatch}, - {"watch_kind", EventWatchKind}, - {"watch_kind_aggregated", EventWatchKindAggregated}, - {"update_with_conflicts", EventUpdateWithConflicts}, - {"watch_for", EventWatchFor}, -} diff --git a/internal/backend/runtime/omni/audit/hooks/hooks.go b/internal/backend/runtime/omni/audit/hooks/hooks.go new file mode 100644 index 00000000..12bc256d --- /dev/null +++ b/internal/backend/runtime/omni/audit/hooks/hooks.go @@ -0,0 +1,388 @@ +// Copyright (c) 2024 Sidero Labs, Inc. +// +// Use of this software is governed by the Business Source License +// included in the LICENSE file. + +// Package hooks define a set of hooks that can be used to audit events in the system. +package hooks + +import ( + "context" + "errors" + "maps" + + "github.com/cosi-project/runtime/pkg/resource" + "github.com/cosi-project/runtime/pkg/state" + + "github.com/siderolabs/omni/client/pkg/omni/resources/auth" + "github.com/siderolabs/omni/client/pkg/omni/resources/omni" + "github.com/siderolabs/omni/internal/backend/runtime/omni/audit" + "github.com/siderolabs/omni/internal/pkg/auth/role" +) + +// Init initializes the audit hooks. +func Init(a *audit.Log) { + audit.ShouldLogCreate(a, publicKeyCreate, audit.WithInternalAgent()) + audit.ShouldLogUpdate(a, publicKeyUpdate, audit.WithInternalAgent()) + audit.ShouldLogUpdateWithConflicts(a, publicKeyUpdate, audit.WithInternalAgent()) + audit.ShouldLogDestroy(a, auth.PublicKeyType, publicKeyDestroy, audit.WithInternalAgent()) + + audit.ShouldLogCreate(a, userCreate, audit.WithInternalAgent()) + audit.ShouldLogCreate(a, identityCreate, audit.WithInternalAgent()) + audit.ShouldLogUpdate(a, userUpdate, audit.WithInternalAgent()) + audit.ShouldLogUpdate(a, identityUpdate, audit.WithInternalAgent()) + audit.ShouldLogUpdateWithConflicts(a, userUpdate, audit.WithInternalAgent()) + audit.ShouldLogUpdateWithConflicts(a, identityUpdate, audit.WithInternalAgent()) + audit.ShouldLogDestroy(a, auth.UserType, userDestroy, audit.WithInternalAgent()) + audit.ShouldLogDestroy(a, auth.IdentityType, identityDestroy, audit.WithInternalAgent()) + + audit.ShouldLogCreate(a, machineCreate, audit.WithInternalAgent()) + audit.ShouldLogUpdate(a, machineUpdate, audit.WithInternalAgent()) + audit.ShouldLogUpdateWithConflicts(a, machineUpdate, audit.WithInternalAgent()) + audit.ShouldLogDestroy(a, omni.MachineType, machineDestroy, audit.WithInternalAgent()) + + audit.ShouldLogCreate(a, machineLabelsCreate, audit.WithInternalAgent()) + audit.ShouldLogUpdate(a, machineLabelsUpdate, audit.WithInternalAgent()) + audit.ShouldLogUpdateWithConflicts(a, machineLabelsUpdate, audit.WithInternalAgent()) + audit.ShouldLogDestroy(a, omni.MachineLabelsType, machineLabelsDestroy, audit.WithInternalAgent()) + + audit.ShouldLogCreate(a, accessPolicyCreate, audit.WithInternalAgent()) + audit.ShouldLogUpdate(a, accessPolicyUpdate, audit.WithInternalAgent()) + audit.ShouldLogUpdateWithConflicts(a, accessPolicyUpdate, audit.WithInternalAgent()) + audit.ShouldLogDestroy(a, auth.AccessPolicyType, accessPolicyDestroy, audit.WithInternalAgent()) + + audit.ShouldLogCreate(a, clusterCreate, audit.WithInternalAgent()) + audit.ShouldLogUpdate(a, clusterUpdate, audit.WithInternalAgent()) + audit.ShouldLogUpdateWithConflicts(a, clusterUpdate, audit.WithInternalAgent()) + audit.ShouldLogDestroy(a, omni.ClusterType, clusterDestroy, audit.WithInternalAgent()) + + audit.ShouldLogCreate(a, machineSetCreate, audit.WithInternalAgent()) + audit.ShouldLogUpdate(a, machineSetUpdate, audit.WithInternalAgent()) + audit.ShouldLogUpdateWithConflicts(a, machineSetUpdate, audit.WithInternalAgent()) + audit.ShouldLogDestroy(a, omni.MachineSetType, machineSetDestroy, audit.WithInternalAgent()) + + audit.ShouldLogCreate(a, machineSetNodeCreate, audit.WithInternalAgent()) + audit.ShouldLogUpdate(a, machineSetNodeUpdate, audit.WithInternalAgent()) + audit.ShouldLogUpdateWithConflicts(a, machineSetNodeUpdate, audit.WithInternalAgent()) + audit.ShouldLogDestroy(a, omni.MachineSetNodeType, machineSetNodeDestroy, audit.WithInternalAgent()) + + audit.ShouldLogCreate(a, configPatchCreate, audit.WithInternalAgent()) + audit.ShouldLogUpdate(a, configPatchUpdate, audit.WithInternalAgent()) + audit.ShouldLogUpdateWithConflicts(a, configPatchUpdate, audit.WithInternalAgent()) + audit.ShouldLogDestroy(a, omni.ConfigPatchType, configPatchDestroy, audit.WithInternalAgent()) +} + +func publicKeyCreate(_ context.Context, data *audit.Data, res *auth.PublicKey, _ ...state.CreateOption) error { + return handlePublicKey(data, res) +} + +func publicKeyUpdate(_ context.Context, data *audit.Data, _, newRes *auth.PublicKey, _ ...state.UpdateOption) error { + return handlePublicKey(data, newRes) +} + +func publicKeyDestroy(_ context.Context, data *audit.Data, ptr resource.Pointer, _ ...state.DestroyOption) error { + data.Session.Fingerprint = ptr.ID() + + return nil +} + +func handlePublicKey(data *audit.Data, res *auth.PublicKey) error { + userID, ok := res.Metadata().Labels().Get(auth.LabelPublicKeyUserID) + if !ok { + return errors.New("missing user ID on public key creation") + } + + r, err := role.Parse(res.TypedSpec().Value.GetRole()) + if err != nil { + return err + } + + data.Session.Fingerprint = res.Metadata().ID() + data.Session.UserID = userID + data.Session.Email = res.TypedSpec().Value.GetIdentity().GetEmail() + data.Session.Role = r + data.Session.PublicKeyExpiration = res.TypedSpec().Value.GetExpiration().Seconds + + return nil +} + +func userCreate(_ context.Context, data *audit.Data, res *auth.User, _ ...state.CreateOption) error { + return handleUser(data, res) +} + +func userUpdate(_ context.Context, data *audit.Data, _, newRes *auth.User, _ ...state.UpdateOption) error { + return handleUser(data, newRes) +} + +func handleUser(data *audit.Data, res *auth.User) error { + initPtrField(&data.NewUser) + + data.NewUser.Role = role.Role(res.TypedSpec().Value.Role) + data.NewUser.UserID = res.Metadata().ID() + + return nil +} + +func userDestroy(_ context.Context, data *audit.Data, ptr resource.Pointer, _ ...state.DestroyOption) error { + initPtrField(&data.NewUser) + + data.NewUser.UserID = ptr.ID() + + return nil +} + +func identityCreate(_ context.Context, data *audit.Data, res *auth.Identity, _ ...state.CreateOption) error { + return handleIdentity(data, res) +} + +func identityUpdate(_ context.Context, data *audit.Data, _, newRes *auth.Identity, _ ...state.UpdateOption) error { + return handleIdentity(data, newRes) +} + +func handleIdentity(data *audit.Data, res *auth.Identity) error { + initPtrField(&data.NewUser) + + data.NewUser.Email = res.Metadata().ID() + data.NewUser.UserID = res.TypedSpec().Value.GetUserId() + data.NewUser.IsServiceAccount = isServiceAccount(res.Metadata()) + + return nil +} + +func identityDestroy(_ context.Context, data *audit.Data, ptr resource.Pointer, _ ...state.DestroyOption) error { + initPtrField(&data.NewUser) + + data.NewUser.Email = ptr.ID() + + md, ok := ptr.(*resource.Metadata) + if !ok { + return nil + } + + data.NewUser.IsServiceAccount = isServiceAccount(md) + + return nil +} + +func isServiceAccount(md *resource.Metadata) bool { + _, isServiceAccount := md.Labels().Get(auth.LabelIdentityTypeServiceAccount) + + return isServiceAccount +} + +func machineCreate(_ context.Context, data *audit.Data, res *omni.Machine, _ ...state.CreateOption) error { + return handleMachine(data, res) +} + +func machineUpdate(_ context.Context, data *audit.Data, _, newRes *omni.Machine, _ ...state.UpdateOption) error { + if newRes.Metadata().Phase() != resource.PhaseTearingDown { + return audit.ErrNoLog + } + + return handleMachine(data, newRes) +} + +func handleMachine(data *audit.Data, res *omni.Machine) error { + initPtrField(&data.Machine) + + data.Machine.ID = res.Metadata().ID() + data.Machine.IsConnected = res.TypedSpec().Value.GetConnected() + data.Machine.ManagementAddress = res.TypedSpec().Value.GetManagementAddress() + data.Machine.Labels = maps.Clone(res.Metadata().Labels().Raw()) + + return nil +} + +func machineDestroy(_ context.Context, data *audit.Data, ptr resource.Pointer, _ ...state.DestroyOption) error { + initPtrField(&data.Machine) + + data.Machine.ID = ptr.ID() + + md, ok := ptr.(*resource.Metadata) + if !ok { + return nil + } + + data.Machine.Labels = maps.Clone(md.Labels().Raw()) + + return nil +} + +func machineLabelsCreate(_ context.Context, data *audit.Data, res *omni.MachineLabels, _ ...state.CreateOption) error { + initPtrField(&data.MachineLabels) + + data.MachineLabels.ID = res.Metadata().ID() + data.MachineLabels.Labels = maps.Clone(res.Metadata().Labels().Raw()) + + return nil +} + +func machineLabelsUpdate(_ context.Context, data *audit.Data, _, newRes *omni.MachineLabels, _ ...state.UpdateOption) error { + initPtrField(&data.MachineLabels) + + data.MachineLabels.ID = newRes.Metadata().ID() + data.MachineLabels.Labels = maps.Clone(newRes.Metadata().Labels().Raw()) + + return nil +} + +func machineLabelsDestroy(_ context.Context, data *audit.Data, ptr resource.Pointer, _ ...state.DestroyOption) error { + initPtrField(&data.MachineLabels) + + data.MachineLabels.ID = ptr.ID() + + return nil +} + +func accessPolicyCreate(_ context.Context, data *audit.Data, res *auth.AccessPolicy, _ ...state.CreateOption) error { + return handleAccessPolicy(data, res) +} + +func accessPolicyUpdate(_ context.Context, data *audit.Data, _, newRes *auth.AccessPolicy, _ ...state.UpdateOption) error { + return handleAccessPolicy(data, newRes) +} + +func handleAccessPolicy(data *audit.Data, res *auth.AccessPolicy) error { + initPtrField(&data.AccessPolicy) + + data.AccessPolicy.ID = res.Metadata().ID() + data.AccessPolicy.ClusterGroups = res.TypedSpec().Value.GetClusterGroups() + data.AccessPolicy.UserGroups = res.TypedSpec().Value.GetUserGroups() + data.AccessPolicy.Rules = res.TypedSpec().Value.GetRules() + data.AccessPolicy.Tests = res.TypedSpec().Value.GetTests() + + return nil +} + +func accessPolicyDestroy(_ context.Context, data *audit.Data, ptr resource.Pointer, _ ...state.DestroyOption) error { + initPtrField(&data.AccessPolicy) + + data.AccessPolicy.ID = ptr.ID() + + return nil +} + +func clusterCreate(_ context.Context, data *audit.Data, res *omni.Cluster, _ ...state.CreateOption) error { + return handleCluster(data, res) +} + +func clusterUpdate(_ context.Context, data *audit.Data, _, newRes *omni.Cluster, _ ...state.UpdateOption) error { + return handleCluster(data, newRes) +} + +func handleCluster(data *audit.Data, res *omni.Cluster) error { + initPtrField(&data.Cluster) + + data.Cluster.ID = res.Metadata().ID() + data.Cluster.BackupConfiguration = res.TypedSpec().Value.GetBackupConfiguration() + data.Cluster.Features = res.TypedSpec().Value.GetFeatures() + data.Cluster.KubernetesVersion = res.TypedSpec().Value.GetKubernetesVersion() + data.Cluster.TalosVersion = res.TypedSpec().Value.GetTalosVersion() + data.Cluster.Labels = maps.Clone(res.Metadata().Labels().Raw()) + + return nil +} + +func clusterDestroy(_ context.Context, data *audit.Data, ptr resource.Pointer, _ ...state.DestroyOption) error { + initPtrField(&data.Cluster) + + data.Cluster.ID = ptr.ID() + + return nil +} + +func machineSetCreate(_ context.Context, data *audit.Data, res *omni.MachineSet, _ ...state.CreateOption) error { + return handleMachineSet(data, res, true) +} + +func machineSetUpdate(_ context.Context, data *audit.Data, _, newRes *omni.MachineSet, _ ...state.UpdateOption) error { + return handleMachineSet(data, newRes, newRes.Metadata().Owner() == "") +} + +func handleMachineSet(data *audit.Data, res *omni.MachineSet, emptyOwner bool) error { + if !emptyOwner { + return audit.ErrNoLog + } + + initPtrField(&data.MachineSet) + + data.MachineSet.ID = res.Metadata().ID() + data.MachineSet.UpdateStrategy = res.TypedSpec().Value.GetUpdateStrategy().String() + data.MachineSet.MachineClass = res.TypedSpec().Value.GetMachineClass() + data.MachineSet.BootstrapSpec = res.TypedSpec().Value.GetBootstrapSpec() + data.MachineSet.DeleteStrategy = res.TypedSpec().Value.GetDeleteStrategy().String() + data.MachineSet.UpdateStrategyConfig = res.TypedSpec().Value.GetUpdateStrategyConfig() + data.MachineSet.DeleteStrategyConfig = res.TypedSpec().Value.GetDeleteStrategyConfig() + data.MachineSet.Labels = maps.Clone(res.Metadata().Labels().Raw()) + + return nil +} + +func machineSetDestroy(_ context.Context, data *audit.Data, ptr resource.Pointer, _ ...state.DestroyOption) error { + initPtrField(&data.MachineSet) + + data.MachineSet.ID = ptr.ID() + + return nil +} + +func machineSetNodeCreate(_ context.Context, data *audit.Data, res *omni.MachineSetNode, _ ...state.CreateOption) error { + return handleMachineSetNode(data, res, true) +} + +func machineSetNodeUpdate(_ context.Context, data *audit.Data, _, newRes *omni.MachineSetNode, _ ...state.UpdateOption) error { + return handleMachineSetNode(data, newRes, newRes.Metadata().Owner() == "") +} + +func handleMachineSetNode(data *audit.Data, res *omni.MachineSetNode, emptyOwner bool) error { + if !emptyOwner { + return audit.ErrNoLog + } + + initPtrField(&data.MachineSetNode) + + data.MachineSetNode.ID = res.Metadata().ID() + data.MachineSetNode.Labels = maps.Clone(res.Metadata().Labels().Raw()) + + return nil +} + +func machineSetNodeDestroy(_ context.Context, data *audit.Data, ptr resource.Pointer, _ ...state.DestroyOption) error { + initPtrField(&data.MachineSetNode) + + data.MachineSetNode.ID = ptr.ID() + + return nil +} + +func configPatchCreate(_ context.Context, data *audit.Data, res *omni.ConfigPatch, _ ...state.CreateOption) error { + return handleConfigPatch(data, res) +} + +func configPatchUpdate(_ context.Context, data *audit.Data, _, newRes *omni.ConfigPatch, _ ...state.UpdateOption) error { + return handleConfigPatch(data, newRes) +} + +func handleConfigPatch(data *audit.Data, res *omni.ConfigPatch) error { + initPtrField(&data.ConfigPatch) + + data.ConfigPatch.ID = res.Metadata().ID() + data.ConfigPatch.Labels = maps.Clone(res.Metadata().Labels().Raw()) + data.ConfigPatch.Data = res.TypedSpec().Value.GetData() + + return nil +} + +func configPatchDestroy(_ context.Context, data *audit.Data, ptr resource.Pointer, _ ...state.DestroyOption) error { + initPtrField(&data.ConfigPatch) + + data.ConfigPatch.ID = ptr.ID() + + return nil +} + +func initPtrField[T any](v **T) { + if *v == nil { + *v = new(T) + } +} diff --git a/internal/backend/runtime/omni/audit/log.go b/internal/backend/runtime/omni/audit/log.go deleted file mode 100644 index 06d39120..00000000 --- a/internal/backend/runtime/omni/audit/log.go +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) 2024 Sidero Labs, Inc. -// -// Use of this software is governed by the Business Source License -// included in the LICENSE file. - -package audit - -import ( - "context" - "fmt" - "os" - "time" - - "github.com/cosi-project/runtime/pkg/resource" - "github.com/siderolabs/gen/pair" - "go.uber.org/zap" - - "github.com/siderolabs/omni/internal/pkg/ctxstore" -) - -// NewLogger creates a new audit logger. -func NewLogger(auditLogDir string, logger *zap.Logger) (*Logger, error) { - err := os.MkdirAll(auditLogDir, 0o755) - if err != nil { - return nil, fmt.Errorf("failed to create audit logger: %w", err) - } - - return &Logger{ - logFile: NewLogFile(auditLogDir), - logger: logger, - }, nil -} - -// Logger logs audit events. -type Logger struct { - gate Gate - logFile *LogFile - logger *zap.Logger -} - -// LogEvent logs an audit event. -func (l *Logger) LogEvent(ctx context.Context, eventType EventType, resType resource.Type, args ...any) { - if !l.gate.Check(ctx, eventType, resType, args...) { - return - } - - value, ok := ctxstore.Value[*Data](ctx) - if !ok { - return - } - - err := l.logFile.Dump(&event{ - Type: eventType, - ResourceType: resType, - Time: time.Now().UnixMilli(), - Data: value, - }) - if err == nil { - return - } - - l.logger.Error("failed to dump audit log", zap.Error(err)) -} - -// ShoudLog adds checks that allow event type to be logged. -func (l *Logger) ShoudLog(eventType EventType, p ...pair.Pair[resource.Type, Check]) { - l.gate.AddChecks(eventType, p) -} - -//nolint:govet -type event struct { - Type EventType `json:"event_type,omitempty"` - ResourceType resource.Type `json:"resource_type,omitempty"` - Time int64 `json:"event_ts,omitempty"` - Data *Data `json:"event_data,omitempty"` -} diff --git a/internal/backend/runtime/omni/audit/log_file_test.go b/internal/backend/runtime/omni/audit/log_file_test.go index c170054a..b4439c10 100644 --- a/internal/backend/runtime/omni/audit/log_file_test.go +++ b/internal/backend/runtime/omni/audit/log_file_test.go @@ -29,9 +29,9 @@ func TestLogFile_CurrentDay(t *testing.T) { dir := t.TempDir() entries := []entry{ - {shift: time.Second, data: audit.Data{UserAgent: "Mozilla/5.0", IPAddress: "10.10.0.1", Email: "random_email1@example.com"}}, - {shift: time.Minute, data: audit.Data{UserAgent: "Mozilla/5.0", IPAddress: "10.10.0.2", Email: "random_email2@example.com"}}, - {shift: 30 * time.Minute, data: audit.Data{UserAgent: "Mozilla/5.0", IPAddress: "10.10.0.3", Email: "random_email3@example.com"}}, + {shift: time.Second, data: makeAuditData("Mozilla/5.0", "10.10.0.1", "random_email1@example.com")}, + {shift: time.Minute, data: makeAuditData("Mozilla/5.0", "10.10.0.2", "random_email2@example.com")}, + {shift: 30 * time.Minute, data: makeAuditData("Mozilla/5.0", "10.10.0.3", "random_email3@example.com")}, } start := time.Date(2012, 1, 1, 23, 0, 0, 0, time.Local) @@ -59,9 +59,9 @@ func TestLogFile_CurrentAndNewDay(t *testing.T) { dir := t.TempDir() entries := []entry{ - {shift: 0, data: audit.Data{UserAgent: "Mozilla/5.0", IPAddress: "10.10.0.1", Email: "random_email1@example.com"}}, - {shift: 55 * time.Minute, data: audit.Data{UserAgent: "Mozilla/5.0", IPAddress: "10.10.0.2", Email: "random_email2@example.com"}}, - {shift: 5 * time.Minute, data: audit.Data{UserAgent: "Mozilla/5.0", IPAddress: "10.10.0.3", Email: "random_email3@example.com"}}, + {shift: 0, data: makeAuditData("Mozilla/5.0", "10.10.0.1", "random_email1@example.com")}, + {shift: 55 * time.Minute, data: makeAuditData("Mozilla/5.0", "10.10.0.2", "random_email2@example.com")}, + {shift: 5 * time.Minute, data: makeAuditData("Mozilla/5.0", "10.10.0.3", "random_email3@example.com")}, } start := time.Date(2012, 1, 1, 23, 0, 0, 0, time.Local) @@ -94,7 +94,7 @@ func TestLogFile_CurrentDayConcurrent(t *testing.T) { address := fmt.Sprintf("10.10.0.%d", i+1) email := fmt.Sprintf("random_email_%d@example.com", i+1) - entries = append(entries, entry{shift: time.Second, data: audit.Data{UserAgent: "Mozilla/5.0", IPAddress: address, Email: email}}) + entries = append(entries, entry{shift: time.Second, data: makeAuditData("Mozilla/5.0", address, email)}) } start := time.Date(2012, 1, 1, 23, 0, 0, 0, time.Local) @@ -179,3 +179,14 @@ func (s *sortedFileFS) ReadFile(name string) ([]byte, error) { func defaultCmp(t *testing.T, expected string, actual string) { require.Equal(t, expected, actual) } + +//nolint:unparam +func makeAuditData(agent, ip, email string) audit.Data { + return audit.Data{ + Session: audit.Session{ + UserAgent: agent, + IPAddress: ip, + Email: email, + }, + } +} diff --git a/internal/backend/runtime/omni/audit/log_test.go b/internal/backend/runtime/omni/audit/log_test.go deleted file mode 100644 index 9236d95e..00000000 --- a/internal/backend/runtime/omni/audit/log_test.go +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright (c) 2024 Sidero Labs, Inc. -// -// Use of this software is governed by the Business Source License -// included in the LICENSE file. - -package audit_test - -import ( - "context" - "embed" - "encoding/json" - "errors" - "io" - "os" - "strings" - "testing" - - "github.com/cosi-project/runtime/pkg/resource" - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" - "github.com/siderolabs/gen/pair" - "github.com/siderolabs/gen/xtesting/must" - "go.uber.org/zap/zaptest" - - "github.com/siderolabs/omni/client/pkg/omni/resources/auth" - "github.com/siderolabs/omni/internal/backend/runtime/omni/audit" - "github.com/siderolabs/omni/internal/pkg/ctxstore" -) - -//go:embed testdata/log -var logDir embed.FS - -func TestLog(t *testing.T) { - tempDir := t.TempDir() - logger := must.Value(audit.NewLogger(tempDir, zaptest.NewLogger(t)))(t) - - logger.ShoudLog(audit.EventCreate|audit.EventUpdate|audit.EventUpdateWithConflicts, - pair.MakePair(auth.PublicKeyType, audit.AllowAll), - ) - - events := []pair.Triple[audit.EventType, resource.Type, *audit.Data]{ - pair.MakeTriple(audit.EventCreate, auth.PublicKeyType, &audit.Data{UserAgent: "Mozilla/5.0", IPAddress: "10.10.0.1", Email: "random_email1@example.com"}), - pair.MakeTriple(audit.EventUpdate, auth.PublicKeyType, &audit.Data{UserAgent: "Mozilla/5.0", IPAddress: "10.10.0.2", Email: "random_email2@example.com"}), - pair.MakeTriple(audit.EventUpdateWithConflicts, auth.PublicKeyType, &audit.Data{UserAgent: "Mozilla/5.0", IPAddress: "10.10.0.3", Email: "random_email3@example.com"}), - pair.MakeTriple(audit.EventDestroy, auth.PublicKeyType, &audit.Data{UserAgent: "Mozilla/5.0", IPAddress: "10.10.0.4", Email: "random_email4@example.com"}), - pair.MakeTriple(audit.EventCreate, auth.PublicKeyType, (*audit.Data)(nil)), - pair.MakeTriple(audit.EventCreate, auth.AuthConfigType, &audit.Data{UserAgent: "Mozilla/5.0", IPAddress: "10.10.0.5", Email: "random_email5@example.com"}), - } - - for _, event := range events { - ctx := context.Background() - - if event.V3 != nil { - ctx = ctxstore.WithValue(ctx, event.V3) - } - - logger.LogEvent(ctx, event.V1, event.V2, 100) - } - - equalDirs( - t, - &wrapFS{ - subFS: fsSub(t, logDir, "log"), - File: "2012-01-01.jsonlog", - }, - os.DirFS(tempDir).(subFS), //nolint:forcetypeassert - cmpIgnoreTime, - ) -} - -type wrapFS struct { - subFS - File string -} - -func (w *wrapFS) ReadFile(string) ([]byte, error) { - return w.subFS.ReadFile(w.File) -} - -func cmpIgnoreTime(t *testing.T, expected string, actual string) { - expectedEvents := loadEvents(t, expected) - actualEvents := loadEvents(t, actual) - - diff := cmp.Diff(expectedEvents, actualEvents, cmpopts.IgnoreMapEntries(func(k string, v any) bool { - _, ok := v.(json.Number) - - return ok && k == "event_ts" - })) - if diff != "" { - t.Fatalf("events mismatch (-want +got):\n%s", diff) - } -} - -func loadEvents(t *testing.T, expected string) []any { - var result []any - - decoder := json.NewDecoder(strings.NewReader(expected)) - decoder.UseNumber() - - for { - var event any - - err := decoder.Decode(&event) - if err != nil { - if errors.Is(err, io.EOF) { - break - } - - t.Fatalf("failed to decode event: %v", err) - } - - result = append(result, event) - } - - return result -} diff --git a/internal/backend/runtime/omni/audit/state.go b/internal/backend/runtime/omni/audit/state.go index 694ffe23..33b8dd68 100644 --- a/internal/backend/runtime/omni/audit/state.go +++ b/internal/backend/runtime/omni/audit/state.go @@ -13,7 +13,7 @@ import ( ) // WrapState wraps the given state with audit log state. -func WrapState(s state.State, l *Logger) state.State { +func WrapState(s state.State, l *Log) state.State { return &auditState{ state: s, logger: l, @@ -22,66 +22,100 @@ func WrapState(s state.State, l *Logger) state.State { type auditState struct { state state.State - logger *Logger + logger *Log } -func (a *auditState) Get(ctx context.Context, ptr resource.Pointer, option ...state.GetOption) (resource.Resource, error) { - a.logger.LogEvent(ctx, EventGet, ptr.Type(), option) +func (a *auditState) Create(ctx context.Context, res resource.Resource, option ...state.CreateOption) error { + err := a.state.Create(ctx, res, option...) + if err != nil { + return err + } - return a.state.Get(ctx, ptr, option...) + if fn := a.logger.LogCreate(res); fn != nil { + return fn(ctx, res, option...) + } + + return nil } -func (a *auditState) List(ctx context.Context, kind resource.Kind, option ...state.ListOption) (resource.List, error) { - a.logger.LogEvent(ctx, EventList, kind.Type(), option) +func (a *auditState) Update(ctx context.Context, newRes resource.Resource, opts ...state.UpdateOption) error { + fn := a.logger.LogUpdate(newRes) + if fn == nil { + return a.state.Update(ctx, newRes, opts...) + } - return a.state.List(ctx, kind, option...) + oldRes, err := a.state.Get(ctx, newRes.Metadata()) + if err != nil && !state.IsNotFoundError(err) { + return err + } + + err = a.state.Update(ctx, newRes, opts...) + if err != nil { + return err + } + + return fn(ctx, oldRes, newRes, opts...) } -func (a *auditState) Create(ctx context.Context, res resource.Resource, option ...state.CreateOption) error { - a.logger.LogEvent(ctx, EventCreate, res.Metadata().Type(), option) +func (a *auditState) Destroy(ctx context.Context, ptr resource.Pointer, option ...state.DestroyOption) error { + err := a.state.Destroy(ctx, ptr, option...) + if err != nil { + return err + } + + if fn := a.logger.LogDestroy(ptr); fn != nil { + return fn(ctx, ptr, option...) + } - return a.state.Create(ctx, res, option...) + return nil } -func (a *auditState) Update(ctx context.Context, newRes resource.Resource, opts ...state.UpdateOption) error { - a.logger.LogEvent(ctx, EventUpdate, newRes.Metadata().Type(), opts) +func (a *auditState) UpdateWithConflicts(ctx context.Context, ptr resource.Pointer, updaterFunc state.UpdaterFunc, opts ...state.UpdateOption) (resource.Resource, error) { + fn := a.logger.LogUpdateWithConflicts(ptr) + if fn == nil { + return a.state.UpdateWithConflicts(ctx, ptr, updaterFunc, opts...) + } + + var oldRes resource.Resource + + newRes, err := a.state.UpdateWithConflicts( + ctx, + ptr, + func(r resource.Resource) error { + oldRes = r.DeepCopy() + + return updaterFunc(r) + }, + opts..., + ) + if err != nil { + return nil, err + } - return a.state.Update(ctx, newRes, opts...) + return newRes, fn(ctx, oldRes, newRes, opts...) } -func (a *auditState) Destroy(ctx context.Context, ptr resource.Pointer, option ...state.DestroyOption) error { - a.logger.LogEvent(ctx, EventDestroy, ptr.Type(), option) +func (a *auditState) Get(ctx context.Context, ptr resource.Pointer, option ...state.GetOption) (resource.Resource, error) { + return a.state.Get(ctx, ptr, option...) +} - return a.state.Destroy(ctx, ptr, option...) +func (a *auditState) List(ctx context.Context, kind resource.Kind, option ...state.ListOption) (resource.List, error) { + return a.state.List(ctx, kind, option...) } func (a *auditState) Watch(ctx context.Context, ptr resource.Pointer, events chan<- state.Event, option ...state.WatchOption) error { - a.logger.LogEvent(ctx, EventWatch, ptr.Type(), option) - return a.state.Watch(ctx, ptr, events, option...) } func (a *auditState) WatchKind(ctx context.Context, kind resource.Kind, events chan<- state.Event, option ...state.WatchKindOption) error { - a.logger.LogEvent(ctx, EventWatchKind, kind.Type(), option) - return a.state.WatchKind(ctx, kind, events, option...) } func (a *auditState) WatchKindAggregated(ctx context.Context, kind resource.Kind, c chan<- []state.Event, option ...state.WatchKindOption) error { - a.logger.LogEvent(ctx, EventWatchKindAggregated, kind.Type(), option) - return a.state.WatchKindAggregated(ctx, kind, c, option...) } -func (a *auditState) UpdateWithConflicts(ctx context.Context, ptr resource.Pointer, updaterFunc state.UpdaterFunc, option ...state.UpdateOption) (resource.Resource, error) { - a.logger.LogEvent(ctx, EventUpdateWithConflicts, ptr.Type(), option) - - return a.state.UpdateWithConflicts(ctx, ptr, updaterFunc, option...) -} - func (a *auditState) WatchFor(ctx context.Context, pointer resource.Pointer, conditionFunc ...state.WatchForConditionFunc) (resource.Resource, error) { - a.logger.LogEvent(ctx, EventWatchFor, pointer.Type(), conditionFunc) - return a.state.WatchFor(ctx, pointer, conditionFunc...) } diff --git a/internal/backend/runtime/omni/audit/testdata/concurrent/2012-01-01.jsonlog b/internal/backend/runtime/omni/audit/testdata/concurrent/2012-01-01.jsonlog index fcf365b1..af155172 100644 --- a/internal/backend/runtime/omni/audit/testdata/concurrent/2012-01-01.jsonlog +++ b/internal/backend/runtime/omni/audit/testdata/concurrent/2012-01-01.jsonlog @@ -1,250 +1,250 @@ -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.1","email":"random_email_1@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.10","email":"random_email_10@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.100","email":"random_email_100@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.101","email":"random_email_101@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.102","email":"random_email_102@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.103","email":"random_email_103@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.104","email":"random_email_104@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.105","email":"random_email_105@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.106","email":"random_email_106@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.107","email":"random_email_107@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.108","email":"random_email_108@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.109","email":"random_email_109@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.11","email":"random_email_11@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.110","email":"random_email_110@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.111","email":"random_email_111@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.112","email":"random_email_112@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.113","email":"random_email_113@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.114","email":"random_email_114@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.115","email":"random_email_115@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.116","email":"random_email_116@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.117","email":"random_email_117@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.118","email":"random_email_118@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.119","email":"random_email_119@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.12","email":"random_email_12@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.120","email":"random_email_120@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.121","email":"random_email_121@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.122","email":"random_email_122@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.123","email":"random_email_123@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.124","email":"random_email_124@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.125","email":"random_email_125@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.126","email":"random_email_126@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.127","email":"random_email_127@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.128","email":"random_email_128@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.129","email":"random_email_129@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.13","email":"random_email_13@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.130","email":"random_email_130@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.131","email":"random_email_131@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.132","email":"random_email_132@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.133","email":"random_email_133@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.134","email":"random_email_134@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.135","email":"random_email_135@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.136","email":"random_email_136@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.137","email":"random_email_137@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.138","email":"random_email_138@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.139","email":"random_email_139@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.14","email":"random_email_14@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.140","email":"random_email_140@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.141","email":"random_email_141@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.142","email":"random_email_142@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.143","email":"random_email_143@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.144","email":"random_email_144@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.145","email":"random_email_145@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.146","email":"random_email_146@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.147","email":"random_email_147@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.148","email":"random_email_148@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.149","email":"random_email_149@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.15","email":"random_email_15@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.150","email":"random_email_150@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.151","email":"random_email_151@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.152","email":"random_email_152@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.153","email":"random_email_153@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.154","email":"random_email_154@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.155","email":"random_email_155@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.156","email":"random_email_156@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.157","email":"random_email_157@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.158","email":"random_email_158@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.159","email":"random_email_159@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.16","email":"random_email_16@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.160","email":"random_email_160@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.161","email":"random_email_161@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.162","email":"random_email_162@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.163","email":"random_email_163@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.164","email":"random_email_164@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.165","email":"random_email_165@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.166","email":"random_email_166@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.167","email":"random_email_167@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.168","email":"random_email_168@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.169","email":"random_email_169@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.17","email":"random_email_17@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.170","email":"random_email_170@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.171","email":"random_email_171@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.172","email":"random_email_172@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.173","email":"random_email_173@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.174","email":"random_email_174@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.175","email":"random_email_175@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.176","email":"random_email_176@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.177","email":"random_email_177@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.178","email":"random_email_178@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.179","email":"random_email_179@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.18","email":"random_email_18@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.180","email":"random_email_180@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.181","email":"random_email_181@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.182","email":"random_email_182@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.183","email":"random_email_183@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.184","email":"random_email_184@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.185","email":"random_email_185@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.186","email":"random_email_186@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.187","email":"random_email_187@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.188","email":"random_email_188@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.189","email":"random_email_189@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.19","email":"random_email_19@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.190","email":"random_email_190@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.191","email":"random_email_191@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.192","email":"random_email_192@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.193","email":"random_email_193@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.194","email":"random_email_194@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.195","email":"random_email_195@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.196","email":"random_email_196@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.197","email":"random_email_197@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.198","email":"random_email_198@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.199","email":"random_email_199@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.2","email":"random_email_2@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.20","email":"random_email_20@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.200","email":"random_email_200@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.201","email":"random_email_201@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.202","email":"random_email_202@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.203","email":"random_email_203@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.204","email":"random_email_204@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.205","email":"random_email_205@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.206","email":"random_email_206@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.207","email":"random_email_207@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.208","email":"random_email_208@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.209","email":"random_email_209@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.21","email":"random_email_21@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.210","email":"random_email_210@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.211","email":"random_email_211@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.212","email":"random_email_212@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.213","email":"random_email_213@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.214","email":"random_email_214@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.215","email":"random_email_215@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.216","email":"random_email_216@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.217","email":"random_email_217@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.218","email":"random_email_218@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.219","email":"random_email_219@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.22","email":"random_email_22@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.220","email":"random_email_220@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.221","email":"random_email_221@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.222","email":"random_email_222@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.223","email":"random_email_223@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.224","email":"random_email_224@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.225","email":"random_email_225@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.226","email":"random_email_226@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.227","email":"random_email_227@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.228","email":"random_email_228@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.229","email":"random_email_229@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.23","email":"random_email_23@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.230","email":"random_email_230@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.231","email":"random_email_231@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.232","email":"random_email_232@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.233","email":"random_email_233@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.234","email":"random_email_234@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.235","email":"random_email_235@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.236","email":"random_email_236@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.237","email":"random_email_237@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.238","email":"random_email_238@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.239","email":"random_email_239@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.24","email":"random_email_24@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.240","email":"random_email_240@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.241","email":"random_email_241@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.242","email":"random_email_242@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.243","email":"random_email_243@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.244","email":"random_email_244@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.245","email":"random_email_245@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.246","email":"random_email_246@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.247","email":"random_email_247@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.248","email":"random_email_248@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.249","email":"random_email_249@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.25","email":"random_email_25@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.250","email":"random_email_250@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.26","email":"random_email_26@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.27","email":"random_email_27@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.28","email":"random_email_28@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.29","email":"random_email_29@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.3","email":"random_email_3@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.30","email":"random_email_30@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.31","email":"random_email_31@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.32","email":"random_email_32@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.33","email":"random_email_33@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.34","email":"random_email_34@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.35","email":"random_email_35@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.36","email":"random_email_36@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.37","email":"random_email_37@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.38","email":"random_email_38@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.39","email":"random_email_39@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.4","email":"random_email_4@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.40","email":"random_email_40@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.41","email":"random_email_41@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.42","email":"random_email_42@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.43","email":"random_email_43@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.44","email":"random_email_44@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.45","email":"random_email_45@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.46","email":"random_email_46@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.47","email":"random_email_47@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.48","email":"random_email_48@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.49","email":"random_email_49@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.5","email":"random_email_5@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.50","email":"random_email_50@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.51","email":"random_email_51@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.52","email":"random_email_52@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.53","email":"random_email_53@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.54","email":"random_email_54@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.55","email":"random_email_55@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.56","email":"random_email_56@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.57","email":"random_email_57@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.58","email":"random_email_58@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.59","email":"random_email_59@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.6","email":"random_email_6@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.60","email":"random_email_60@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.61","email":"random_email_61@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.62","email":"random_email_62@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.63","email":"random_email_63@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.64","email":"random_email_64@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.65","email":"random_email_65@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.66","email":"random_email_66@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.67","email":"random_email_67@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.68","email":"random_email_68@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.69","email":"random_email_69@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.7","email":"random_email_7@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.70","email":"random_email_70@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.71","email":"random_email_71@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.72","email":"random_email_72@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.73","email":"random_email_73@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.74","email":"random_email_74@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.75","email":"random_email_75@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.76","email":"random_email_76@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.77","email":"random_email_77@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.78","email":"random_email_78@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.79","email":"random_email_79@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.8","email":"random_email_8@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.80","email":"random_email_80@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.81","email":"random_email_81@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.82","email":"random_email_82@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.83","email":"random_email_83@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.84","email":"random_email_84@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.85","email":"random_email_85@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.86","email":"random_email_86@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.87","email":"random_email_87@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.88","email":"random_email_88@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.89","email":"random_email_89@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.9","email":"random_email_9@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.90","email":"random_email_90@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.91","email":"random_email_91@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.92","email":"random_email_92@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.93","email":"random_email_93@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.94","email":"random_email_94@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.95","email":"random_email_95@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.96","email":"random_email_96@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.97","email":"random_email_97@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.98","email":"random_email_98@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.99","email":"random_email_99@example.com"} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.1","email":"random_email_1@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.10","email":"random_email_10@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.100","email":"random_email_100@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.101","email":"random_email_101@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.102","email":"random_email_102@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.103","email":"random_email_103@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.104","email":"random_email_104@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.105","email":"random_email_105@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.106","email":"random_email_106@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.107","email":"random_email_107@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.108","email":"random_email_108@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.109","email":"random_email_109@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.11","email":"random_email_11@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.110","email":"random_email_110@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.111","email":"random_email_111@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.112","email":"random_email_112@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.113","email":"random_email_113@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.114","email":"random_email_114@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.115","email":"random_email_115@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.116","email":"random_email_116@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.117","email":"random_email_117@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.118","email":"random_email_118@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.119","email":"random_email_119@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.12","email":"random_email_12@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.120","email":"random_email_120@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.121","email":"random_email_121@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.122","email":"random_email_122@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.123","email":"random_email_123@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.124","email":"random_email_124@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.125","email":"random_email_125@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.126","email":"random_email_126@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.127","email":"random_email_127@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.128","email":"random_email_128@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.129","email":"random_email_129@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.13","email":"random_email_13@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.130","email":"random_email_130@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.131","email":"random_email_131@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.132","email":"random_email_132@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.133","email":"random_email_133@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.134","email":"random_email_134@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.135","email":"random_email_135@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.136","email":"random_email_136@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.137","email":"random_email_137@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.138","email":"random_email_138@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.139","email":"random_email_139@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.14","email":"random_email_14@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.140","email":"random_email_140@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.141","email":"random_email_141@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.142","email":"random_email_142@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.143","email":"random_email_143@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.144","email":"random_email_144@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.145","email":"random_email_145@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.146","email":"random_email_146@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.147","email":"random_email_147@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.148","email":"random_email_148@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.149","email":"random_email_149@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.15","email":"random_email_15@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.150","email":"random_email_150@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.151","email":"random_email_151@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.152","email":"random_email_152@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.153","email":"random_email_153@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.154","email":"random_email_154@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.155","email":"random_email_155@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.156","email":"random_email_156@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.157","email":"random_email_157@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.158","email":"random_email_158@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.159","email":"random_email_159@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.16","email":"random_email_16@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.160","email":"random_email_160@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.161","email":"random_email_161@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.162","email":"random_email_162@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.163","email":"random_email_163@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.164","email":"random_email_164@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.165","email":"random_email_165@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.166","email":"random_email_166@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.167","email":"random_email_167@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.168","email":"random_email_168@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.169","email":"random_email_169@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.17","email":"random_email_17@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.170","email":"random_email_170@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.171","email":"random_email_171@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.172","email":"random_email_172@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.173","email":"random_email_173@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.174","email":"random_email_174@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.175","email":"random_email_175@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.176","email":"random_email_176@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.177","email":"random_email_177@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.178","email":"random_email_178@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.179","email":"random_email_179@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.18","email":"random_email_18@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.180","email":"random_email_180@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.181","email":"random_email_181@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.182","email":"random_email_182@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.183","email":"random_email_183@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.184","email":"random_email_184@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.185","email":"random_email_185@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.186","email":"random_email_186@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.187","email":"random_email_187@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.188","email":"random_email_188@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.189","email":"random_email_189@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.19","email":"random_email_19@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.190","email":"random_email_190@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.191","email":"random_email_191@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.192","email":"random_email_192@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.193","email":"random_email_193@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.194","email":"random_email_194@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.195","email":"random_email_195@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.196","email":"random_email_196@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.197","email":"random_email_197@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.198","email":"random_email_198@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.199","email":"random_email_199@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.2","email":"random_email_2@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.20","email":"random_email_20@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.200","email":"random_email_200@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.201","email":"random_email_201@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.202","email":"random_email_202@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.203","email":"random_email_203@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.204","email":"random_email_204@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.205","email":"random_email_205@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.206","email":"random_email_206@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.207","email":"random_email_207@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.208","email":"random_email_208@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.209","email":"random_email_209@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.21","email":"random_email_21@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.210","email":"random_email_210@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.211","email":"random_email_211@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.212","email":"random_email_212@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.213","email":"random_email_213@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.214","email":"random_email_214@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.215","email":"random_email_215@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.216","email":"random_email_216@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.217","email":"random_email_217@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.218","email":"random_email_218@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.219","email":"random_email_219@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.22","email":"random_email_22@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.220","email":"random_email_220@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.221","email":"random_email_221@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.222","email":"random_email_222@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.223","email":"random_email_223@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.224","email":"random_email_224@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.225","email":"random_email_225@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.226","email":"random_email_226@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.227","email":"random_email_227@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.228","email":"random_email_228@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.229","email":"random_email_229@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.23","email":"random_email_23@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.230","email":"random_email_230@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.231","email":"random_email_231@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.232","email":"random_email_232@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.233","email":"random_email_233@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.234","email":"random_email_234@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.235","email":"random_email_235@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.236","email":"random_email_236@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.237","email":"random_email_237@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.238","email":"random_email_238@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.239","email":"random_email_239@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.24","email":"random_email_24@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.240","email":"random_email_240@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.241","email":"random_email_241@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.242","email":"random_email_242@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.243","email":"random_email_243@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.244","email":"random_email_244@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.245","email":"random_email_245@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.246","email":"random_email_246@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.247","email":"random_email_247@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.248","email":"random_email_248@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.249","email":"random_email_249@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.25","email":"random_email_25@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.250","email":"random_email_250@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.26","email":"random_email_26@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.27","email":"random_email_27@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.28","email":"random_email_28@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.29","email":"random_email_29@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.3","email":"random_email_3@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.30","email":"random_email_30@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.31","email":"random_email_31@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.32","email":"random_email_32@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.33","email":"random_email_33@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.34","email":"random_email_34@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.35","email":"random_email_35@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.36","email":"random_email_36@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.37","email":"random_email_37@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.38","email":"random_email_38@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.39","email":"random_email_39@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.4","email":"random_email_4@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.40","email":"random_email_40@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.41","email":"random_email_41@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.42","email":"random_email_42@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.43","email":"random_email_43@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.44","email":"random_email_44@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.45","email":"random_email_45@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.46","email":"random_email_46@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.47","email":"random_email_47@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.48","email":"random_email_48@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.49","email":"random_email_49@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.5","email":"random_email_5@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.50","email":"random_email_50@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.51","email":"random_email_51@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.52","email":"random_email_52@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.53","email":"random_email_53@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.54","email":"random_email_54@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.55","email":"random_email_55@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.56","email":"random_email_56@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.57","email":"random_email_57@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.58","email":"random_email_58@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.59","email":"random_email_59@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.6","email":"random_email_6@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.60","email":"random_email_60@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.61","email":"random_email_61@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.62","email":"random_email_62@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.63","email":"random_email_63@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.64","email":"random_email_64@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.65","email":"random_email_65@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.66","email":"random_email_66@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.67","email":"random_email_67@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.68","email":"random_email_68@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.69","email":"random_email_69@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.7","email":"random_email_7@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.70","email":"random_email_70@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.71","email":"random_email_71@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.72","email":"random_email_72@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.73","email":"random_email_73@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.74","email":"random_email_74@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.75","email":"random_email_75@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.76","email":"random_email_76@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.77","email":"random_email_77@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.78","email":"random_email_78@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.79","email":"random_email_79@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.8","email":"random_email_8@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.80","email":"random_email_80@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.81","email":"random_email_81@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.82","email":"random_email_82@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.83","email":"random_email_83@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.84","email":"random_email_84@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.85","email":"random_email_85@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.86","email":"random_email_86@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.87","email":"random_email_87@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.88","email":"random_email_88@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.89","email":"random_email_89@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.9","email":"random_email_9@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.90","email":"random_email_90@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.91","email":"random_email_91@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.92","email":"random_email_92@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.93","email":"random_email_93@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.94","email":"random_email_94@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.95","email":"random_email_95@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.96","email":"random_email_96@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.97","email":"random_email_97@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.98","email":"random_email_98@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.99","email":"random_email_99@example.com"}} diff --git a/internal/backend/runtime/omni/audit/testdata/currentday/2012-01-01.jsonlog b/internal/backend/runtime/omni/audit/testdata/currentday/2012-01-01.jsonlog index 61cf3408..e08dbb5c 100644 --- a/internal/backend/runtime/omni/audit/testdata/currentday/2012-01-01.jsonlog +++ b/internal/backend/runtime/omni/audit/testdata/currentday/2012-01-01.jsonlog @@ -1,3 +1,3 @@ -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.1","email":"random_email1@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.2","email":"random_email2@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.3","email":"random_email3@example.com"} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.1","email":"random_email1@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.2","email":"random_email2@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.3","email":"random_email3@example.com"}} diff --git a/internal/backend/runtime/omni/audit/testdata/log/2012-01-01.jsonlog b/internal/backend/runtime/omni/audit/testdata/log/2012-01-01.jsonlog index c8e7628c..bd4670f1 100644 --- a/internal/backend/runtime/omni/audit/testdata/log/2012-01-01.jsonlog +++ b/internal/backend/runtime/omni/audit/testdata/log/2012-01-01.jsonlog @@ -1,3 +1,5 @@ -{"event_type":"create","resource_type":"PublicKeys.omni.sidero.dev","event_data":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.1","email":"random_email1@example.com"}} -{"event_type":"update","resource_type":"PublicKeys.omni.sidero.dev","event_data":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.2","email":"random_email2@example.com"}} -{"event_type":"update_with_conflicts","resource_type":"PublicKeys.omni.sidero.dev","event_data":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.3","email":"random_email3@example.com"}} +{"event_type":"create","resource_type":"PublicKeys.omni.sidero.dev","event_ts":1723129619182,"event_data":{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.1","user_id":"002cf196-1767-43fd-8e3d-91241e2ce70c","role":"Admin","email":"dmitry.matrenichev@siderolabs.com","fingerprint":"917e47635eb900d0ae66271dd1e06966e048c4f3","public_key_expiration":1325587579}}} +{"event_type":"update","resource_type":"PublicKeys.omni.sidero.dev","event_ts":1723129619182,"event_data":{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.1","user_id":"002cf196-1767-43fd-8e3d-91241e2ce70c","role":"Admin","email":"dmitry.matrenichev@siderolabs.com","fingerprint":"917e47635eb900d0ae66271dd1e06966e048c4f3","public_key_expiration":1325587579}}} +{"event_type":"update_with_conflicts","resource_type":"PublicKeys.omni.sidero.dev","event_ts":1723129619182,"event_data":{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.1","user_id":"002cf196-1767-43fd-8e3d-91241e2ce70c","role":"Admin","email":"dmitry.matrenichev@siderolabs.com","fingerprint":"917e47635eb900d0ae66271dd1e06966e048c4f3","public_key_expiration":1325587579}}} +{"event_type":"destroy","resource_type":"PublicKeys.omni.sidero.dev","event_ts":1723129619182,"event_data":{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.1","fingerprint":"917e47635eb900d0ae66271dd1e06966e048c4f3"}}} +{"event_type":"create","resource_type":"PublicKeys.omni.sidero.dev","event_ts":1723129619182,"event_data":{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.1","user_id":"002cf196-1767-43fd-8e3d-91241e2ce70c","role":"Admin","email":"dmitry.matrenichev@siderolabs.com","fingerprint":"917e47635eb900d0ae66271dd1e06966e048c4f3","public_key_expiration":1325587579}}} diff --git a/internal/backend/runtime/omni/audit/testdata/nextday/2012-01-01.jsonlog b/internal/backend/runtime/omni/audit/testdata/nextday/2012-01-01.jsonlog index c67503c7..37e99a06 100644 --- a/internal/backend/runtime/omni/audit/testdata/nextday/2012-01-01.jsonlog +++ b/internal/backend/runtime/omni/audit/testdata/nextday/2012-01-01.jsonlog @@ -1,2 +1,2 @@ -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.1","email":"random_email1@example.com"} -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.2","email":"random_email2@example.com"} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.1","email":"random_email1@example.com"}} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.2","email":"random_email2@example.com"}} diff --git a/internal/backend/runtime/omni/audit/testdata/nextday/2012-01-02.jsonlog b/internal/backend/runtime/omni/audit/testdata/nextday/2012-01-02.jsonlog index e416028b..4f5f91d7 100644 --- a/internal/backend/runtime/omni/audit/testdata/nextday/2012-01-02.jsonlog +++ b/internal/backend/runtime/omni/audit/testdata/nextday/2012-01-02.jsonlog @@ -1 +1 @@ -{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.3","email":"random_email3@example.com"} +{"session":{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.3","email":"random_email3@example.com"}} diff --git a/internal/backend/runtime/omni/state.go b/internal/backend/runtime/omni/state.go index 3f6d6494..059d67ef 100644 --- a/internal/backend/runtime/omni/state.go +++ b/internal/backend/runtime/omni/state.go @@ -8,6 +8,7 @@ package omni import ( "context" "fmt" + "net/http" "strings" "github.com/cosi-project/runtime/pkg/resource" @@ -17,16 +18,15 @@ import ( "github.com/cosi-project/runtime/pkg/state/impl/namespaced" "github.com/cosi-project/runtime/pkg/state/registry" "github.com/prometheus/client_golang/prometheus" - "github.com/siderolabs/gen/pair" "go.etcd.io/bbolt" "go.uber.org/zap" "github.com/siderolabs/omni/client/pkg/omni/resources" - "github.com/siderolabs/omni/client/pkg/omni/resources/auth" resourceregistry "github.com/siderolabs/omni/client/pkg/omni/resources/registry" "github.com/siderolabs/omni/client/pkg/omni/resources/system" "github.com/siderolabs/omni/internal/backend/logging" "github.com/siderolabs/omni/internal/backend/runtime/omni/audit" + "github.com/siderolabs/omni/internal/backend/runtime/omni/audit/hooks" "github.com/siderolabs/omni/internal/backend/runtime/omni/cloudprovider" "github.com/siderolabs/omni/internal/backend/runtime/omni/controllers/omni/etcdbackup/store" "github.com/siderolabs/omni/internal/backend/runtime/omni/external" @@ -37,149 +37,197 @@ import ( ) // NewState creates a production Omni state. -// -//nolint:cyclop,gocognit func NewState(ctx context.Context, params *config.Params, logger *zap.Logger, metricsRegistry prometheus.Registerer, f func(context.Context, state.State, *virtual.State) error) error { stateFunc := func(ctx context.Context, persistentStateBuilder namespaced.StateBuilder) error { primaryStorageCoreState := persistentStateBuilder(resources.DefaultNamespace) + virtualState := virtual.NewState(state.WrapCore(primaryStorageCoreState)) - secondaryStorageCoreState, secondaryStorageBackingStore, err := newBoltPersistentState( - params.SecondaryStorage.Path, &bbolt.Options{ - NoSync: true, // we do not need fsync for the secondary storage - }, true, logger) + namespacedState, closer, err := newNamespacedState(params, primaryStorageCoreState, virtualState, logger) if err != nil { - return fmt.Errorf("failed to create BoltDB state for secondary storage: %w", err) + return err } - defer secondaryStorageBackingStore.Close() //nolint:errcheck + defer closer() - virtualState := virtual.NewState(state.WrapCore(primaryStorageCoreState)) + measuredState := stateWithMetrics(namespacedState, metricsRegistry) + resourceState := state.WrapCore(measuredState) - storeFactory, err := store.NewStoreFactory() - if err != nil { - return fmt.Errorf("failed to create etcd backup store: %w", err) + if err = initResources(ctx, resourceState, logger); err != nil { + return err } - cloudProviderState := cloudprovider.NewState(primaryStorageCoreState, logger.With(logging.Component("cloudprovider_state"))) - - namespacedState := namespaced.NewState(func(ns resource.Namespace) state.CoreState { - switch ns { - case resources.VirtualNamespace: - return virtualState - case resources.MetricsNamespace: - return secondaryStorageCoreState - case meta.NamespaceName, resources.EphemeralNamespace: - return inmem.NewStateWithOptions(inmem.WithHistoryGap(20))(ns) - case resources.ExternalNamespace: - return &external.State{ - CoreState: primaryStorageCoreState, - StoreFactory: storeFactory, - Logger: logger, - } - case resources.CloudProviderNamespace: - return cloudProviderState - default: - if strings.HasPrefix(ns, resources.CloudProviderSpecificNamespacePrefix) { - return cloudProviderState - } - - return primaryStorageCoreState - } - }) + return f( + ctx, + resourceState, + virtualState, + ) + } - measuredState := wrapStateWithMetrics(namespacedState) + switch params.Storage.Kind { + case "boltdb": + return buildBoltPersistentState(ctx, params.Storage.Boltdb.Path, logger, stateFunc) + case "etcd": + return buildEtcdPersistentState(ctx, params, logger, stateFunc) + default: + return fmt.Errorf("unknown storage kind %q", params.Storage.Kind) + } +} - metricsRegistry.MustRegister(measuredState) +func newNamespacedState(params *config.Params, primaryStorageCoreState state.CoreState, virtualState *virtual.State, logger *zap.Logger) (*namespaced.State, func(), error) { + secondaryStorageCoreState, secondaryStorageBackingStore, err := newBoltPersistentState( + params.SecondaryStorage.Path, &bbolt.Options{ + NoSync: true, // we do not need fsync for the secondary storage + }, true, logger) + if err != nil { + return nil, nil, fmt.Errorf("failed to create BoltDB state for secondary storage: %w", err) + } - resourceState := state.WrapCore(measuredState) + storeFactory, err := store.NewStoreFactory() + if err != nil { + return nil, nil, fmt.Errorf("failed to create etcd backup store: %w", err) + } - namespaceRegistry := registry.NewNamespaceRegistry(resourceState) - resourceRegistry := registry.NewResourceRegistry(resourceState) + cloudProviderState := cloudprovider.NewState(primaryStorageCoreState, logger.With(logging.Component("cloudprovider_state"))) + + namespacedState := namespaced.NewState(func(ns resource.Namespace) state.CoreState { + switch ns { + case resources.VirtualNamespace: + return virtualState + case resources.MetricsNamespace: + return secondaryStorageCoreState + case meta.NamespaceName, resources.EphemeralNamespace: + return inmem.NewStateWithOptions(inmem.WithHistoryGap(20))(ns) + case resources.ExternalNamespace: + return &external.State{ + CoreState: primaryStorageCoreState, + StoreFactory: storeFactory, + Logger: logger, + } + case resources.CloudProviderNamespace: + return cloudProviderState + default: + if strings.HasPrefix(ns, resources.CloudProviderSpecificNamespacePrefix) { + return cloudProviderState + } - if err := namespaceRegistry.RegisterDefault(ctx); err != nil { - return err + return primaryStorageCoreState } + }) - if err := resourceRegistry.RegisterDefault(ctx); err != nil { - return err - } + return namespacedState, func() { + secondaryStorageBackingStore.Close() //nolint:errcheck + }, nil +} - // register Omni namespaces - for _, ns := range []struct { - name string - description string - }{ - {resources.DefaultNamespace, "Default namespace for resources"}, - {resources.EphemeralNamespace, "Ephemeral namespace for resources"}, - {resources.VirtualNamespace, "Namespace for virtual resources"}, - {resources.ExternalNamespace, "Namespace for external resources"}, - {resources.MetricsNamespace, "Secondary storage namespace for resources"}, - } { - if err := namespaceRegistry.Register(ctx, ns.name, ns.description); err != nil { - return err - } - } +func initResources(ctx context.Context, resourceState state.State, logger *zap.Logger) error { + namespaceRegistry := registry.NewNamespaceRegistry(resourceState) + resourceRegistry := registry.NewResourceRegistry(resourceState) - // register Omni resources - for _, r := range resourceregistry.Resources { - if err := resourceRegistry.Register(ctx, r); err != nil { - return err - } - } + if err := namespaceRegistry.RegisterDefault(ctx); err != nil { + return err + } - sysVersion := system.NewSysVersion(resources.EphemeralNamespace, system.SysVersionID) - sysVersion.TypedSpec().Value.BackendVersion = version.Tag - sysVersion.TypedSpec().Value.InstanceName = config.Config.Name - sysVersion.TypedSpec().Value.BackendApiVersion = version.API + if err := resourceRegistry.RegisterDefault(ctx); err != nil { + return err + } - if err := resourceState.Create(ctx, sysVersion); err != nil { + // register Omni namespaces + for _, ns := range []struct { + name string + description string + }{ + {resources.DefaultNamespace, "Default namespace for resources"}, + {resources.EphemeralNamespace, "Ephemeral namespace for resources"}, + {resources.VirtualNamespace, "Namespace for virtual resources"}, + {resources.ExternalNamespace, "Namespace for external resources"}, + {resources.MetricsNamespace, "Secondary storage namespace for resources"}, + } { + if err := namespaceRegistry.Register(ctx, ns.name, ns.description); err != nil { return err } + } - migrationsManager := migration.NewManager(resourceState, logger.With(logging.Component("migration"))) - if err := migrationsManager.Run(ctx); err != nil { + // register Omni resources + for _, r := range resourceregistry.Resources { + if err := resourceRegistry.Register(ctx, r); err != nil { return err } + } - resourceState, fileErr := wrapWithAudit(resourceState, params, logger) - if fileErr != nil { - return fileErr - } + sysVersion := system.NewSysVersion(resources.EphemeralNamespace, system.SysVersionID) + sysVersion.TypedSpec().Value.BackendVersion = version.Tag + sysVersion.TypedSpec().Value.InstanceName = config.Config.Name + sysVersion.TypedSpec().Value.BackendApiVersion = version.API - return f( - ctx, - resourceState, - virtualState, - ) + if err := resourceState.Create(ctx, sysVersion); err != nil { + return err } - switch params.Storage.Kind { - case "boltdb": - return buildBoltPersistentState(ctx, params.Storage.Boltdb.Path, logger, stateFunc) - case "etcd": - return buildEtcdPersistentState(ctx, params, logger, stateFunc) - default: - return fmt.Errorf("unknown storage kind %q", params.Storage.Kind) + if err := migration.NewManager(resourceState, logger.With(logging.Component("migration"))).Run(ctx); err != nil { + return err } + + return nil +} + +func stateWithMetrics(namespacedState *namespaced.State, metricsRegistry prometheus.Registerer) *stateMetrics { + measuredState := wrapStateWithMetrics(namespacedState) + + metricsRegistry.MustRegister(measuredState) + + return measuredState } -func wrapWithAudit(resState state.State, params *config.Params, logger *zap.Logger) (state.State, error) { +// NewAuditWrap creates a new audit wrap. +func NewAuditWrap(resState state.State, params *config.Params, logger *zap.Logger) (*AuditWrap, error) { if params.AuditLogDir == "" { logger.Info("audit log disabled") - return resState, nil + return &AuditWrap{state: resState}, nil } logger.Info("audit log enabled", zap.String("dir", params.AuditLogDir)) - l, err := audit.NewLogger(params.AuditLogDir, logger) + a, err := audit.NewLog(params.AuditLogDir, logger) if err != nil { return nil, err } - l.ShoudLog(audit.EventCreate|audit.EventUpdate|audit.EventUpdateWithConflicts, - pair.MakePair(auth.PublicKeyType, audit.AllowAll), - ) + hooks.Init(a) + + return &AuditWrap{state: resState, log: a, dir: params.AuditLogDir}, nil +} + +// AuditWrap is builder/wrapper for creating logged access to Omni and Talos nodes. +type AuditWrap struct { + state state.State + log *audit.Log + dir string +} + +// Wrap implements [k8sproxy.MiddlewareWrapper]. +func (w *AuditWrap) Wrap(handler http.Handler) http.Handler { + if w.log == nil { + return handler + } + + return w.log.Wrap(handler) +} + +// AuditTalosAccess logs a Talos access event. It does nothing if the audit log is disabled. +func (w *AuditWrap) AuditTalosAccess(ctx context.Context, fullMethodName, clusterID, nodeID string) error { + if w.log == nil { + return nil + } + + return w.log.AuditTalosAccess(ctx, fullMethodName, clusterID, nodeID) +} + +// WrapState wraps the state with audit logging. It does nothing if the audit log is disabled. +func (w *AuditWrap) WrapState(resourceState state.State) state.State { + if w.log == nil { + return resourceState + } - return audit.WrapState(resState, l), nil + return audit.WrapState(resourceState, w.log) } diff --git a/internal/backend/server.go b/internal/backend/server.go index d971055e..2c75726a 100644 --- a/internal/backend/server.go +++ b/internal/backend/server.go @@ -101,6 +101,7 @@ type Server struct { dnsService *dns.Service workloadProxyReconciler *workloadproxy.Reconciler imageFactoryClient *imagefactory.Client + auditor Auditor linkCounterDeltaCh chan<- siderolink.LinkCounterDeltas siderolinkEventsCh chan<- *omnires.MachineStatusSnapshot @@ -128,6 +129,7 @@ func NewServer( authConfig *authres.Config, keyFile, certFile string, proxyServer Proxy, + auditor Auditor, logger *zap.Logger, ) (*Server, error) { s := &Server{ @@ -138,13 +140,14 @@ func NewServer( dnsService: dnsService, workloadProxyReconciler: workloadProxyReconciler, imageFactoryClient: imageFactoryClient, + auditor: auditor, linkCounterDeltaCh: linkCounterDeltaCh, siderolinkEventsCh: siderolinkEventsCh, proxyServer: proxyServer, bindAddress: bindAddress, metricsBindAddress: metricsBindAddress, - k8sProxyBindAddress: k8sProxyBindAddress, pprofBindAddress: pprofBindAddress, + k8sProxyBindAddress: k8sProxyBindAddress, keyFile: keyFile, certFile: certFile, } @@ -235,6 +238,7 @@ func (s *Server) Run(ctx context.Context) error { runtimeState, s.dnsService, authres.Enabled(s.authConfig), + s.auditor, interceptor.NewSignature(s.authenticatorFunc(), s.logger).Unary(), ) if err != nil { @@ -247,6 +251,11 @@ func (s *Server) Run(ctx context.Context) error { grpcProxyServer := router.NewServer(rtr, router.Interceptors(s.logger), + grpc.ChainStreamInterceptor( + grpcutil.StreamSetAuditData(), + // enabled is always true here because we are interested in audit data rather than auth process + interceptor.NewAuthConfig(true, s.logger).Stream(), + ), grpc.MaxRecvMsgSize(constants.GRPCMaxMessageSize), ) crtData := certData{certFile: s.certFile, keyFile: s.keyFile} @@ -264,7 +273,7 @@ func (s *Server) Run(ctx context.Context) error { func() error { return runGRPCServer(ctx, grpcServer, grpcTransport, s.logger) }, func() error { return runMetricsServer(ctx, s.metricsBindAddress, s.logger) }, func() error { - return runK8sProxyServer(ctx, s.k8sProxyBindAddress, oidcStorage, crtData, runtimeState, s.logger) + return runK8sProxyServer(ctx, s.k8sProxyBindAddress, oidcStorage, crtData, runtimeState, s.auditor, s.logger) }, func() error { return s.proxyServer.Run(ctx, unifiedHandler, s.logger) }, func() error { return s.logHandler.Start(ctx) }, @@ -740,8 +749,14 @@ type oidcStore interface { GetPublicKeyByID(keyID string) (any, error) } -func runK8sProxyServer(ctx context.Context, bindAddress string, oidcStorage oidcStore, data certData, - runtimeState state.State, logger *zap.Logger, +func runK8sProxyServer( + ctx context.Context, + bindAddress string, + oidcStorage oidcStore, + data certData, + runtimeState state.State, + wrapper k8sproxy.MiddlewareWrapper, + logger *zap.Logger, ) error { keyFunc := func(_ context.Context, keyID string) (any, error) { return oidcStorage.GetPublicKeyByID(keyID) @@ -758,7 +773,7 @@ func runK8sProxyServer(ctx context.Context, bindAddress string, oidcStorage oidc return uuid.TypedSpec().Value.Uuid, nil } - k8sProxyHandler, err := k8sproxy.NewHandler(keyFunc, clusterUUIDResolver, logger) + k8sProxyHandler, err := k8sproxy.NewHandler(keyFunc, clusterUUIDResolver, wrapper, logger) if err != nil { return err } @@ -1162,3 +1177,9 @@ var assetsData = []struct { "talosctl-windows-amd64.exe", }, } + +// Auditor is a common interface for audit log. +type Auditor interface { + router.TalosAuditor + k8sproxy.MiddlewareWrapper +} diff --git a/internal/pkg/auth/interceptor/auth_config.go b/internal/pkg/auth/interceptor/auth_config.go index 8b1a91c2..e07af0cc 100644 --- a/internal/pkg/auth/interceptor/auth_config.go +++ b/internal/pkg/auth/interceptor/auth_config.go @@ -14,6 +14,7 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/metadata" + "github.com/siderolabs/omni/internal/backend/runtime/omni/audit" "github.com/siderolabs/omni/internal/pkg/auth" "github.com/siderolabs/omni/internal/pkg/ctxstore" ) @@ -65,5 +66,16 @@ func (c *AuthConfig) intercept(ctx context.Context, method string) context.Conte md = metadata.New(nil) } - return ctxstore.WithValue(ctx, auth.GRPCMessageContextKey{Message: message.NewGRPC(md, method)}) + msg := message.NewGRPC(md, method) + + auditData, ok := ctxstore.Value[*audit.Data](ctx) + if ok { + sig, err := msg.Signature() + if err == nil { + auditData.Session.Fingerprint = sig.KeyFingerprint + auditData.Session.Email = sig.Identity + } + } + + return ctxstore.WithValue(ctx, auth.GRPCMessageContextKey{Message: msg}) } diff --git a/internal/pkg/auth/interceptor/jwt.go b/internal/pkg/auth/interceptor/jwt.go index d79e3f9a..f1ab3d8d 100644 --- a/internal/pkg/auth/interceptor/jwt.go +++ b/internal/pkg/auth/interceptor/jwt.go @@ -97,8 +97,8 @@ func (i *JWT) intercept(ctx context.Context) (context.Context, error) { return nil, status.Error(codes.Internal, "missing or invalid audit data") } - auditData.Email = claims.VerifiedEmail - auditData.ConfirmationType = audit.Auth0 + auditData.Session.Email = claims.VerifiedEmail + auditData.Session.ConfirmationType = audit.Auth0 ctx = ctxstore.WithValue(ctx, auth.VerifiedEmailContextKey{Email: claims.VerifiedEmail}) diff --git a/internal/pkg/auth/interceptor/saml.go b/internal/pkg/auth/interceptor/saml.go index 0eed3d7c..c719ba15 100644 --- a/internal/pkg/auth/interceptor/saml.go +++ b/internal/pkg/auth/interceptor/saml.go @@ -92,8 +92,8 @@ func (i *SAML) intercept(ctx context.Context) (context.Context, error) { return nil, status.Error(codes.Internal, "missing or invalid audit data") } - auditData.Email = session.TypedSpec().Value.Email - auditData.ConfirmationType = audit.SAML + auditData.Session.Email = session.TypedSpec().Value.Email + auditData.Session.ConfirmationType = audit.SAML ctx = ctxstore.WithValue(ctx, auth.VerifiedEmailContextKey{Email: session.TypedSpec().Value.Email}) diff --git a/internal/pkg/auth/interceptor/signature.go b/internal/pkg/auth/interceptor/signature.go index 61e5932a..83f11d99 100644 --- a/internal/pkg/auth/interceptor/signature.go +++ b/internal/pkg/auth/interceptor/signature.go @@ -117,8 +117,9 @@ func (i *Signature) intercept(ctx context.Context) (context.Context, error) { return nil, errGRPCInvalidSignature } - auditData.UserID = authenticator.UserID - auditData.Role = authenticator.Role + auditData.Session.Email = authenticator.Identity + auditData.Session.UserID = authenticator.UserID + auditData.Session.Role = authenticator.Role grpc_ctxtags.Extract(ctx). Set("authenticator.user_id", authenticator.UserID). diff --git a/internal/pkg/grpcutil/audit.go b/internal/pkg/grpcutil/audit.go index 242a7c57..80a422f4 100644 --- a/internal/pkg/grpcutil/audit.go +++ b/internal/pkg/grpcutil/audit.go @@ -19,8 +19,10 @@ func SetAuditInCtx(ctx context.Context) context.Context { m := grpc_ctxtags.Extract(ctx).Values() return ctxstore.WithValue(ctx, &audit.Data{ - UserAgent: typeAssertOrZero[string](m["user_agent"]), - IPAddress: typeAssertOrZero[string](m["peer.address"]), + Session: audit.Session{ + UserAgent: typeAssertOrZero[string](m["user_agent"]), + IPAddress: typeAssertOrZero[string](m["peer.address"]), + }, }) }