From 0f6fca992822d3a5d75b189f6841a13015366287 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 4 Dec 2024 13:21:22 +0000 Subject: [PATCH] Renovate: Update github.com/sapcc/go-bits digest to aa06a1b --- go.mod | 2 +- go.sum | 4 +- .../sapcc/go-bits/audittools/auditor.go | 256 ++++++++++++++++++ .../sapcc/go-bits/audittools/doc.go | 70 +---- .../sapcc/go-bits/audittools/event.go | 28 +- .../sapcc/go-bits/audittools/rabbitmq.go | 20 +- .../sapcc/go-bits/audittools/trail.go | 14 +- vendor/modules.txt | 2 +- 8 files changed, 299 insertions(+), 97 deletions(-) create mode 100644 vendor/github.com/sapcc/go-bits/audittools/auditor.go diff --git a/go.mod b/go.mod index ae2118d7e..3531c6efd 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,7 @@ require ( github.com/redis/go-redis/v9 v9.7.0 github.com/rs/cors v1.11.1 github.com/sapcc/go-api-declarations v1.13.0 - github.com/sapcc/go-bits v0.0.0-20241128180218-03123d6bae9b + github.com/sapcc/go-bits v0.0.0-20241204103411-aa06a1b92800 github.com/spf13/cobra v1.8.1 github.com/timewasted/go-accept-headers v0.0.0-20130320203746-c78f304b1b09 go.uber.org/automaxprocs v1.6.0 diff --git a/go.sum b/go.sum index 6e81b674f..1d7bb2bbc 100644 --- a/go.sum +++ b/go.sum @@ -178,8 +178,8 @@ github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc= github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= github.com/sapcc/go-api-declarations v1.13.0 h1:4ufQUF7rwhLz7kPDVFCkw6CpQ8VeO2clJg4pjwTTpTU= github.com/sapcc/go-api-declarations v1.13.0/go.mod h1:83R3hTANhuRXt/pXDby37IJetw8l7DG41s33Tp9NXxI= -github.com/sapcc/go-bits v0.0.0-20241128180218-03123d6bae9b h1:VNSYY/wn26woM2RbEex+K+4qx7mtniH5tLjqXzGb+2o= -github.com/sapcc/go-bits v0.0.0-20241128180218-03123d6bae9b/go.mod h1:Fa38gpJczKo2cgyb21f2RkrvqvaFtLXztXyYgTZ7ZVE= +github.com/sapcc/go-bits v0.0.0-20241204103411-aa06a1b92800 h1:6nwYrzWL5mB8kg5A29nQLLg+cx4/onf2BPhHzCVEohA= +github.com/sapcc/go-bits v0.0.0-20241204103411-aa06a1b92800/go.mod h1:Fa38gpJczKo2cgyb21f2RkrvqvaFtLXztXyYgTZ7ZVE= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= diff --git a/vendor/github.com/sapcc/go-bits/audittools/auditor.go b/vendor/github.com/sapcc/go-bits/audittools/auditor.go new file mode 100644 index 000000000..2137189a8 --- /dev/null +++ b/vendor/github.com/sapcc/go-bits/audittools/auditor.go @@ -0,0 +1,256 @@ +/******************************************************************************* +* +* Copyright 2024 SAP SE +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You should have received a copy of the License along with this +* program. If not, you may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +*******************************************************************************/ + +package audittools + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net" + "net/url" + "strconv" + "testing" + + "github.com/prometheus/client_golang/prometheus" + "github.com/sapcc/go-api-declarations/cadf" + + "github.com/sapcc/go-bits/assert" + "github.com/sapcc/go-bits/logg" + "github.com/sapcc/go-bits/osext" +) + +// Auditor is a high-level interface for audit event acceptors. +// In a real process, use NewAuditor() or NewNullAuditor() depending on whether you have RabbitMQ client credentials. +// In a test scenario, use NewMockAuditor() to get an assertable mock implementation. +type Auditor interface { + Record(EventParameters) +} + +//////////////////////////////////////////////////////////////////////////////// +// type standardAuditor + +// AuditorOpts contains options for NewAuditor(). +type AuditorOpts struct { + // Required. Identifies the current process. + // The Observer.ID field should be set to a UUID, such as those generated by GenerateUUID(). + // + // When recording events, the EventParameters.Observer field does not need to be filled by the caller. + // It will instead be filled with this Observer. + Observer Observer + + // Optional. If given, RabbitMQ connection options will be read from the following environment variables: + // - "${PREFIX}_HOSTNAME" (defaults to "localhost") + // - "${PREFIX}_PORT" (defaults to "5672") + // - "${PREFIX}_USERNAME" (defaults to "guest") + // - "${PREFIX}_PASSWORD" (defaults to "guest") + // - "${PREFIX}_QUEUE_NAME" (required) + EnvPrefix string + + // Required if EnvPrefix is empty, ignored otherwise. + // Contains the RabbitMQ connection options that would otherwise be read from environment variables. + ConnectionURL string + QueueName string + + // Optional. If given, the Auditor will register its Prometheus metrics with this registry instead of the default registry. + // The following metrics are registered: + // - "audittools_successful_submissions" (counter, no labels) + // - "audittools_failed_submissions" (counter, no labels) + Registry prometheus.Registerer +} + +func (opts AuditorOpts) getConnectionOptions() (rabbitURL url.URL, queueName string, err error) { + // option 1: passed explicitly + if opts.EnvPrefix == "" { + if opts.ConnectionURL == "" { + return url.URL{}, "", errors.New("missing required value: AuditorOpts.ConnectionURL") + } + if opts.QueueName == "" { + return url.URL{}, "", errors.New("missing required value: AuditorOpts.QueueName") + } + rabbitURL, err := url.Parse(opts.ConnectionURL) + if err != nil { + return url.URL{}, "", fmt.Errorf("while parsing AuditorOpts.ConnectionURL (%q): %w", opts.ConnectionURL, err) + } + return *rabbitURL, opts.QueueName, nil + } + + // option 2: passed via environment variables + queueName, err = osext.NeedGetenv(opts.EnvPrefix + "_QUEUE_NAME") + if err != nil { + return url.URL{}, "", err + } + hostname := osext.GetenvOrDefault(opts.EnvPrefix+"_HOSTNAME", "localhost") + port, err := strconv.Atoi(osext.GetenvOrDefault(opts.EnvPrefix+"_PORT", "5672")) + if err != nil { + return url.URL{}, "", fmt.Errorf("invalid value for %s_PORT: %w", opts.EnvPrefix, err) + } + username := osext.GetenvOrDefault(opts.EnvPrefix+"_USERNAME", "guest") + pass := osext.GetenvOrDefault(opts.EnvPrefix+"_PASSWORD", "guest") + rabbitURL = url.URL{ + Scheme: "amqp", + Host: net.JoinHostPort(hostname, strconv.Itoa(port)), + User: url.UserPassword(username, pass), + Path: "/", + } + return rabbitURL, queueName, nil +} + +type standardAuditor struct { + Observer Observer + EventSink chan<- cadf.Event +} + +// NewAuditor builds an Auditor connected to a RabbitMQ instance, using the provided configuration. +// This is the recommended high-level constructor for an audit event receiver. +// The more low-level type AuditTrail should be used instead only if absolutely necessary. +func NewAuditor(ctx context.Context, opts AuditorOpts) (Auditor, error) { + // validate provided options + if opts.Observer.TypeURI == "" { + return nil, errors.New("missing required value: AuditorOpts.Observer.TypeURI") + } + if opts.Observer.Name == "" { + return nil, errors.New("missing required value: AuditorOpts.Observer.Name") + } + if opts.Observer.ID == "" { + return nil, errors.New("missing required value: AuditorOpts.Observer.ID") + } + if opts.EnvPrefix == "" { + return nil, errors.New("missing required value: AuditorOpts.EnvPrefix") + } + + // register Prometheus metrics + successCounter := prometheus.NewCounter(prometheus.CounterOpts{ + Name: "audittools_successful_submissions", + Help: "Counter for successful audit event submissions to the Hermes RabbitMQ server.", + }) + failureCounter := prometheus.NewCounter(prometheus.CounterOpts{ + Name: "audittools_failed_submissions", + Help: "Counter for failed (but retryable) audit event submissions to the Hermes RabbitMQ server.", + }) + successCounter.Add(0) + failureCounter.Add(0) + if opts.Registry == nil { + prometheus.MustRegister(successCounter) + prometheus.MustRegister(failureCounter) + } else { + opts.Registry.MustRegister(successCounter) + opts.Registry.MustRegister(failureCounter) + } + + // spawn event delivery goroutine + rabbitURL, queueName, err := opts.getConnectionOptions() + if err != nil { + return nil, err + } + eventChan := make(chan cadf.Event, 20) + go AuditTrail{ + EventSink: eventChan, + OnSuccessfulPublish: func() { successCounter.Inc() }, + OnFailedPublish: func() { failureCounter.Inc() }, + }.Commit(ctx, rabbitURL, queueName) + + return &standardAuditor{ + Observer: opts.Observer, + EventSink: eventChan, + }, nil +} + +// Record implements the Auditor interface. +func (a *standardAuditor) Record(params EventParameters) { + params.Observer = a.Observer + a.EventSink <- NewEvent(params) +} + +//////////////////////////////////////////////////////////////////////////////// +// type nullAuditor + +// NewNullAuditor returns an Auditor that does nothing (except produce a debug log of the discarded event). +// This is only intended to be used for non-productive deployments without a Hermes instance. +func NewNullAuditor() Auditor { + return nullAuditor{} +} + +type nullAuditor struct{} + +// Record implements the Auditor interface. +func (nullAuditor) Record(params EventParameters) { + if logg.ShowDebug { + msg, err := json.Marshal(NewEvent(params)) + if err == nil { + logg.Debug("audit event received: %s", string(msg)) + } + } +} + +//////////////////////////////////////////////////////////////////////////////// +// type MockAuditor + +// MockAuditor is a test recorder that satisfies the Auditor interface. +type MockAuditor struct { + events []cadf.Event +} + +// NewMockAuditor constructs a new MockAuditor instance. +func NewMockAuditor() *MockAuditor { + return &MockAuditor{} +} + +// Record implements the Auditor interface. +func (a *MockAuditor) Record(params EventParameters) { + a.events = append(a.events, a.normalize(NewEvent(params))) +} + +// ExpectEvents checks that the recorded events are equivalent to the supplied expectation. +// At the end of the call, the recording will be disposed, so the next ExpectEvents call will not check against the same events again. +func (a *MockAuditor) ExpectEvents(t *testing.T, expectedEvents ...cadf.Event) { + t.Helper() + if len(expectedEvents) == 0 { + expectedEvents = nil + } else { + for idx, event := range expectedEvents { + expectedEvents[idx] = a.normalize(event) + } + } + assert.DeepEqual(t, "CADF events", a.events, expectedEvents) + + // reset state for next test + a.events = nil +} + +// IgnoreEventsUntilNow clears the list of recorded events, so that the next +// ExpectEvents() will only cover events generated after this point. +func (a *MockAuditor) IgnoreEventsUntilNow() { + a.events = nil +} + +func (a *MockAuditor) normalize(event cadf.Event) cadf.Event { + // overwrite some attributes where we don't care about variance + event.TypeURI = "http://schemas.dmtf.org/cloud/audit/1.0/event" + event.ID = "00000000-0000-0000-0000-000000000000" + event.EventTime = "2006-01-02T15:04:05.999999+00:00" + event.EventType = "activity" + if event.Initiator.TypeURI == standardUserInfoTypeURI { + // we do not care about the Initiator unless it's a NonStandardUserInfo + event.Initiator = cadf.Resource{} + } + event.Observer = cadf.Resource{} + return event +} diff --git a/vendor/github.com/sapcc/go-bits/audittools/doc.go b/vendor/github.com/sapcc/go-bits/audittools/doc.go index 3168ff9b8..28c6d5e75 100644 --- a/vendor/github.com/sapcc/go-bits/audittools/doc.go +++ b/vendor/github.com/sapcc/go-bits/audittools/doc.go @@ -18,71 +18,11 @@ *******************************************************************************/ /* -Package audittools provides helper functions for establishing a connection to -a RabbitMQ server (with sane defaults) and publishing messages to it. +Package audittools provides a microframework for establishing a connection to +a RabbitMQ server (with sane defaults) and publishing audit messages in the CADF format to it. -It comes with a ready-to-use implementation that can be used to publish the audit trail -of an application to a RabbitMQ server, or it can be used as a reference to build your -own. - -One usage of the aforementioned implementation can be: - - package yourPackageName - - import ( - "net/url" - ... - - "github.com/sapcc/go-bits/audittools" - ... - ) - - var eventPublishSuccessCounter = prometheus.NewCounter( - prometheus.CounterOpts{ - Name: "yourApplication_successful_auditevent_publish", - Help: "Counter for successful audit event publish to RabbitMQ server.", - }, - ) - var eventPublishFailedCounter = prometheus.NewCounter( - prometheus.CounterOpts{ - Name: "yourApplication_failed_auditevent_publish", - Help: "Counter for failed audit event publish to RabbitMQ server.", - }, - ) - - var EventSink chan<- cadf.Event - - func init() { - s := make(chan cadf.Event, 20) - EventSink = s - - onSuccessFunc := func() { - eventPublishSuccessCounter.Inc() - } - onFailFunc() := func() { - eventPublishFailedCounter.Inc() - } - - rabbitmqQueueName := "down-the-rabbit-hole" - rabbitmqURI := url.URL{ - Scheme: "amqp", - Host: net.JoinHostPort("localhost", "5672"), - User: url.UserPassword("guest", "guest"), - Path: "/", - } - - go audittools.AuditTrail{ - EventSink: s, - OnSuccessfulPublish: onSuccessFunc, - OnFailedPublish: onFailFunc, - }.Commit(rabbitmqURI.String(), rabbitmqQueueName) - } - - func someFunction() { - event := generateCADFEvent() - if EventSink != nil { - EventSink <- event - } - } +To use it, build an AuditTrail object and spawn its Commit() event loop at initialization time. +Then push events into it as part of your request handlers. +Check the example on type AuditTrail for details. */ package audittools diff --git a/vendor/github.com/sapcc/go-bits/audittools/event.go b/vendor/github.com/sapcc/go-bits/audittools/event.go index 1b542e5f6..fec7bdbc4 100644 --- a/vendor/github.com/sapcc/go-bits/audittools/event.go +++ b/vendor/github.com/sapcc/go-bits/audittools/event.go @@ -28,7 +28,7 @@ import ( "github.com/sapcc/go-api-declarations/cadf" "github.com/sapcc/go-bits/httpext" - "github.com/sapcc/go-bits/logg" + "github.com/sapcc/go-bits/must" ) // TargetRenderer is the interface that different event types "must" implement @@ -37,6 +37,14 @@ type TargetRenderer interface { Render() cadf.Resource } +// Observer is like cadf.Resource, but contains only the fields that need to be +// set for an event observer. +type Observer struct { + TypeURI string + Name string + ID string +} + // UserInfo is implemented by types that describe a user who is taking an // action on an OpenStack API. The most important implementor of this interface // is *gopherpolicy.Token. @@ -77,14 +85,12 @@ type EventParameters struct { // It is recommended to use a constant from: https://golang.org/pkg/net/http/#pkg-constants ReasonCode int Action cadf.Action - Observer struct { - TypeURI string - Name string - ID string - } - Target TargetRenderer + Observer Observer + Target TargetRenderer } +const standardUserInfoTypeURI = "service/security/account/user" + // NewEvent uses EventParameters to generate an audit event. // Warning: this function uses GenerateUUID() to generate the Event.ID, if that fails // then the concerning error will be logged and it will result in program termination. @@ -99,7 +105,7 @@ func NewEvent(p EventParameters) cadf.Event { initiator = u.AsInitiator() } else { initiator = cadf.Resource{ - TypeURI: "service/security/account/user", + TypeURI: standardUserInfoTypeURI, // information about user Name: p.User.UserName(), Domain: p.User.UserDomainName(), @@ -143,9 +149,5 @@ func NewEvent(p EventParameters) cadf.Event { // GenerateUUID generates an UUID based on random numbers (RFC 4122). // Failure will result in program termination. func GenerateUUID() string { - u, err := uuid.NewV4() - if err != nil { - logg.Fatal(err.Error()) - } - return u.String() + return must.Return(uuid.NewV4()).String() } diff --git a/vendor/github.com/sapcc/go-bits/audittools/rabbitmq.go b/vendor/github.com/sapcc/go-bits/audittools/rabbitmq.go index ec430e9ef..74e355ad2 100644 --- a/vendor/github.com/sapcc/go-bits/audittools/rabbitmq.go +++ b/vendor/github.com/sapcc/go-bits/audittools/rabbitmq.go @@ -31,9 +31,9 @@ import ( "github.com/sapcc/go-api-declarations/cadf" ) -// RabbitConnection represents a unique connection to some RabbitMQ server with +// rabbitConnection represents a unique connection to some RabbitMQ server with // an open Channel and a declared Queue. -type RabbitConnection struct { +type rabbitConnection struct { Inner *amqp.Connection Channel *amqp.Channel QueueName string @@ -41,9 +41,9 @@ type RabbitConnection struct { LastConnectedAt time.Time } -// NewRabbitConnection returns a new RabbitConnection using the specified amqp URI +// newRabbitConnection returns a new rabbitConnection using the specified amqp URI // and queue name. -func NewRabbitConnection(uri url.URL, queueName string) (*RabbitConnection, error) { +func newRabbitConnection(uri url.URL, queueName string) (*rabbitConnection, error) { // establish a connection with the RabbitMQ server conn, err := amqp.Dial(uri.String()) if err != nil { @@ -69,7 +69,7 @@ func NewRabbitConnection(uri url.URL, queueName string) (*RabbitConnection, erro return nil, fmt.Errorf("audittools: rabbitmq: failed to declare a queue: %w", err) } - return &RabbitConnection{ + return &rabbitConnection{ Inner: conn, Channel: ch, QueueName: queueName, @@ -77,21 +77,21 @@ func NewRabbitConnection(uri url.URL, queueName string) (*RabbitConnection, erro }, nil } -// Disconnect is a helper function for closing a RabbitConnection. -func (c *RabbitConnection) Disconnect() { +// Disconnect is a helper function for closing a rabbitConnection. +func (c *rabbitConnection) Disconnect() { c.Channel.Close() c.Inner.Close() } // IsNilOrClosed is like (*amqp.Connection).IsClosed() but it also returns true -// if RabbitConnection or the underlying amqp.Connection are nil. -func (c *RabbitConnection) IsNilOrClosed() bool { +// if rabbitConnection or the underlying amqp.Connection are nil. +func (c *rabbitConnection) IsNilOrClosed() bool { return c == nil || c.Inner == nil || c.Inner.IsClosed() } // PublishEvent publishes a cadf.Event to a specific RabbitMQ Connection. // A nil pointer for event parameter will return an error. -func (c *RabbitConnection) PublishEvent(ctx context.Context, event *cadf.Event) error { +func (c *rabbitConnection) PublishEvent(ctx context.Context, event *cadf.Event) error { if c.IsNilOrClosed() { return amqp.ErrClosed } diff --git a/vendor/github.com/sapcc/go-bits/audittools/trail.go b/vendor/github.com/sapcc/go-bits/audittools/trail.go index b46c9448f..6585c81f5 100644 --- a/vendor/github.com/sapcc/go-bits/audittools/trail.go +++ b/vendor/github.com/sapcc/go-bits/audittools/trail.go @@ -31,6 +31,9 @@ import ( // AuditTrail holds an event sink for receiving audit events and closure functions // that are executed in case of successful and failed publishing. +// +// This is a low-level interface. New applications should use func NewAuditor +// unless the Auditor interface is too opinionated for them. type AuditTrail struct { EventSink <-chan cadf.Event OnSuccessfulPublish func() @@ -39,10 +42,11 @@ type AuditTrail struct { // Commit takes a AuditTrail that receives audit events from an event sink and publishes them to // a specific RabbitMQ Connection using the specified amqp URI and queue name. -// The OnSuccessfulPublish and OnFailedPublish closures are executed as per -// their respective case. +// The OnSuccessfulPublish and OnFailedPublish closures are executed as per their respective case. +// +// This function blocks the current goroutine forever. It should be invoked with the "go" keyword. func (t AuditTrail) Commit(ctx context.Context, rabbitmqURI url.URL, rabbitmqQueueName string) { - rc, err := NewRabbitConnection(rabbitmqURI, rabbitmqQueueName) + rc, err := newRabbitConnection(rabbitmqURI, rabbitmqQueueName) if err != nil { logg.Error(err.Error()) } @@ -92,7 +96,7 @@ func (t AuditTrail) Commit(ctx context.Context, rabbitmqURI url.URL, rabbitmqQue } } -func refreshConnectionIfClosedOrOld(rc *RabbitConnection, uri url.URL, queueName string) *RabbitConnection { +func refreshConnectionIfClosedOrOld(rc *rabbitConnection, uri url.URL, queueName string) *rabbitConnection { if !rc.IsNilOrClosed() { if time.Since(rc.LastConnectedAt) < 5*time.Minute { return rc @@ -100,7 +104,7 @@ func refreshConnectionIfClosedOrOld(rc *RabbitConnection, uri url.URL, queueName rc.Disconnect() } - connection, err := NewRabbitConnection(uri, queueName) + connection, err := newRabbitConnection(uri, queueName) if err != nil { logg.Error(err.Error()) return nil diff --git a/vendor/modules.txt b/vendor/modules.txt index 28f80fd8f..ecae8b1a5 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -181,7 +181,7 @@ github.com/samber/lo/internal/rand github.com/sapcc/go-api-declarations/bininfo github.com/sapcc/go-api-declarations/cadf github.com/sapcc/go-api-declarations/liquid -# github.com/sapcc/go-bits v0.0.0-20241128180218-03123d6bae9b +# github.com/sapcc/go-bits v0.0.0-20241204103411-aa06a1b92800 ## explicit; go 1.23 github.com/sapcc/go-bits/assert github.com/sapcc/go-bits/audittools