audit-go/audit/api/builder.go
Christian Schaible f8f0b48437 Add event builder
2024-09-30 13:08:43 +02:00

584 lines
20 KiB
Go

package api
import (
"context"
"dev.azure.com/schwarzit/schwarzit.stackit-core-platform/audit-go.git/audit/utils"
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-core-platform/audit-go.git/gen/go/audit/v1"
"errors"
"fmt"
"go.opentelemetry.io/otel/trace"
"log/slog"
"time"
)
type SequenceNumber uint64
type AuditParameters struct {
// A map that is added as "details" to the message
Details map[string]interface{}
// The type of the event
EventType EventType
// A set of user-defined (key, value) data that provides additional
// information about the log entry.
Labels map[string]string
// UUID identifier of the object, the audit event refers to
ObjectId string
// Type of the object, the audit event refers to
ObjectType SingularType
// Log severity
Severity auditV1.LogSeverity
}
func getObjectIdAndTypeFromAuditParams(
ctx context.Context,
auditParams *AuditParameters,
) (string, *SingularType, *PluralType, error) {
objectId := auditParams.ObjectId
if objectId == "" {
return "", nil, nil, errors.New("object id missing")
}
var objectType *SingularType
if auditParams.ObjectType != "" {
objectType = &auditParams.ObjectType
}
if objectType == nil {
return "", nil, nil, errors.New("singular type missing")
}
// Convert to plural type
plural, err := objectType.AsPluralType()
if err != nil {
slog.LogAttrs(ctx, slog.LevelError, "failed to convert singular type to plural type", slog.Any("error", err))
return "", nil, nil, err
}
return objectId, objectType, &plural, nil
}
// AuditLogEntryBuilder collects audit params to construct auditV1.AuditLogEntry
type AuditLogEntryBuilder struct {
auditParams AuditParameters
auditRequest AuditRequest
auditResponse AuditResponse
auditMetadata AuditMetadata
// Region and optional zone id. If both, separated with a - (dash).
// Example: eu01
location string
// The ID of the K8s Pod, Service-Instance, etc (must be unique for a sending service)
workerId string
}
// NewAuditLogEntryBuilder returns a builder to construct auditV1.AuditLogEntry
func NewAuditLogEntryBuilder() *AuditLogEntryBuilder {
requestTime := time.Now().UTC()
return &AuditLogEntryBuilder{
auditParams: AuditParameters{
EventType: EventTypeAdminActivity,
},
auditRequest: AuditRequest{
Request: nil,
RequestClientIP: "0.0.0.0",
RequestCorrelationId: nil,
RequestId: nil,
RequestTime: &requestTime,
},
auditResponse: AuditResponse{
ResponseBodyBytes: nil,
ResponseStatusCode: 200,
ResponseHeaders: make(map[string][]string),
ResponseNumItems: nil,
ResponseTime: nil,
},
auditMetadata: AuditMetadata{
AuditInsertId: "",
AuditLabels: nil,
AuditLogName: "",
AuditLogSeverity: auditV1.LogSeverity_LOG_SEVERITY_DEFAULT,
AuditOperationName: "",
AuditPermission: nil,
AuditPermissionGranted: nil,
AuditResourceName: "",
AuditServiceName: "",
AuditTime: nil,
},
location: "",
workerId: "",
}
}
// WithRequiredApiRequest adds api request details
func (builder *AuditLogEntryBuilder) WithRequiredApiRequest(request ApiRequest) *AuditLogEntryBuilder {
builder.auditRequest.Request = &request
return builder
}
// WithRequiredLocation adds the region and optional zone id. If both, separated with a - (dash).
// Example: eu01
func (builder *AuditLogEntryBuilder) WithRequiredLocation(location string) *AuditLogEntryBuilder {
builder.location = location
return builder
}
// WithRequiredRequestClientIp adds the client ip
func (builder *AuditLogEntryBuilder) WithRequiredRequestClientIp(requestClientIp string) *AuditLogEntryBuilder {
builder.auditRequest.RequestClientIP = requestClientIp
return builder
}
// WithRequestCorrelationId adds an optional request correlation id
func (builder *AuditLogEntryBuilder) WithRequestCorrelationId(requestCorrelationId string) *AuditLogEntryBuilder {
builder.auditRequest.RequestCorrelationId = &requestCorrelationId
return builder
}
// WithRequestId adds an optional request id
func (builder *AuditLogEntryBuilder) WithRequestId(requestId string) *AuditLogEntryBuilder {
builder.auditRequest.RequestId = &requestId
return builder
}
// WithRequestTime sets the request time on the builder. If not set - the instantiation time of the builder is used.
func (builder *AuditLogEntryBuilder) WithRequestTime(requestTime time.Time) *AuditLogEntryBuilder {
builder.auditRequest.RequestTime = &requestTime
return builder
}
// WithRequiredServiceName adds the service name in lowercase (allowed characters are [a-z-]).
func (builder *AuditLogEntryBuilder) WithRequiredServiceName(serviceName string) *AuditLogEntryBuilder {
builder.auditMetadata.AuditServiceName = serviceName
return builder
}
// WithRequiredWorkerId adds the ID of the K8s Pod, Service-Instance, etc. (must be unique for a sending service)
func (builder *AuditLogEntryBuilder) WithRequiredWorkerId(workerId string) *AuditLogEntryBuilder {
builder.workerId = workerId
return builder
}
// WithRequiredObjectId adds the object identifier.
// May be prefilled by audit middleware (if the identifier can be extracted from the url path).
func (builder *AuditLogEntryBuilder) WithRequiredObjectId(objectId string) *AuditLogEntryBuilder {
builder.auditParams.ObjectId = objectId
return builder
}
// WithRequiredObjectType adds the object type.
// May be prefilled by audit middleware (if the type can be extracted from the url path).
func (builder *AuditLogEntryBuilder) WithRequiredObjectType(objectType SingularType) *AuditLogEntryBuilder {
builder.auditParams.ObjectType = objectType
return builder
}
// WithRequiredOperation adds the name of the service method or operation.
//
// Format: stackit.<product>.<version>.<type-chain>.<operation>
// Where:
//
// Product: The name of the service in lowercase
// Version: Optional API version
// Type-Chain: Chained path to object
// Operation: The name of the operation in lowercase
//
// Examples:
//
// "stackit.resource-manager.v1.organizations.create"
// "stackit.authorization.v1.projects.volumes.create"
// "stackit.authorization.v2alpha.projects.volumes.create"
// "stackit.authorization.v2.folders.move"
// "stackit.resource-manager.health"
func (builder *AuditLogEntryBuilder) WithRequiredOperation(operation string) *AuditLogEntryBuilder {
builder.auditMetadata.AuditOperationName = operation
return builder
}
// WithAuditPermission adds the IAM permission
//
// Examples:
//
// "resourcemanager.project.edit"
func (builder *AuditLogEntryBuilder) WithAuditPermission(permission string) *AuditLogEntryBuilder {
builder.auditMetadata.AuditPermission = &permission
return builder
}
// WithAuditPermissionCheckResult adds the IAM permission check result
func (builder *AuditLogEntryBuilder) WithAuditPermissionCheckResult(permissionCheckResult bool) *AuditLogEntryBuilder {
builder.auditMetadata.AuditPermissionGranted = &permissionCheckResult
return builder
}
// WithLabels adds A set of user-defined (key, value) data that provides additional
// information about the log entry.
func (builder *AuditLogEntryBuilder) WithLabels(labels map[string]string) *AuditLogEntryBuilder {
builder.auditMetadata.AuditLabels = &labels
return builder
}
// WithNumResponseItems adds the number of items returned to the client if applicable.
func (builder *AuditLogEntryBuilder) WithNumResponseItems(numResponseItems int64) *AuditLogEntryBuilder {
builder.auditResponse.ResponseNumItems = &numResponseItems
return builder
}
// WithEventType overwrites the default event type EventTypeAdminActivity
func (builder *AuditLogEntryBuilder) WithEventType(eventType EventType) *AuditLogEntryBuilder {
builder.auditParams.EventType = eventType
return builder
}
// WithDetails adds an optional details object to the audit log entry
func (builder *AuditLogEntryBuilder) WithDetails(details map[string]interface{}) *AuditLogEntryBuilder {
builder.auditParams.Details = details
return builder
}
// WithSeverity overwrites the default log severity level auditV1.LogSeverity_LOG_SEVERITY_DEFAULT
func (builder *AuditLogEntryBuilder) WithSeverity(severity auditV1.LogSeverity) *AuditLogEntryBuilder {
builder.auditMetadata.AuditLogSeverity = severity
return builder
}
// WithStatusCode adds the (http) response status code
func (builder *AuditLogEntryBuilder) WithStatusCode(statusCode int) *AuditLogEntryBuilder {
builder.auditResponse.ResponseStatusCode = statusCode
return builder
}
// WithResponseBodyBytes adds the response body as bytes (json or protobuf expected)
func (builder *AuditLogEntryBuilder) WithResponseBodyBytes(responseBody *[]byte) *AuditLogEntryBuilder {
builder.auditResponse.ResponseBodyBytes = responseBody
return builder
}
// WithResponseHeaders adds response headers
func (builder *AuditLogEntryBuilder) WithResponseHeaders(responseHeaders map[string][]string) *AuditLogEntryBuilder {
builder.auditResponse.ResponseHeaders = responseHeaders
return builder
}
// WithResponseTime adds the time when the response is sent
func (builder *AuditLogEntryBuilder) WithResponseTime(responseTime time.Time) *AuditLogEntryBuilder {
builder.auditResponse.ResponseTime = &responseTime
return builder
}
// Build constructs the auditV1.AuditLogEntry.
//
// Parameters:
// - A context object
// - A SequenceNumber
//
// Returns:
// - The auditV1.AuditLogEntry protobuf message or
// - Error if the entry cannot be built
func (builder *AuditLogEntryBuilder) Build(ctx context.Context, sequenceNumber SequenceNumber) (*auditV1.AuditLogEntry, error) {
auditTime := time.Now()
builder.auditMetadata.AuditTime = &auditTime
objectId, _, pluralType, err := getObjectIdAndTypeFromAuditParams(ctx, &builder.auditParams)
if err != nil {
return nil, err
}
resourceName := fmt.Sprintf("%s/%s", *pluralType, objectId)
builder.auditMetadata.AuditInsertId = NewInsertId(time.Now().UTC(), builder.location, builder.workerId, uint64(sequenceNumber))
builder.auditMetadata.AuditLogName = fmt.Sprintf("%s/%s/logs/%s", *pluralType, objectId, builder.auditParams.EventType)
builder.auditMetadata.AuditResourceName = resourceName
var details *map[string]interface{} = nil
if len(builder.auditParams.Details) > 0 {
details = &builder.auditParams.Details
}
// Instantiate the audit event
return NewAuditLogEntry(
builder.auditRequest,
builder.auditResponse,
details,
builder.auditMetadata,
nil,
nil,
)
}
// AuditEventBuilder collects audit log parameters, validates input and
// returns a cloud event that can be sent to the audit log system.
type AuditEventBuilder struct {
// The audit api used to validate, serialize and send events
api *AuditApi
// The audit log entry builder which is used to build the actual protobuf message
auditLogEntryBuilder *AuditLogEntryBuilder
// Status whether the event has been built
built bool
// Sequence number generator providing sequential increasing numbers for the insert IDs
sequenceNumberGenerator *utils.SequenceNumberGenerator
// Opentelemtry tracer
tracer trace.Tracer
// Visibility of the event
visibility auditV1.Visibility
}
// NewAuditEventBuilder returns a builder that collects audit log parameters,
// validates input and returns a cloud event that can be sent to the audit log system.
func NewAuditEventBuilder(
// The audit api used to validate, serialize and send events
api *AuditApi,
// The sequence number generator can be used to get and revert sequence numbers to build audit log events
sequenceNumberGenerator *utils.SequenceNumberGenerator,
// Tracer
tracer trace.Tracer,
// The service name in lowercase (allowed characters are [a-z-]).
serviceName string,
// The ID of the K8s Pod, Service-Instance, etc. (must be unique for a sending service)
workerId string,
// The location of the service (e.g. eu01)
location string,
) *AuditEventBuilder {
return &AuditEventBuilder{
api: api,
auditLogEntryBuilder: NewAuditLogEntryBuilder().
WithRequiredServiceName(serviceName).
WithRequiredWorkerId(workerId).
WithRequiredLocation(location),
sequenceNumberGenerator: sequenceNumberGenerator,
tracer: tracer,
visibility: auditV1.Visibility_VISIBILITY_PUBLIC,
}
}
// NextSequenceNumber returns the next sequence number from utils.SequenceNumberGenerator.
// In case of an error RevertSequenceNumber must be called to prevent gaps in the sequence of numbers.
func (builder *AuditEventBuilder) NextSequenceNumber() SequenceNumber {
return SequenceNumber((*builder.sequenceNumberGenerator).Next())
}
// RevertSequenceNumber can be called to decrease the sequence number on the utils.SequenceNumberGenerator in case of an error
func (builder *AuditEventBuilder) RevertSequenceNumber() {
(*builder.sequenceNumberGenerator).Revert()
}
// WithAuditLogEntryBuilder overwrites the preconfigured AuditLogEntryBuilder
func (builder *AuditEventBuilder) WithAuditLogEntryBuilder(auditLogEntryBuilder *AuditLogEntryBuilder) *AuditEventBuilder {
builder.auditLogEntryBuilder = auditLogEntryBuilder
return builder
}
// WithRequiredApiRequest adds api request details
func (builder *AuditEventBuilder) WithRequiredApiRequest(request ApiRequest) *AuditEventBuilder {
builder.auditLogEntryBuilder.WithRequiredApiRequest(request)
return builder
}
// WithRequiredRequestClientIp adds the client ip
func (builder *AuditEventBuilder) WithRequiredRequestClientIp(requestClientIp string) *AuditEventBuilder {
builder.auditLogEntryBuilder.WithRequiredRequestClientIp(requestClientIp)
return builder
}
// WithRequestCorrelationId adds an optional request correlation id
func (builder *AuditEventBuilder) WithRequestCorrelationId(requestCorrelationId string) *AuditEventBuilder {
builder.auditLogEntryBuilder.WithRequestCorrelationId(requestCorrelationId)
return builder
}
// WithRequestId adds an optional request id
func (builder *AuditEventBuilder) WithRequestId(requestId string) *AuditEventBuilder {
builder.auditLogEntryBuilder.WithRequestId(requestId)
return builder
}
// WithRequestTime sets the request time on the builder. If not set - the instantiation time of the builder is used.
func (builder *AuditEventBuilder) WithRequestTime(requestTime time.Time) *AuditEventBuilder {
builder.auditLogEntryBuilder.WithRequestTime(requestTime)
return builder
}
// WithRequiredObjectId adds the object identifier.
// May be prefilled by audit middleware (if the identifier can be extracted from the url path).
func (builder *AuditEventBuilder) WithRequiredObjectId(objectId string) *AuditEventBuilder {
builder.auditLogEntryBuilder.WithRequiredObjectId(objectId)
return builder
}
// WithRequiredObjectType adds the object type.
// May be prefilled by audit middleware (if the type can be extracted from the url path).
func (builder *AuditEventBuilder) WithRequiredObjectType(objectType SingularType) *AuditEventBuilder {
builder.auditLogEntryBuilder.WithRequiredObjectType(objectType)
return builder
}
// WithRequiredOperation adds the name of the service method or operation.
//
// Format: stackit.<product>.<version>.<type-chain>.<operation>
// Where:
//
// Product: The name of the service in lowercase
// Version: Optional API version
// Type-Chain: Chained path to object
// Operation: The name of the operation in lowercase
//
// Examples:
//
// "stackit.resource-manager.v1.organizations.create"
// "stackit.authorization.v1.projects.volumes.create"
// "stackit.authorization.v2alpha.projects.volumes.create"
// "stackit.authorization.v2.folders.move"
// "stackit.resource-manager.health"
func (builder *AuditEventBuilder) WithRequiredOperation(operation string) *AuditEventBuilder {
builder.auditLogEntryBuilder.auditMetadata.AuditOperationName = operation
return builder
}
// WithAuditPermission adds the IAM permission
//
// Examples:
//
// "resourcemanager.project.edit"
func (builder *AuditEventBuilder) WithAuditPermission(permission string) *AuditEventBuilder {
builder.auditLogEntryBuilder.WithAuditPermission(permission)
return builder
}
// WithAuditPermissionCheckResult adds the IAM permission check result
func (builder *AuditEventBuilder) WithAuditPermissionCheckResult(permissionCheckResult bool) *AuditEventBuilder {
builder.auditLogEntryBuilder.WithAuditPermissionCheckResult(permissionCheckResult)
return builder
}
// WithLabels adds A set of user-defined (key, value) data that provides additional
// information about the log entry.
func (builder *AuditEventBuilder) WithLabels(labels map[string]string) *AuditEventBuilder {
builder.auditLogEntryBuilder.WithLabels(labels)
return builder
}
// WithNumResponseItems adds the number of items returned to the client if applicable.
func (builder *AuditEventBuilder) WithNumResponseItems(numResponseItems int64) *AuditEventBuilder {
builder.auditLogEntryBuilder.WithNumResponseItems(numResponseItems)
return builder
}
// WithEventType overwrites the default event type EventTypeAdminActivity
func (builder *AuditEventBuilder) WithEventType(eventType EventType) *AuditEventBuilder {
builder.auditLogEntryBuilder.WithEventType(eventType)
return builder
}
// WithDetails adds an optional details object to the audit log entry
func (builder *AuditEventBuilder) WithDetails(details map[string]interface{}) *AuditEventBuilder {
builder.auditLogEntryBuilder.WithDetails(details)
return builder
}
// WithSeverity overwrites the default log severity level auditV1.LogSeverity_LOG_SEVERITY_DEFAULT
func (builder *AuditEventBuilder) WithSeverity(severity auditV1.LogSeverity) *AuditEventBuilder {
builder.auditLogEntryBuilder.WithSeverity(severity)
return builder
}
// WithStatusCode adds the (http) response status code
func (builder *AuditEventBuilder) WithStatusCode(statusCode int) *AuditEventBuilder {
builder.auditLogEntryBuilder.WithStatusCode(statusCode)
return builder
}
// WithResponseBodyBytes adds the response body as bytes (json or protobuf expected)
func (builder *AuditEventBuilder) WithResponseBodyBytes(responseBody *[]byte) *AuditEventBuilder {
builder.auditLogEntryBuilder.WithResponseBodyBytes(responseBody)
return builder
}
// WithResponseHeaders adds response headers
func (builder *AuditEventBuilder) WithResponseHeaders(responseHeaders map[string][]string) *AuditEventBuilder {
builder.auditLogEntryBuilder.WithResponseHeaders(responseHeaders)
return builder
}
// WithResponseTime adds the time when the response is sent
func (builder *AuditEventBuilder) WithResponseTime(responseTime time.Time) *AuditEventBuilder {
builder.auditLogEntryBuilder.WithResponseTime(responseTime)
return builder
}
// WithVisibility overwrites the default visibility auditV1.Visibility_VISIBILITY_PUBLIC
func (builder *AuditEventBuilder) WithVisibility(visibility auditV1.Visibility) *AuditEventBuilder {
builder.visibility = visibility
return builder
}
// IsBuilt returns the status whether the cloud event has been built
func (builder *AuditEventBuilder) IsBuilt() bool {
return builder.built
}
// Build constructs the CloudEvent.
//
// Parameters:
// - A context object
// - A sequence number. AuditEventBuilder.NextSequenceNumber can be used to get the next SequenceNumber.
//
// Returns:
// - The CloudEvent containing the audit log entry
// - The RoutableIdentifier required for routing the cloud event
// - The operation name
// - Error if the event cannot be built
func (builder *AuditEventBuilder) Build(ctx context.Context, sequenceNumber SequenceNumber) (*CloudEvent, *RoutableIdentifier, string, error) {
if builder.auditLogEntryBuilder == nil {
return nil, nil, "", fmt.Errorf("audit log entry builder not set")
}
auditLogEntry, err := builder.auditLogEntryBuilder.Build(ctx, sequenceNumber)
if err != nil {
return nil, nil, "", err
}
objectId := builder.auditLogEntryBuilder.auditParams.ObjectId
objectType := builder.auditLogEntryBuilder.auditParams.ObjectType
ctx, span := builder.tracer.Start(ctx, "create-audit-event")
defer span.End()
w3cTraceParent := TraceParentFromSpan(span)
var traceParent = &w3cTraceParent
var traceState *string = nil
visibility := builder.visibility
operation := auditLogEntry.ProtoPayload.OperationName
routingIdentifier := NewAuditRoutingIdentifier(objectId, objectType)
// Validate and serialize the protobuf event into a cloud event
_, validateSerializeSpan := builder.tracer.Start(ctx, "validate-and-serialize-audit-event")
cloudEvent, err := (*builder.api).ValidateAndSerializeWithTrace(auditLogEntry, visibility, routingIdentifier, traceParent, traceState)
validateSerializeSpan.End()
if err != nil {
return nil, nil, "", err
}
builder.built = true
return cloudEvent,
routingIdentifier,
operation,
nil
}