audit-go/audit/api/builder.go
2024-10-30 15:41:48 +01:00

658 lines
23 KiB
Go

package api
import (
"context"
"dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/audit/utils"
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
"errors"
"fmt"
"github.com/google/uuid"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
"time"
)
const quadZero = "0.0.0.0"
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 ObjectType
ResponseBody any
// Log severity
Severity auditV1.LogSeverity
}
func getObjectIdAndTypeFromAuditParams(
auditParams *AuditParameters,
) (string, *ObjectType, error) {
objectId := auditParams.ObjectId
if objectId == "" {
return "", nil, errors.New("object id missing")
}
var objectType *ObjectType
if auditParams.ObjectType != "" {
objectType = &auditParams.ObjectType
}
if objectType == nil {
return "", nil, errors.New("object type missing")
}
if err := objectType.IsSupportedType(); err != nil {
return "", nil, err
}
return objectId, objectType, 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
// Opentelemetry tracer
tracer trace.Tracer
// 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: &ApiRequest{},
RequestClientIP: quadZero,
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: "",
tracer: otel.Tracer("audit-log-entry-builder"),
workerId: "",
}
}
func (builder *AuditLogEntryBuilder) AsSystemEvent() *AuditLogEntryBuilder {
if builder.auditRequest.Request == nil {
builder.auditRequest.Request = &ApiRequest{}
}
if builder.auditRequest.Request.Header == nil {
builder.auditRequest.Request.Header = map[string][]string{"user-agent": {"none"}}
}
if builder.auditRequest.Request.Host == "" {
builder.auditRequest.Request.Host = quadZero
}
if builder.auditRequest.Request.Method == "" {
builder.auditRequest.Request.Method = "OTHER"
}
if builder.auditRequest.Request.Scheme == "" {
builder.auditRequest.Request.Scheme = "none"
}
if builder.auditRequest.Request.Proto == "" {
builder.auditRequest.Request.Proto = "none"
}
if builder.auditRequest.Request.URL.Path == "" {
builder.auditRequest.Request.URL.Path = "none"
}
if builder.auditRequest.RequestClientIP == "" {
builder.auditRequest.RequestClientIP = quadZero
}
builder.WithEventType(EventTypeSystemEvent)
return builder
}
// 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 ObjectType) *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
}
// WithResponseBody adds the response body to the builder and transforms it in the Build method (json serializable or protobuf message expected)
func (builder *AuditLogEntryBuilder) WithResponseBody(responseBody any) *AuditLogEntryBuilder {
builder.auditParams.ResponseBody = responseBody
return builder
}
// WithResponseBodyBytes adds the response body as bytes (serialized json or protobuf message 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) {
_, span := builder.tracer.Start(ctx, "build-audit-log-entry")
defer span.End()
auditTime := time.Now()
builder.auditMetadata.AuditTime = &auditTime
objectId, objectType, err := getObjectIdAndTypeFromAuditParams(&builder.auditParams)
if err != nil {
return nil, err
}
if builder.auditResponse.ResponseBodyBytes != nil && builder.auditParams.ResponseBody != nil {
return nil, errors.New("responseBodyBytes and responseBody set")
} else if builder.auditParams.ResponseBody != nil {
responseBytes, err := ResponseBodyToBytes(builder.auditParams.ResponseBody)
if err != nil {
return nil, err
}
builder.auditResponse.ResponseBodyBytes = responseBytes
}
resourceName := fmt.Sprintf("%s/%s", objectType.Plural(), objectId)
var logIdentifier string
var logType ObjectType
if builder.auditParams.EventType == EventTypeSystemEvent {
logIdentifier = SystemIdentifier.Identifier
logType = ObjectTypeSystem
} else {
logIdentifier = objectId
logType = *objectType
}
builder.auditMetadata.AuditInsertId = NewInsertId(time.Now().UTC(), builder.location, builder.workerId, uint64(sequenceNumber))
builder.auditMetadata.AuditLogName = fmt.Sprintf("%s/%s/logs/%s", logType.Plural(), logIdentifier, 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,
)
}
// 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
// Opentelemetry 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,
// 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: otel.Tracer("audit-event-builder"),
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()
}
func (builder *AuditEventBuilder) AsSystemEvent() *AuditEventBuilder {
builder.auditLogEntryBuilder.AsSystemEvent()
builder.WithVisibility(auditV1.Visibility_VISIBILITY_PRIVATE)
return builder
}
// 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 ObjectType) *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
}
// WithResponseBody adds the response body to the builder and transforms it in the Build method (json serializable or protobuf message expected)
func (builder *AuditEventBuilder) WithResponseBody(responseBody any) *AuditEventBuilder {
builder.auditLogEntryBuilder.WithResponseBody(responseBody)
return builder
}
// WithResponseBodyBytes adds the response body as bytes (serialized json or protobuf message 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, error) {
if builder.auditLogEntryBuilder == nil {
return nil, nil, fmt.Errorf("audit log entry builder not set")
}
ctx, span := builder.tracer.Start(ctx, "build-audit-event")
defer span.End()
visibility := builder.visibility
objectId := builder.auditLogEntryBuilder.auditParams.ObjectId
objectType := builder.auditLogEntryBuilder.auditParams.ObjectType
var routingIdentifier *RoutableIdentifier
if builder.auditLogEntryBuilder.auditParams.EventType == EventTypeSystemEvent {
routingIdentifier = NewAuditRoutingIdentifier(uuid.Nil.String(), ObjectTypeSystem)
if objectId == "" {
objectId = uuid.Nil.String()
builder.WithRequiredObjectId(objectId)
}
if objectType == "" {
objectType = ObjectTypeSystem
builder.WithRequiredObjectType(objectType)
}
} else {
routingIdentifier = NewAuditRoutingIdentifier(objectId, objectType)
}
auditLogEntry, err := builder.auditLogEntryBuilder.Build(ctx, sequenceNumber)
if err != nil {
return nil, nil, err
}
// Validate and serialize the protobuf event into a cloud event
cloudEvent, err := (*builder.api).ValidateAndSerialize(ctx, auditLogEntry, visibility, routingIdentifier)
if err != nil {
return nil, nil, err
}
builder.built = true
return cloudEvent,
routingIdentifier,
nil
}