mirror of
https://dev.azure.com/schwarzit/schwarzit.stackit-public/_git/audit-go
synced 2026-02-07 16:47:24 +00:00
Add reusable code to create audit events
This commit is contained in:
parent
8ad633a5c4
commit
61ac703743
6 changed files with 2038 additions and 2 deletions
84
audit/api/log.go
Normal file
84
audit/api/log.go
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-core-platform/audit-go.git/gen/go/audit/v1"
|
||||
"encoding/json"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
"log/slog"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LogEvent logs an event to the terminal
|
||||
func LogEvent(
|
||||
event *CloudEvent,
|
||||
auditEvent *auditV1.AuditLogEntry,
|
||||
routableIdentifier *RoutableIdentifier,
|
||||
visibility auditV1.Visibility,
|
||||
) error {
|
||||
|
||||
// Convert to json
|
||||
auditEventJson, err := protojson.Marshal(auditEvent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
auditEventMap := make(map[string]interface{})
|
||||
err = json.Unmarshal(auditEventJson, &auditEventMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
objectIdentifierJson, err := protojson.Marshal(routableIdentifier.ToObjectIdentifier())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
objectIdentifierMap := make(map[string]interface{})
|
||||
err = json.Unmarshal(objectIdentifierJson, &objectIdentifierMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cloudEvent := cloudEvent{
|
||||
SpecVersion: event.SpecVersion,
|
||||
Source: event.Source,
|
||||
Id: event.Id,
|
||||
Time: event.Time,
|
||||
DataContentType: event.DataContentType,
|
||||
DataType: event.DataType,
|
||||
Subject: event.Subject,
|
||||
Data: routableEvent{
|
||||
OperationName: auditEvent.ProtoPayload.OperationName,
|
||||
Visibility: visibility.String(),
|
||||
ResourceReference: objectIdentifierMap,
|
||||
Data: auditEventMap,
|
||||
},
|
||||
TraceParent: event.TraceParent,
|
||||
TraceState: event.TraceState,
|
||||
}
|
||||
cloudEventJson, err := json.Marshal(cloudEvent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
slog.Info(string(cloudEventJson))
|
||||
return nil
|
||||
}
|
||||
|
||||
type cloudEvent struct {
|
||||
SpecVersion string
|
||||
Source string
|
||||
Id string
|
||||
Time time.Time
|
||||
DataContentType string
|
||||
DataType string
|
||||
Subject string
|
||||
Data routableEvent
|
||||
TraceParent *string
|
||||
TraceState *string
|
||||
}
|
||||
|
||||
type routableEvent struct {
|
||||
OperationName string
|
||||
Visibility string
|
||||
ResourceReference map[string]interface{}
|
||||
Data map[string]interface{}
|
||||
}
|
||||
897
audit/api/model.go
Normal file
897
audit/api/model.go
Normal file
|
|
@ -0,0 +1,897 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"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"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/google/uuid"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
"google.golang.org/protobuf/types/known/wrapperspb"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var ErrInvalidRequestBody = errors.New("invalid request body")
|
||||
var ErrInvalidResponse = errors.New("invalid response")
|
||||
var ErrInvalidAuthorizationHeaderValue = errors.New("invalid authorization header value")
|
||||
var ErrInvalidBearerToken = errors.New("invalid bearer token")
|
||||
var ErrTokenIsNotBearerToken = errors.New("token is not a bearer token")
|
||||
|
||||
var objectTypeIdPattern, _ = regexp.Compile(".*/(projects|folders|organizations)/([0-9a-fA-F-]{36})(?:/.*)?")
|
||||
|
||||
type Request struct {
|
||||
Body *[]byte
|
||||
Header map[string][]string
|
||||
Host string
|
||||
Method string
|
||||
Scheme string
|
||||
Proto string
|
||||
URL RequestUrl
|
||||
}
|
||||
|
||||
type RequestUrl struct {
|
||||
Path string
|
||||
RawQuery string
|
||||
}
|
||||
|
||||
// AuditRequest bundles request related parameters
|
||||
type AuditRequest struct {
|
||||
|
||||
// The operation request. This may not include all request parameters,
|
||||
// such as those that are too large, privacy-sensitive, or duplicated
|
||||
// elsewhere in the log record.
|
||||
// It should never include user-generated data, such as file contents.
|
||||
//
|
||||
// Required: false
|
||||
Request *Request
|
||||
|
||||
// The IP address of the caller.
|
||||
// For caller from internet, this will be public IPv4 or IPv6 address.
|
||||
// For caller from a VM / K8s Service / etc, this will be the SIT proxy's IPv4 address.
|
||||
//
|
||||
// Required: true
|
||||
RequestClientIP string
|
||||
|
||||
// Correlate multiple audit logs by setting the same id
|
||||
//
|
||||
// Required: false
|
||||
RequestCorrelationId *string
|
||||
|
||||
// The unique ID for a request, which can be propagated to downstream
|
||||
// systems. The ID should have low probability of collision
|
||||
// within a single day for a specific service.
|
||||
//
|
||||
// More information can be found here: https://google.aip.dev/155
|
||||
//
|
||||
// Format: <idempotency-key>
|
||||
// Where:
|
||||
// Idempotency-key: Typically consists of a id + version
|
||||
//
|
||||
// Examples:
|
||||
// 5e3952a9-b628-4be6-ac61-b1c6eb4a110c/5
|
||||
//
|
||||
// Required: false
|
||||
RequestId *string
|
||||
|
||||
// The timestamp when the `destination` service receives the first byte of
|
||||
// the request.
|
||||
//
|
||||
// Required: false
|
||||
RequestTime *time.Time
|
||||
}
|
||||
|
||||
// AuditResponse bundles response related parameters
|
||||
type AuditResponse struct {
|
||||
|
||||
// The operation response. This may not include all response elements,
|
||||
// such as those that are too large, privacy-sensitive, or duplicated
|
||||
// elsewhere in the log record.
|
||||
//
|
||||
// Required: false
|
||||
ResponseBodyBytes *[]byte
|
||||
|
||||
// The http or gRPC status code.
|
||||
//
|
||||
// Examples:
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
|
||||
// https://grpc.github.io/grpc/core/md_doc_statuscodes.html
|
||||
//
|
||||
// Required: true
|
||||
ResponseStatusCode int
|
||||
|
||||
// The HTTP response headers.
|
||||
//
|
||||
// Required: true
|
||||
ResponseHeaders map[string][]string
|
||||
|
||||
// The number of items returned from a List or Query API method,
|
||||
// if applicable.
|
||||
//
|
||||
// Required: false
|
||||
ResponseNumItems *int64
|
||||
|
||||
// The timestamp when the "destination" service generates the first byte of
|
||||
// the response.
|
||||
//
|
||||
// Required: false
|
||||
ResponseTime *time.Time
|
||||
}
|
||||
|
||||
// AuditMetadata bundles audit event related metadata
|
||||
type AuditMetadata struct {
|
||||
|
||||
// A unique identifier for the log entry.
|
||||
// Is used to check completeness of audit events over time.
|
||||
//
|
||||
// Format: <unix-timestamp>/<region-zone>/<worker-id>/<sequence-number>
|
||||
// Where:
|
||||
// Unix-Timestamp: A UTC unix timestamp in seconds is expected
|
||||
// Region-Zone: The region and (optional) zone id. If both, separated with a - (dash)
|
||||
// Worker-Id: The ID of the K8s Pod, Service-Instance, etc (must be unique for a sending service)
|
||||
// Sequence-Number: Increasing number, representing the message offset per Worker-Id
|
||||
// If the Worker-Id changes, the sequence-number has to be reset to 0.
|
||||
//
|
||||
// Examples:
|
||||
// "1721899117/eu01/319a7fb9-edd2-46c6-953a-a724bb377c61/8792726390909855142"
|
||||
//
|
||||
// Required: true
|
||||
AuditInsertId string
|
||||
|
||||
// A set of user-defined (key, value) data that provides additional
|
||||
// information about the log entry.
|
||||
//
|
||||
// Required: false
|
||||
AuditLabels *map[string]string
|
||||
|
||||
// The resource name of the log to which this log entry belongs.
|
||||
//
|
||||
// Format: <pluralType>/<identifier>/logs/<eventType>
|
||||
// Where:
|
||||
// Plural-Types: One from the list of supported data types
|
||||
// Event-Types: admin-activity, system-event, policy-denied, data-access
|
||||
//
|
||||
// Examples:
|
||||
// "projects/00b0f972-59ff-48f2-a4f9-29c57b75c2fa/logs/admin-activity"
|
||||
//
|
||||
// Required: true
|
||||
AuditLogName string
|
||||
|
||||
// The severity of the log entry.
|
||||
//
|
||||
// Required: true
|
||||
AuditLogSeverity auditV1.LogSeverity
|
||||
|
||||
// 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"
|
||||
//
|
||||
// Required: true
|
||||
AuditOperationName string
|
||||
|
||||
// The required IAM permission.
|
||||
//
|
||||
// Examples:
|
||||
// "resourcemanager.project.edit"
|
||||
//
|
||||
// Required: false
|
||||
AuditPermission *string
|
||||
|
||||
// Result of the IAM permission check.
|
||||
//
|
||||
// Required: false
|
||||
AuditPermissionGranted *bool
|
||||
|
||||
// The resource or collection that is the target of the operation.
|
||||
// The name is a scheme-less URI, not including the API service name.
|
||||
//
|
||||
// Format: <pluralType>/<id>[/locations/<region-zone>][/<details>]
|
||||
// Where:
|
||||
// Plural-Type: One from the list of supported data types
|
||||
// Id: The identifier of the object
|
||||
// Region-Zone: Optional region and zone id. If both, separated with a - (dash). Alternatively _ (underscore).
|
||||
// Details: Optional "<key>/<id>" pairs
|
||||
//
|
||||
// Examples:
|
||||
// "organizations/40ab14ad-b7b0-4b1c-be41-5bc820a968d1"
|
||||
// "projects/7046e7b6-5ae9-441c-99fe-2cd28a5078ec/locations/_/instances/instance-20240723-174217"
|
||||
// "projects/7046e7b6-5ae9-441c-99fe-2cd28a5078ec/locations/eu01/instances/instance-20240723-174217"
|
||||
// "projects/dd7d1807-54e9-4426-8994-721758b5b554/locations/eu01/vms/b6851b4e-7a9d-4973-ab0f-a80a13ee3060/ports/78f8bad4-a291-4fa3-b07f-4a1985d3dbe8"
|
||||
//
|
||||
// Required: true
|
||||
AuditResourceName string
|
||||
|
||||
// The name of the API service performing the operation.
|
||||
//
|
||||
// Examples:
|
||||
// "resource-manager"
|
||||
//
|
||||
// Required: true
|
||||
AuditServiceName string
|
||||
|
||||
// The time the event described by the log entry occurred.
|
||||
//
|
||||
// Required: false
|
||||
AuditTime *time.Time
|
||||
}
|
||||
|
||||
// NewAuditLogEntry constructs a new audit log event for the given parameters
|
||||
func NewAuditLogEntry(
|
||||
|
||||
// Required request parameters
|
||||
auditRequest AuditRequest,
|
||||
|
||||
// Required response parameters
|
||||
auditResponse AuditResponse,
|
||||
|
||||
// Optional map that is added as "details" to the message
|
||||
eventMetadata *map[string]interface{},
|
||||
|
||||
// Required metadata
|
||||
auditMetadata AuditMetadata,
|
||||
|
||||
// Optional W3C trace parent
|
||||
userProvidedTraceParent *string,
|
||||
|
||||
// Optional W3C trace state
|
||||
userProvidedTraceState *string,
|
||||
) (*auditV1.AuditLogEntry, error) {
|
||||
|
||||
// Get request headers
|
||||
filteredRequestHeaders := FilterAndMergeHeaders(auditRequest.Request.Header)
|
||||
filteredResponseHeaders := FilterAndMergeHeaders(auditResponse.ResponseHeaders)
|
||||
|
||||
// Get response body
|
||||
responseBody, err := NewResponseBody(auditResponse.ResponseBodyBytes)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidResponse
|
||||
}
|
||||
var responseLength *int64 = nil
|
||||
if responseBody != nil {
|
||||
length := int64(len(*auditResponse.ResponseBodyBytes))
|
||||
responseLength = &length
|
||||
}
|
||||
|
||||
// Get request body
|
||||
requestBody, err := NewRequestBody(auditRequest.Request)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidRequestBody
|
||||
}
|
||||
|
||||
// Get audit attributes from request
|
||||
auditClaims, authenticationPrincipal, audiences, authenticationInfo, err :=
|
||||
AuditAttributesFromAuthorizationHeader(auditRequest.Request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get request scheme (http, https)
|
||||
scheme := auditRequest.Request.Scheme
|
||||
|
||||
// Initialize authorization info if available
|
||||
var authorizationInfo []*auditV1.AuthorizationInfo = nil
|
||||
if auditMetadata.AuditPermission != nil && auditMetadata.AuditPermissionGranted != nil {
|
||||
authorizationInfo = []*auditV1.AuthorizationInfo{
|
||||
NewAuthorizationInfo(
|
||||
auditMetadata.AuditResourceName,
|
||||
*auditMetadata.AuditPermission,
|
||||
*auditMetadata.AuditPermissionGranted)}
|
||||
}
|
||||
|
||||
// Initialize labels if available
|
||||
var labels map[string]string = nil
|
||||
if auditMetadata.AuditLabels != nil {
|
||||
labels = *auditMetadata.AuditLabels
|
||||
}
|
||||
|
||||
// Initialize metadata/details
|
||||
var metadata *structpb.Struct = nil
|
||||
if eventMetadata != nil {
|
||||
metadataStruct, err := structpb.NewStruct(*eventMetadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
metadata = metadataStruct
|
||||
}
|
||||
|
||||
// Get request and audit time
|
||||
var concreteRequestTime = time.Now().UTC()
|
||||
if auditRequest.RequestTime != nil {
|
||||
concreteRequestTime = *auditRequest.RequestTime
|
||||
}
|
||||
var concreteAuditTime = concreteRequestTime
|
||||
if auditMetadata.AuditTime != nil {
|
||||
concreteAuditTime = *auditMetadata.AuditTime
|
||||
}
|
||||
var concreteResponseTime = concreteRequestTime
|
||||
if auditResponse.ResponseTime != nil {
|
||||
concreteResponseTime = *auditResponse.ResponseTime
|
||||
}
|
||||
|
||||
// Initialize the audit log entry
|
||||
event := auditV1.AuditLogEntry{
|
||||
LogName: auditMetadata.AuditLogName,
|
||||
ProtoPayload: &auditV1.AuditLog{
|
||||
ServiceName: auditMetadata.AuditServiceName,
|
||||
OperationName: auditMetadata.AuditOperationName,
|
||||
ResourceName: auditMetadata.AuditResourceName,
|
||||
AuthenticationInfo: authenticationInfo,
|
||||
AuthorizationInfo: authorizationInfo,
|
||||
RequestMetadata: NewRequestMetadata(
|
||||
auditRequest.Request,
|
||||
filteredRequestHeaders,
|
||||
auditRequest.RequestId,
|
||||
scheme,
|
||||
concreteRequestTime,
|
||||
auditRequest.RequestClientIP,
|
||||
authenticationPrincipal,
|
||||
audiences,
|
||||
auditClaims),
|
||||
Request: requestBody,
|
||||
ResponseMetadata: NewResponseMetadata(
|
||||
auditResponse.ResponseStatusCode,
|
||||
auditResponse.ResponseNumItems,
|
||||
responseLength,
|
||||
filteredResponseHeaders,
|
||||
concreteResponseTime),
|
||||
Response: responseBody,
|
||||
Metadata: metadata,
|
||||
},
|
||||
InsertId: auditMetadata.AuditInsertId,
|
||||
Labels: labels,
|
||||
CorrelationId: auditRequest.RequestCorrelationId,
|
||||
Timestamp: timestamppb.New(concreteAuditTime),
|
||||
Severity: auditMetadata.AuditLogSeverity,
|
||||
TraceParent: userProvidedTraceParent,
|
||||
TraceState: userProvidedTraceState,
|
||||
}
|
||||
return &event, nil
|
||||
}
|
||||
|
||||
// GetCalledServiceNameFromRequest extracts the called service name from subdomain name
|
||||
func GetCalledServiceNameFromRequest(request *Request, fallbackName string) string {
|
||||
var calledServiceName = fallbackName
|
||||
host := request.Host
|
||||
if !strings.Contains(host, "localhost") {
|
||||
dotIdx := strings.Index(host, ".")
|
||||
if dotIdx != -1 {
|
||||
calledServiceName = host[0:dotIdx]
|
||||
}
|
||||
}
|
||||
return calledServiceName
|
||||
}
|
||||
|
||||
// AuditSpan is an abstraction for trace.Span that can easier be tested
|
||||
type AuditSpan interface {
|
||||
SpanContext() trace.SpanContext
|
||||
}
|
||||
|
||||
// TraceParentFromSpan returns W3C conform trace parent from AuditSpan
|
||||
func TraceParentFromSpan(span AuditSpan) string {
|
||||
traceVersion := "00"
|
||||
traceId := span.SpanContext().TraceID().String()
|
||||
parentId := span.SpanContext().SpanID().String()
|
||||
// Trace flags according to W3C documentation:
|
||||
// https://www.w3.org/TR/trace-context/#sampled-flag
|
||||
var traceFlags = "00"
|
||||
if span.SpanContext().TraceFlags().IsSampled() {
|
||||
traceFlags = "01"
|
||||
}
|
||||
|
||||
// Format: <version>-<trace-id>-<parent-id>-<trace-flags>
|
||||
// Example: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
|
||||
w3cTraceParent := fmt.Sprintf("%s-%s-%s-%s", traceVersion, traceId, parentId, traceFlags)
|
||||
return w3cTraceParent
|
||||
}
|
||||
|
||||
// NewPbInt64Value returns protobuf int64 wrapper if value is not nil.
|
||||
func NewPbInt64Value(value *int64) *wrapperspb.Int64Value {
|
||||
if value != nil {
|
||||
return wrapperspb.Int64(*value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewRequestMetadata returns initialized protobuf RequestMetadata object.
|
||||
func NewRequestMetadata(
|
||||
request *Request,
|
||||
requestHeaders map[string]string,
|
||||
requestId *string,
|
||||
requestScheme string,
|
||||
requestTime time.Time,
|
||||
clientIp string,
|
||||
authenticationPrincipal string,
|
||||
audiences []string,
|
||||
auditClaims *structpb.Struct,
|
||||
) *auditV1.RequestMetadata {
|
||||
|
||||
return &auditV1.RequestMetadata{
|
||||
CallerIp: clientIp,
|
||||
CallerSuppliedUserAgent: requestHeaders["User-Agent"],
|
||||
RequestAttributes: NewRequestAttributes(
|
||||
request,
|
||||
requestHeaders,
|
||||
requestId,
|
||||
requestScheme,
|
||||
requestTime,
|
||||
authenticationPrincipal,
|
||||
audiences,
|
||||
auditClaims,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// NewRequestAttributes returns initialized protobuf AttributeContext_Request object.
|
||||
func NewRequestAttributes(
|
||||
request *Request,
|
||||
requestHeaders map[string]string,
|
||||
requestId *string,
|
||||
requestScheme string,
|
||||
requestTime time.Time,
|
||||
authenticationPrincipal string,
|
||||
audiences []string,
|
||||
auditClaims *structpb.Struct,
|
||||
) *auditV1.AttributeContext_Request {
|
||||
|
||||
rawQuery := request.URL.RawQuery
|
||||
var query *string = nil
|
||||
if rawQuery != "" {
|
||||
escapedQuery := url.QueryEscape(rawQuery)
|
||||
query = &escapedQuery
|
||||
}
|
||||
|
||||
return &auditV1.AttributeContext_Request{
|
||||
Id: requestId,
|
||||
Method: utils.StringToHttpMethod(request.Method),
|
||||
Headers: requestHeaders,
|
||||
Path: request.URL.Path,
|
||||
Host: request.Host,
|
||||
Scheme: requestScheme,
|
||||
Query: query,
|
||||
Time: timestamppb.New(requestTime),
|
||||
Protocol: request.Proto,
|
||||
Auth: &auditV1.AttributeContext_Auth{
|
||||
Principal: authenticationPrincipal,
|
||||
Audiences: audiences,
|
||||
Claims: auditClaims,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewAuthorizationInfo returns protobuf AuthorizationInfo for the given parameters.
|
||||
func NewAuthorizationInfo(resourceName string, permission string, granted bool) *auditV1.AuthorizationInfo {
|
||||
return &auditV1.AuthorizationInfo{
|
||||
Resource: resourceName,
|
||||
Permission: &permission,
|
||||
Granted: &granted,
|
||||
}
|
||||
}
|
||||
|
||||
// NewInsertId returns a correctly formatted insert it.
|
||||
func NewInsertId(insertTime time.Time, location string, workerId string, eventSequenceNumber uint64) string {
|
||||
return fmt.Sprintf("%d/%s/%s/%d", insertTime.UnixNano(), location, workerId, eventSequenceNumber)
|
||||
}
|
||||
|
||||
// NewResponseMetadata returns protobuf response status with status code and short message.
|
||||
func NewResponseMetadata(statusCode int, numResponseItems *int64, responseSize *int64, headers map[string]string, responseTime time.Time) *auditV1.ResponseMetadata {
|
||||
|
||||
var message *string = nil
|
||||
if statusCode >= 400 && statusCode < 500 {
|
||||
text := "Client error"
|
||||
message = &text
|
||||
} else if statusCode >= 500 {
|
||||
text := "Server error"
|
||||
message = &text
|
||||
}
|
||||
|
||||
var size *wrapperspb.Int64Value = nil
|
||||
if responseSize != nil {
|
||||
size = wrapperspb.Int64(*responseSize)
|
||||
}
|
||||
return &auditV1.ResponseMetadata{
|
||||
StatusCode: wrapperspb.Int32(int32(statusCode)),
|
||||
ErrorMessage: message,
|
||||
ErrorDetails: nil,
|
||||
ResponseAttributes: &auditV1.AttributeContext_Response{
|
||||
NumResponseItems: NewPbInt64Value(numResponseItems),
|
||||
Size: size,
|
||||
Headers: headers,
|
||||
Time: timestamppb.New(responseTime),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewResponseBody converts the JSON byte response into a protobuf struct.
|
||||
func NewResponseBody(response *[]byte) (*structpb.Struct, error) {
|
||||
|
||||
// Return if nil
|
||||
if response == nil || len(*response) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Convert to protobuf struct
|
||||
return byteArrayToPbStruct(*response)
|
||||
}
|
||||
|
||||
// NewRequestBody converts the request body into a protobuf struct.
|
||||
func NewRequestBody(request *Request) (*structpb.Struct, error) {
|
||||
|
||||
if request.Body == nil || len(*request.Body) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Convert to protobuf struct
|
||||
return byteArrayToPbStruct(*request.Body)
|
||||
}
|
||||
|
||||
// byteArrayToPbStruct converts a given json byte array into a protobuf struct.
|
||||
func byteArrayToPbStruct(bytes []byte) (*structpb.Struct, error) {
|
||||
var bodyMap map[string]interface{}
|
||||
err := json.Unmarshal(bytes, &bodyMap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return structpb.NewStruct(bodyMap)
|
||||
}
|
||||
|
||||
// FilterAndMergeHeaders filters the "Authorization" and "B3" headers as well as
|
||||
// all headers starting with the prefixes "X-" and "STACKIT-".
|
||||
// Headers are merged if there is more than one value for a given name.
|
||||
func FilterAndMergeHeaders(headers map[string][]string) map[string]string {
|
||||
var resultMap = make(map[string]string)
|
||||
skipHeaders := []string{"authorization", "b3"}
|
||||
skipPrefixHeaders := []string{"x-", "stackit-"}
|
||||
|
||||
if len(headers) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for headerName, headerValues := range headers {
|
||||
headerLower := strings.ToLower(headerName)
|
||||
|
||||
// Check if headers with a specific prefix is found
|
||||
skip := false
|
||||
for _, skipPrefix := range skipPrefixHeaders {
|
||||
if strings.HasPrefix(headerLower, skipPrefix) {
|
||||
skip = true
|
||||
}
|
||||
}
|
||||
|
||||
// Keep header if not on filter list or value is empty
|
||||
if !skip && !slices.Contains(skipHeaders, headerLower) && len(headerValues) > 0 {
|
||||
resultMap[headerName] = strings.Join(headerValues, ",")
|
||||
}
|
||||
}
|
||||
|
||||
return resultMap
|
||||
}
|
||||
|
||||
// NewAuditRoutingIdentifier instantiates a new auditApi.RoutableIdentifier for
|
||||
// the given object ID and singular type.
|
||||
func NewAuditRoutingIdentifier(objectId string, singularType SingularType) *RoutableIdentifier {
|
||||
return &RoutableIdentifier{
|
||||
Identifier: objectId,
|
||||
Type: singularType,
|
||||
}
|
||||
}
|
||||
|
||||
// AuditAttributesFromAuthorizationHeader extracts the following claims from given http.Request:
|
||||
// - auditClaims - filtered list of claims
|
||||
// - authenticationPrincipal - principal identifier
|
||||
// - audiences - list of audience claims
|
||||
// - authenticationInfo - information about the user or service-account authentication
|
||||
func AuditAttributesFromAuthorizationHeader(request *Request) (
|
||||
*structpb.Struct,
|
||||
string,
|
||||
[]string,
|
||||
*auditV1.AuthenticationInfo,
|
||||
error,
|
||||
) {
|
||||
|
||||
var principalId string
|
||||
var principalEmail string
|
||||
var auditClaims *structpb.Struct = nil
|
||||
var authenticationPrincipal = "none/none"
|
||||
var serviceAccountName *string = nil
|
||||
audiences := make([]string, 0)
|
||||
var delegationInfo []*auditV1.ServiceAccountDelegationInfo = nil
|
||||
|
||||
authorizationHeaders := request.Header["Authorization"]
|
||||
authorizationHeader := strings.Join(authorizationHeaders, ",")
|
||||
trimmedAuthorizationHeader := strings.TrimSpace(authorizationHeader)
|
||||
if len(trimmedAuthorizationHeader) > 0 {
|
||||
|
||||
// Parse claims
|
||||
parsedClaims, filteredClaims, err := parseClaimsFromAuthorizationHeader(trimmedAuthorizationHeader)
|
||||
if err != nil {
|
||||
return nil, authenticationPrincipal, nil, nil, err
|
||||
}
|
||||
|
||||
// Convert filtered claims to protobuf struct
|
||||
auditClaimsStruct, err := structpb.NewStruct(filteredClaims)
|
||||
if err != nil {
|
||||
return nil, authenticationPrincipal, nil, nil, err
|
||||
}
|
||||
auditClaims = auditClaimsStruct
|
||||
|
||||
// Extract principal data
|
||||
authenticationPrincipal = extractAuthenticationPrincipal(parsedClaims)
|
||||
principalId, principalEmail = extractSubjectAndEmail(parsedClaims)
|
||||
|
||||
// Extract service account delegation info data
|
||||
delegationInfo = extractServiceAccountDelegationInfo(parsedClaims)
|
||||
|
||||
// Extract audiences data
|
||||
audiences, err = extractAudiences(parsedClaims)
|
||||
if err != nil {
|
||||
return nil, authenticationPrincipal, nil, nil, err
|
||||
}
|
||||
|
||||
// Extract project id and service account id
|
||||
projectId := extractServiceAccountProjectId(parsedClaims)
|
||||
serviceAccountId := extractServiceAccountId(parsedClaims)
|
||||
|
||||
// Calculate service account name if project id and service account id are available
|
||||
if projectId != nil && serviceAccountId != nil {
|
||||
accountName := fmt.Sprintf("projects/%s/serviceAccounts/%s", *projectId, *serviceAccountId)
|
||||
serviceAccountName = &accountName
|
||||
}
|
||||
}
|
||||
|
||||
authenticationInfo := auditV1.AuthenticationInfo{
|
||||
PrincipalId: principalId,
|
||||
PrincipalEmail: principalEmail,
|
||||
ServiceAccountName: serviceAccountName,
|
||||
ServiceAccountDelegationInfo: delegationInfo,
|
||||
}
|
||||
|
||||
return auditClaims, authenticationPrincipal, audiences, &authenticationInfo, nil
|
||||
}
|
||||
|
||||
func extractServiceAccountId(parsedClaims map[string]interface{}) *string {
|
||||
projectId, projectIdExists := parsedClaims["stackit/serviceaccount/service-account.uid"]
|
||||
if projectIdExists {
|
||||
projectIdString := fmt.Sprintf("%s", projectId)
|
||||
return &projectIdString
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func extractServiceAccountProjectId(parsedClaims map[string]interface{}) *string {
|
||||
projectId, projectIdExists := parsedClaims["stackit/project/project.id"]
|
||||
if projectIdExists {
|
||||
projectIdString := fmt.Sprintf("%s", projectId)
|
||||
return &projectIdString
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func extractAudiences(parsedClaims map[string]interface{}) ([]string, error) {
|
||||
audClaim, _ := json.Marshal(parsedClaims["aud"])
|
||||
|
||||
var audiences []string
|
||||
if err := json.Unmarshal(audClaim, &audiences); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return audiences, nil
|
||||
}
|
||||
|
||||
func extractAuthenticationPrincipal(parsedClaims map[string]interface{}) string {
|
||||
subClaim, subExists := parsedClaims["sub"]
|
||||
issuerClaim, issuerExists := parsedClaims["iss"]
|
||||
|
||||
var principal = "none/none"
|
||||
if subExists && issuerExists {
|
||||
principal = fmt.Sprintf("%s/%s", url.QueryEscape(subClaim.(string)), url.QueryEscape(issuerClaim.(string)))
|
||||
}
|
||||
return principal
|
||||
}
|
||||
|
||||
func parseClaimsFromAuthorizationHeader(authorizationHeader string) (map[string]interface{}, map[string]interface{}, error) {
|
||||
parts := strings.Split(authorizationHeader, " ")
|
||||
if len(parts) != 2 {
|
||||
return nil, nil, ErrInvalidAuthorizationHeaderValue
|
||||
}
|
||||
if !strings.EqualFold(parts[0], "Bearer") {
|
||||
return nil, nil, ErrTokenIsNotBearerToken
|
||||
}
|
||||
jwt := parts[1]
|
||||
authorizationHeaderParts := strings.Split(jwt, ".")
|
||||
|
||||
parsedClaims := make(map[string]interface{})
|
||||
if len(authorizationHeaderParts) == 3 {
|
||||
// base64 decoding
|
||||
decodedString, err := base64.RawURLEncoding.DecodeString(authorizationHeaderParts[1])
|
||||
if err != nil {
|
||||
return parsedClaims, nil, ErrInvalidBearerToken
|
||||
}
|
||||
|
||||
// unmarshall claim part of token
|
||||
err = json.Unmarshal(decodedString, &parsedClaims)
|
||||
if err != nil {
|
||||
return parsedClaims, nil, err
|
||||
}
|
||||
|
||||
// Collect user-friendly filtered subset of claims
|
||||
filteredClaims := make(map[string]interface{})
|
||||
_ = json.Unmarshal(decodedString, &filteredClaims)
|
||||
keysToDelete := make([]string, 0)
|
||||
for key := range filteredClaims {
|
||||
if key != "aud" && key != "email" && key != "iss" && key != "jti" && key != "sub" {
|
||||
keysToDelete = append(keysToDelete, key)
|
||||
}
|
||||
}
|
||||
for _, key := range keysToDelete {
|
||||
delete(filteredClaims, key)
|
||||
}
|
||||
return parsedClaims, filteredClaims, nil
|
||||
}
|
||||
return parsedClaims, nil, ErrInvalidBearerToken
|
||||
}
|
||||
|
||||
func extractServiceAccountDelegationInfoDetails(token map[string]interface{}) []*auditV1.ServiceAccountDelegationInfo {
|
||||
principalId, principalEmail := extractSubjectAndEmail(token)
|
||||
|
||||
delegation := auditV1.ServiceAccountDelegationInfo{Authority: &auditV1.ServiceAccountDelegationInfo_IdpPrincipal_{IdpPrincipal: &auditV1.ServiceAccountDelegationInfo_IdpPrincipal{
|
||||
PrincipalId: principalId,
|
||||
PrincipalEmail: principalEmail,
|
||||
ServiceMetadata: nil,
|
||||
}}}
|
||||
|
||||
delegations := []*auditV1.ServiceAccountDelegationInfo{&delegation}
|
||||
nestedDelegations := extractServiceAccountDelegationInfo(token)
|
||||
if len(nestedDelegations) > 0 {
|
||||
return append(delegations, nestedDelegations...)
|
||||
} else {
|
||||
return delegations
|
||||
}
|
||||
}
|
||||
|
||||
func extractServiceAccountDelegationInfo(token map[string]interface{}) []*auditV1.ServiceAccountDelegationInfo {
|
||||
actor := token["act"]
|
||||
if actor != nil {
|
||||
actorMap, hasActorClaim := actor.(map[string]interface{})
|
||||
if hasActorClaim {
|
||||
return extractServiceAccountDelegationInfoDetails(actorMap)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func extractSubjectAndEmail(token map[string]interface{}) (string, string) {
|
||||
var principalEmail string
|
||||
principalId := fmt.Sprintf("%s", token["sub"])
|
||||
principalEmailRaw := token["email"]
|
||||
if principalEmailRaw == nil {
|
||||
principalEmail = "do-not-reply@stackit.cloud"
|
||||
} else {
|
||||
principalEmail = fmt.Sprintf("%s", principalEmailRaw)
|
||||
}
|
||||
return principalId, principalEmail
|
||||
}
|
||||
|
||||
// OperationNameFromUrlPath converts the request url path into an operation name.
|
||||
// UUIDs and query parameters are filtered out, slashes replaced by dots.
|
||||
// HTTP methods are added as suffix as follows:
|
||||
// - POST - create
|
||||
// - PUT - update
|
||||
// - PATCH - update
|
||||
// - DELETE - delete
|
||||
// - others - read
|
||||
func OperationNameFromUrlPath(path string, requestMethod string) string {
|
||||
queryIdx := strings.Index(path, "?")
|
||||
if queryIdx != -1 {
|
||||
path = path[:queryIdx]
|
||||
}
|
||||
path = strings.TrimPrefix(path, "/")
|
||||
path = strings.TrimSuffix(path, "/")
|
||||
split := strings.Split(path, "/")
|
||||
|
||||
operation := ""
|
||||
for _, part := range split {
|
||||
// skip uuids in path
|
||||
_, err := uuid.Parse(part)
|
||||
if err == nil {
|
||||
continue
|
||||
}
|
||||
operation = fmt.Sprintf("%s/%s", operation, part)
|
||||
}
|
||||
|
||||
operation = strings.ReplaceAll(operation, "/", ".")
|
||||
operation = strings.TrimPrefix(operation, ".")
|
||||
operation = strings.ToLower(operation)
|
||||
if len(operation) > 0 {
|
||||
method := utils.StringToHttpMethod(requestMethod)
|
||||
var action string
|
||||
switch method {
|
||||
case auditV1.AttributeContext_HTTP_METHOD_PUT:
|
||||
fallthrough
|
||||
case auditV1.AttributeContext_HTTP_METHOD_PATCH:
|
||||
action = "update"
|
||||
case auditV1.AttributeContext_HTTP_METHOD_POST:
|
||||
action = "create"
|
||||
case auditV1.AttributeContext_HTTP_METHOD_DELETE:
|
||||
action = "delete"
|
||||
default:
|
||||
action = "read"
|
||||
}
|
||||
operation = fmt.Sprintf("%s.%s", operation, action)
|
||||
}
|
||||
|
||||
return operation
|
||||
}
|
||||
|
||||
// OperationNameFromGrpcMethod converts the grpc path into an operation name.
|
||||
func OperationNameFromGrpcMethod(path string) string {
|
||||
operation := strings.TrimPrefix(path, "/")
|
||||
operation = strings.TrimSuffix(operation, "/")
|
||||
|
||||
operation = strings.ReplaceAll(operation, "/", ".")
|
||||
operation = strings.TrimPrefix(operation, ".")
|
||||
operation = strings.ToLower(operation)
|
||||
|
||||
return operation
|
||||
}
|
||||
|
||||
func GetObjectIdAndTypeFromUrlPath(path string) (
|
||||
string,
|
||||
*SingularType,
|
||||
*PluralType,
|
||||
error,
|
||||
) {
|
||||
|
||||
// Extract object id and type from request url
|
||||
objectTypeIdMatches := objectTypeIdPattern.FindStringSubmatch(path)
|
||||
if len(objectTypeIdMatches) > 0 {
|
||||
objectTypePlural := AsPluralType(objectTypeIdMatches[1])
|
||||
objectTypeSingular, err := objectTypePlural.AsSingularType()
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
}
|
||||
objectType := &objectTypeSingular
|
||||
objectId := objectTypeIdMatches[2]
|
||||
|
||||
return objectId, objectType, &objectTypePlural, nil
|
||||
}
|
||||
|
||||
return "", nil, nil, nil
|
||||
}
|
||||
|
||||
func ToArrayMap(input map[string]string) map[string][]string {
|
||||
output := map[string][]string{}
|
||||
for key, value := range input {
|
||||
output[key] = []string{value}
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
func StringAttributeFromMetadata(metadata map[string][]string, name string) string {
|
||||
var value = ""
|
||||
rawValue, hasAttribute := metadata[name]
|
||||
if hasAttribute && len(rawValue) > 0 {
|
||||
value = rawValue[0]
|
||||
}
|
||||
return value
|
||||
}
|
||||
988
audit/api/model_test.go
Normal file
988
audit/api/model_test.go
Normal file
|
|
@ -0,0 +1,988 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-core-platform/audit-go.git/gen/go/audit/v1"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
"google.golang.org/protobuf/types/known/wrapperspb"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type mockSpan struct {
|
||||
spanContext trace.SpanContext
|
||||
}
|
||||
|
||||
func (s *mockSpan) SpanContext() trace.SpanContext {
|
||||
return s.spanContext
|
||||
}
|
||||
|
||||
func Test_TraceParentFromSpan(t *testing.T) {
|
||||
tracer := otel.Tracer("test")
|
||||
|
||||
verifyTraceParent := func(traceParent string, span trace.Span, isSampled bool) {
|
||||
parts := strings.Split(traceParent, "-")
|
||||
assert.Equal(t, 4, len(parts))
|
||||
|
||||
// trace version
|
||||
assert.Equal(t, "00", parts[0])
|
||||
assert.Equal(t, span.SpanContext().TraceID().String(), parts[1])
|
||||
assert.Equal(t, span.SpanContext().SpanID().String(), parts[2])
|
||||
|
||||
var traceFlags = "00"
|
||||
if isSampled {
|
||||
traceFlags = "01"
|
||||
}
|
||||
assert.Equal(t, traceFlags, parts[3])
|
||||
}
|
||||
|
||||
t.Run("sampled", func(t *testing.T) {
|
||||
_, span := tracer.Start(context.Background(), "test")
|
||||
updatedFlags := span.SpanContext().TraceFlags().WithSampled(true)
|
||||
updatedContext := span.SpanContext().WithTraceFlags(updatedFlags)
|
||||
|
||||
mockedSpan := mockSpan{
|
||||
spanContext: updatedContext,
|
||||
}
|
||||
|
||||
traceParent := TraceParentFromSpan(&mockedSpan)
|
||||
verifyTraceParent(traceParent, span, true)
|
||||
})
|
||||
|
||||
t.Run("non-sampled", func(t *testing.T) {
|
||||
_, span := tracer.Start(context.Background(), "test")
|
||||
traceParent := TraceParentFromSpan(span)
|
||||
verifyTraceParent(traceParent, span, false)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func Test_GetCalledServiceNameFromRequest(t *testing.T) {
|
||||
|
||||
t.Run("localhost", func(t *testing.T) {
|
||||
request := Request{Host: "localhost:8080"}
|
||||
serviceName := GetCalledServiceNameFromRequest(&request, "resource-manager")
|
||||
assert.Equal(t, "resource-manager", serviceName)
|
||||
})
|
||||
|
||||
t.Run("cf", func(t *testing.T) {
|
||||
request := Request{Host: "stackit-resource-manager-go-dev.apps.01.cf.eu01.stackit.cloud"}
|
||||
serviceName := GetCalledServiceNameFromRequest(&request, "resource-manager")
|
||||
assert.Equal(t, "stackit-resource-manager-go-dev", serviceName)
|
||||
})
|
||||
|
||||
t.Run("cf invalid host", func(t *testing.T) {
|
||||
request := Request{Host: ""}
|
||||
serviceName := GetCalledServiceNameFromRequest(&request, "resource-manager")
|
||||
assert.Equal(t, "resource-manager", serviceName)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_NewPbInt64Value(t *testing.T) {
|
||||
|
||||
t.Run("nil", func(t *testing.T) {
|
||||
value := NewPbInt64Value(nil)
|
||||
assert.Nil(t, value)
|
||||
})
|
||||
|
||||
t.Run("value", func(t *testing.T) {
|
||||
var input int64 = 1
|
||||
value := NewPbInt64Value(&input)
|
||||
assert.Equal(t, wrapperspb.Int64Value{Value: 1}.Value, value.Value)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_NewResponseMetadata(t *testing.T) {
|
||||
|
||||
headers := make(map[string]string)
|
||||
headers["Content-Type"] = "application/json"
|
||||
responseTime := time.Now().UTC()
|
||||
responseItems := int64(10)
|
||||
responseSize := int64(100)
|
||||
|
||||
t.Run("no error", func(t *testing.T) {
|
||||
for code := 1; code < 400; code++ {
|
||||
metadata := NewResponseMetadata(code, &responseItems, &responseSize, headers, responseTime)
|
||||
assert.Equal(t, wrapperspb.Int32(int32(code)).Value, metadata.StatusCode.Value)
|
||||
assert.Nil(t, metadata.ErrorMessage)
|
||||
assert.Nil(t, metadata.ErrorDetails)
|
||||
assert.Equal(t, wrapperspb.Int64(responseItems), metadata.ResponseAttributes.NumResponseItems)
|
||||
assert.Equal(t, wrapperspb.Int64(responseSize), metadata.ResponseAttributes.Size)
|
||||
assert.Equal(t, timestamppb.New(responseTime), metadata.ResponseAttributes.Time)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("client error", func(t *testing.T) {
|
||||
for code := 400; code < 500; code++ {
|
||||
metadata := NewResponseMetadata(code, nil, nil, headers, responseTime)
|
||||
assert.Equal(t, wrapperspb.Int32(int32(code)).Value, metadata.StatusCode.Value)
|
||||
assert.Equal(t, "Client error", *metadata.ErrorMessage)
|
||||
assert.Nil(t, metadata.ErrorDetails)
|
||||
assert.Nil(t, metadata.ResponseAttributes.NumResponseItems)
|
||||
assert.Nil(t, metadata.ResponseAttributes.Size)
|
||||
assert.Equal(t, timestamppb.New(responseTime), metadata.ResponseAttributes.Time)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("server error", func(t *testing.T) {
|
||||
for code := 500; code < 600; code++ {
|
||||
metadata := NewResponseMetadata(code, nil, nil, headers, responseTime)
|
||||
assert.Equal(t, wrapperspb.Int32(int32(code)).Value, metadata.StatusCode.Value)
|
||||
assert.Equal(t, "Server error", *metadata.ErrorMessage)
|
||||
assert.Nil(t, metadata.ErrorDetails)
|
||||
assert.Nil(t, metadata.ResponseAttributes.NumResponseItems)
|
||||
assert.Nil(t, metadata.ResponseAttributes.Size)
|
||||
assert.Equal(t, timestamppb.New(responseTime), metadata.ResponseAttributes.Time)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func Test_NewRequestMetadata(t *testing.T) {
|
||||
|
||||
userAgent := "userAgent"
|
||||
requestHeaders := make(map[string][]string)
|
||||
requestHeaders["User-Agent"] = []string{userAgent}
|
||||
requestHeaders["Custom"] = []string{"customHeader"}
|
||||
|
||||
request := Request{
|
||||
Method: "GET",
|
||||
URL: RequestUrl{Path: "/audit/new", RawQuery: "topic=project"},
|
||||
Host: "localhost:8080",
|
||||
Proto: "HTTP/1.1",
|
||||
Scheme: "http",
|
||||
Header: requestHeaders,
|
||||
}
|
||||
|
||||
requestId := "requestId"
|
||||
requestScheme := "requestScheme"
|
||||
requestTime := time.Now().UTC()
|
||||
|
||||
audiences := []string{"audience"}
|
||||
authenticationPrincipal := "authenticationPrincipal"
|
||||
|
||||
claimMap := make(map[string]interface{})
|
||||
auditClaims, _ := structpb.NewStruct(claimMap)
|
||||
|
||||
clientIp := "clientIp"
|
||||
|
||||
filteredHeaders := make(map[string]string)
|
||||
filteredHeaders["Custom"] = "customHeader"
|
||||
filteredHeaders["User-Agent"] = userAgent
|
||||
|
||||
verifyRequestMetadata := func(requestMetadata *auditV1.RequestMetadata, requestId *string) {
|
||||
assert.Equal(t, clientIp, requestMetadata.CallerIp)
|
||||
assert.Equal(t, userAgent, requestMetadata.CallerSuppliedUserAgent)
|
||||
assert.NotNil(t, requestMetadata.RequestAttributes)
|
||||
|
||||
attributes := requestMetadata.RequestAttributes
|
||||
assert.Equal(t, requestId, attributes.Id)
|
||||
assert.Equal(t, filteredHeaders, attributes.Headers)
|
||||
assert.Equal(t, request.URL.Path, attributes.Path)
|
||||
assert.Equal(t, request.Host, attributes.Host)
|
||||
assert.Equal(t, requestScheme, attributes.Scheme)
|
||||
assert.Equal(t, timestamppb.New(requestTime), attributes.Time)
|
||||
assert.Equal(t, request.Proto, attributes.Protocol)
|
||||
assert.NotNil(t, attributes.Auth)
|
||||
|
||||
auth := attributes.Auth
|
||||
assert.Equal(t, authenticationPrincipal, auth.Principal)
|
||||
assert.Equal(t, audiences, auth.Audiences)
|
||||
assert.Equal(t, auditClaims, auth.Claims)
|
||||
}
|
||||
|
||||
t.Run("with query parameters", func(t *testing.T) {
|
||||
requestMetadata := NewRequestMetadata(
|
||||
&request,
|
||||
filteredHeaders,
|
||||
&requestId,
|
||||
requestScheme,
|
||||
requestTime,
|
||||
clientIp,
|
||||
authenticationPrincipal,
|
||||
audiences,
|
||||
auditClaims,
|
||||
)
|
||||
|
||||
verifyRequestMetadata(requestMetadata, &requestId)
|
||||
assert.Equal(t, "topic%3Dproject", *requestMetadata.RequestAttributes.Query)
|
||||
})
|
||||
|
||||
t.Run("without query parameters", func(t *testing.T) {
|
||||
request := Request{
|
||||
Method: "GET",
|
||||
URL: RequestUrl{Path: "/audit/new", RawQuery: ""},
|
||||
Host: "localhost:8080",
|
||||
Proto: "HTTP/1.1",
|
||||
Header: requestHeaders,
|
||||
}
|
||||
|
||||
requestMetadata := NewRequestMetadata(
|
||||
&request,
|
||||
filteredHeaders,
|
||||
&requestId,
|
||||
requestScheme,
|
||||
requestTime,
|
||||
clientIp,
|
||||
authenticationPrincipal,
|
||||
audiences,
|
||||
auditClaims,
|
||||
)
|
||||
|
||||
verifyRequestMetadata(requestMetadata, &requestId)
|
||||
assert.Nil(t, requestMetadata.RequestAttributes.Query)
|
||||
})
|
||||
|
||||
t.Run("without request id", func(t *testing.T) {
|
||||
request := Request{
|
||||
Method: "GET",
|
||||
URL: RequestUrl{Path: "/audit/new", RawQuery: "topic=project"},
|
||||
Host: "localhost:8080",
|
||||
Proto: "HTTP/1.1",
|
||||
Header: requestHeaders,
|
||||
}
|
||||
|
||||
requestMetadata := NewRequestMetadata(
|
||||
&request, filteredHeaders,
|
||||
nil,
|
||||
requestScheme,
|
||||
requestTime,
|
||||
clientIp,
|
||||
authenticationPrincipal,
|
||||
audiences,
|
||||
auditClaims,
|
||||
)
|
||||
verifyRequestMetadata(requestMetadata, nil)
|
||||
})
|
||||
|
||||
t.Run("various default http methods", func(t *testing.T) {
|
||||
httpMethods := []string{"GET", "HEAD", "POST", "PUT", "DELETE", "CONNECT", "OPTIONS", "TRACE", "PATCH"}
|
||||
for _, httpMethod := range httpMethods {
|
||||
request := Request{
|
||||
Method: httpMethod,
|
||||
URL: RequestUrl{Path: "/audit/new", RawQuery: "topic=project"},
|
||||
Host: "localhost:8080",
|
||||
Proto: "HTTP/1.1",
|
||||
Header: requestHeaders,
|
||||
}
|
||||
|
||||
requestMetadata := NewRequestMetadata(
|
||||
&request, filteredHeaders,
|
||||
&requestId, requestScheme,
|
||||
requestTime, clientIp,
|
||||
authenticationPrincipal,
|
||||
audiences,
|
||||
auditClaims,
|
||||
)
|
||||
|
||||
verifyRequestMetadata(requestMetadata, &requestId)
|
||||
expectedMethod := fmt.Sprintf("HTTP_METHOD_%s", httpMethod)
|
||||
assert.Equal(t, expectedMethod, requestMetadata.RequestAttributes.Method.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("unknown http method", func(t *testing.T) {
|
||||
request := Request{
|
||||
Method: "",
|
||||
URL: RequestUrl{Path: "/audit/new", RawQuery: "topic=project"},
|
||||
Host: "localhost:8080",
|
||||
Proto: "HTTP/1.1",
|
||||
Header: requestHeaders,
|
||||
}
|
||||
|
||||
requestMetadata := NewRequestMetadata(
|
||||
&request, filteredHeaders,
|
||||
&requestId, requestScheme,
|
||||
requestTime, clientIp,
|
||||
authenticationPrincipal,
|
||||
audiences,
|
||||
auditClaims,
|
||||
)
|
||||
|
||||
verifyRequestMetadata(requestMetadata, &requestId)
|
||||
assert.Equal(t,
|
||||
auditV1.AttributeContext_HTTP_METHOD_UNSPECIFIED.String(),
|
||||
requestMetadata.RequestAttributes.Method.String())
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
func Test_FilterAndMergeRequestHeaders(t *testing.T) {
|
||||
|
||||
t.Run("skip headers", func(t *testing.T) {
|
||||
headers := make(map[string][]string)
|
||||
headers["Authorization"] = []string{"ey..."}
|
||||
headers["B3"] = []string{"80f198ee56343ba864fe8b2a57d3eff7-e457b5a2e4d86bd1-1-05e3ac9a4f6e3b90"}
|
||||
|
||||
filteredHeaders := FilterAndMergeHeaders(headers)
|
||||
assert.Equal(t, 0, len(filteredHeaders))
|
||||
})
|
||||
|
||||
t.Run("skip headers by prefix", func(t *testing.T) {
|
||||
headers := make(map[string][]string)
|
||||
headers["X-Forwarded-Proto"] = []string{"https"}
|
||||
headers["Stackit-test"] = []string{"test"}
|
||||
|
||||
filteredHeaders := FilterAndMergeHeaders(headers)
|
||||
assert.Equal(t, 0, len(filteredHeaders))
|
||||
})
|
||||
|
||||
t.Run("merge headers", func(t *testing.T) {
|
||||
headers := make(map[string][]string)
|
||||
headers["Custom1"] = []string{"value1", "value2"}
|
||||
headers["Custom2"] = []string{"value3", "value4"}
|
||||
|
||||
filteredHeaders := FilterAndMergeHeaders(headers)
|
||||
assert.Equal(t, 2, len(filteredHeaders))
|
||||
assert.Equal(t, "value1,value2", filteredHeaders["Custom1"])
|
||||
assert.Equal(t, "value3,value4", filteredHeaders["Custom2"])
|
||||
})
|
||||
}
|
||||
|
||||
func Test_AuditAttributesFromAuthorizationHeader(t *testing.T) {
|
||||
|
||||
t.Run("basic token", func(t *testing.T) {
|
||||
headerValue := "Basic username:password"
|
||||
headers := make(map[string][]string)
|
||||
headers["Authorization"] = []string{headerValue}
|
||||
request := Request{Header: headers}
|
||||
|
||||
_, _, _, _, err := AuditAttributesFromAuthorizationHeader(&request)
|
||||
assert.ErrorIs(t, err, ErrTokenIsNotBearerToken)
|
||||
})
|
||||
|
||||
t.Run("invalid header value", func(t *testing.T) {
|
||||
headerValue := "a b c"
|
||||
headers := make(map[string][]string)
|
||||
headers["Authorization"] = []string{headerValue}
|
||||
request := Request{Header: headers}
|
||||
|
||||
_, _, _, _, err := AuditAttributesFromAuthorizationHeader(&request)
|
||||
assert.ErrorIs(t, err, ErrInvalidAuthorizationHeaderValue)
|
||||
})
|
||||
|
||||
t.Run("invalid token too many parts", func(t *testing.T) {
|
||||
headerValue := "Bearer a.b.c.d"
|
||||
headers := make(map[string][]string)
|
||||
headers["Authorization"] = []string{headerValue}
|
||||
request := Request{Header: headers}
|
||||
|
||||
_, _, _, _, err := AuditAttributesFromAuthorizationHeader(&request)
|
||||
assert.ErrorIs(t, err, ErrInvalidBearerToken)
|
||||
})
|
||||
|
||||
t.Run("invalid bearer token", func(t *testing.T) {
|
||||
headerValue := "Bearer a.b.c"
|
||||
headers := make(map[string][]string)
|
||||
headers["Authorization"] = []string{headerValue}
|
||||
request := Request{Header: headers}
|
||||
|
||||
_, _, _, _, err := AuditAttributesFromAuthorizationHeader(&request)
|
||||
assert.ErrorIs(t, err, ErrInvalidBearerToken)
|
||||
})
|
||||
|
||||
t.Run("client credentials token", func(t *testing.T) {
|
||||
headerValue := "Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjFlOGJlZjc1LWRmY2QtNGE3My1hMzkxLTU0YTdhZjU3YTdkNiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsic3RhY2tpdC1yZXNvdXJjZS1tYW5hZ2VyLWRldiJdLCJjbGllbnRfaWQiOiJzdGFja2l0LXJlc291cmNlLW1hbmFnZXItZGV2IiwiZXhwIjoxNzI0NDA1MzI2LCJpYXQiOjE3MjQ0MDQ0MjYsImlzcyI6Imh0dHBzOi8vYWNjb3VudHMuZGV2LnN0YWNraXQuY2xvdWQiLCJqdGkiOiJlNDZlYmEzOC1kZWRiLTQ1NDEtOTRmMy00OWY5N2E5MzRkNTgiLCJuYmYiOjE3MjQ0MDQ0MjYsInNjb3BlIjoidWFhLm5vbmUiLCJzdWIiOiJzdGFja2l0LXJlc291cmNlLW1hbmFnZXItZGV2In0.JP5Uy7AMdK4ukzQ6aOYzbVwEmq0Tp2ppQGRqGOhuVQgbqs6yJ33GKXo7RPsJVLw3FR7XAxENIVqNvzGotbDXr0NjBGdzyxIHzrOaUqM4w1iLzD1KF51dXFwkoigqDdD7Ze9eI_Uo3tSn8FwGLTSoO-ONQYpnceCiGut2Gc6VIL8HOLdh8dzlRENGQtgYd-3Y5zqpoLrsR2Bd-0sv15sF-5aI0CqcC8gE70JPImKf2u_IYI-TYMDNk86YSCtaYO5-alOrHXXWwgzSoH-r2s5qoOhPbei9myV_P4fdcKXxMqfap9hImXPUooVhpdUr1AabZw3MtW7rION8tJAiauhMQA"
|
||||
headers := make(map[string][]string)
|
||||
headers["Authorization"] = []string{headerValue}
|
||||
request := Request{Header: headers}
|
||||
|
||||
auditClaims, authenticationPrincipal, audiences, authenticationInfo, err :=
|
||||
AuditAttributesFromAuthorizationHeader(&request)
|
||||
|
||||
auditClaimsMap := auditClaims.AsMap()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "https://accounts.dev.stackit.cloud", auditClaimsMap["iss"])
|
||||
assert.Equal(t, "stackit-resource-manager-dev", auditClaimsMap["sub"])
|
||||
assert.Equal(t, []interface{}{"stackit-resource-manager-dev"}, auditClaimsMap["aud"].([]interface{}))
|
||||
assert.Equal(t, "e46eba38-dedb-4541-94f3-49f97a934d58", auditClaimsMap["jti"])
|
||||
|
||||
principal := fmt.Sprintf("%s/%s",
|
||||
url.QueryEscape("stackit-resource-manager-dev"),
|
||||
url.QueryEscape("https://accounts.dev.stackit.cloud"))
|
||||
assert.Equal(t, principal, authenticationPrincipal)
|
||||
|
||||
assert.Equal(t, []string{"stackit-resource-manager-dev"}, audiences)
|
||||
|
||||
assert.Equal(t, "stackit-resource-manager-dev", authenticationInfo.PrincipalId)
|
||||
assert.Equal(t, "do-not-reply@stackit.cloud", authenticationInfo.PrincipalEmail)
|
||||
|
||||
assert.Nil(t, authenticationInfo.ServiceAccountName)
|
||||
assert.Nil(t, authenticationInfo.ServiceAccountDelegationInfo)
|
||||
})
|
||||
|
||||
t.Run("service account access token", func(t *testing.T) {
|
||||
headerValue := "Bearer eyJraWQiOiJaVFJqWlRNek5tSmlNRGt3TldJMU5USTRZVGxpT1RjMllUWXlZVE16WldNIiwiYWxnIjoiUlM1MTIifQ.eyJzdWIiOiIxMGYzOGIwMS01MzRiLTQ3YmItYTAzYS1lMjk0Y2EyYmU0ZGUiLCJhdWQiOlsic3RhY2tpdCIsImFwaSJdLCJzdGFja2l0L3NlcnZpY2VhY2NvdW50L3Rva2VuLnNvdXJjZSI6ImxlZ2FjeSIsInN0YWNraXQvc2VydmljZWFjY291bnQvbmFtZXNwYWNlIjoiYXBpIiwic3RhY2tpdC9wcm9qZWN0L3Byb2plY3QuaWQiOiJkYWNjNzgzMC04NDNlLTRjNWUtODZmZi1hYTBmYjUxZDYzNmYiLCJhenAiOiJjZDk0ZjAxYS1kZjJlLTQ0NTYtOTAyZS00OGY1ZTU3ZjBiNjMiLCJpc3MiOiJzdGFja2l0L3NlcnZpY2VhY2NvdW50Iiwic3RhY2tpdC9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQudWlkIjoiMTBmMzhiMDEtNTM0Yi00N2JiLWEwM2EtZTI5NGNhMmJlNGRlIiwiZXhwIjoxNzIyNjY5MzQzLCJpYXQiOjE3MjI1ODI5NDMsImVtYWlsIjoibXktc2VydmljZS15aWZjOWUxQHNhLnN0YWNraXQuY2xvdWQiLCJqdGkiOiI4NGMzMGE0Ni0xMDAxLTQzNmYtODU5Zi04OWMwYmExOWJlMWUifQ.hb8X9VKc9xViHgNMyFHT9ePj_lyEwTV1D2es8E278WtoCJ9-4GPPQGjhcLGGrigjnvpRYV2LKzNqpQslerT5lFT_pHACsryaAE0ImYjmoe-nutA7BBpYuM_JN6pk5VIjVFLTqRKeIvFexPacqS2Vo3YoK1GvxPB8WPWBbGIsBtMl-PTm8OTwwzooBOoCRhhMR-E1lFbAymLsc1JI4yDQKLLomvhEopgmocCnQ-P1QkiKMqdkNxiD_YYLLYTOApg6d62BhqpH66ziqx493AStdZ8d5Kjvf3e1knDhaxVwNCghQj7lSo2kNAqZe__g2tiXpiZNTXBFJ_5HgQMLh67wng"
|
||||
headers := make(map[string][]string)
|
||||
headers["Authorization"] = []string{headerValue}
|
||||
request := Request{Header: headers}
|
||||
|
||||
auditClaims, authenticationPrincipal, audiences, authenticationInfo, err :=
|
||||
AuditAttributesFromAuthorizationHeader(&request)
|
||||
|
||||
auditClaimsMap := auditClaims.AsMap()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "stackit/serviceaccount", auditClaimsMap["iss"])
|
||||
assert.Equal(t, "10f38b01-534b-47bb-a03a-e294ca2be4de", auditClaimsMap["sub"])
|
||||
assert.Equal(t, []interface{}{"stackit", "api"}, auditClaimsMap["aud"].([]interface{}))
|
||||
assert.Equal(t, "84c30a46-1001-436f-859f-89c0ba19be1e", auditClaimsMap["jti"])
|
||||
|
||||
principal := fmt.Sprintf("%s/%s",
|
||||
url.QueryEscape("10f38b01-534b-47bb-a03a-e294ca2be4de"),
|
||||
url.QueryEscape("stackit/serviceaccount"))
|
||||
assert.Equal(t, principal, authenticationPrincipal)
|
||||
|
||||
assert.Equal(t, []string{"stackit", "api"}, audiences)
|
||||
|
||||
assert.Equal(t, "10f38b01-534b-47bb-a03a-e294ca2be4de", authenticationInfo.PrincipalId)
|
||||
assert.Equal(t, "my-service-yifc9e1@sa.stackit.cloud", authenticationInfo.PrincipalEmail)
|
||||
|
||||
assert.Equal(t,
|
||||
"projects/dacc7830-843e-4c5e-86ff-aa0fb51d636f/serviceAccounts/10f38b01-534b-47bb-a03a-e294ca2be4de",
|
||||
*authenticationInfo.ServiceAccountName)
|
||||
assert.Nil(t, authenticationInfo.ServiceAccountDelegationInfo)
|
||||
})
|
||||
|
||||
t.Run("impersonated token of access token", func(t *testing.T) {
|
||||
headerValue := "Bearer eyJraWQiOiJaVFJqWlRNek5tSmlNRGt3TldJMU5USTRZVGxpT1RjMllUWXlZVE16WldNIiwiYWxnIjoiUlM1MTIifQ.eyJzdWIiOiJmNDUwMDliMi02NDMzLTQzYzEtYjZjNy02MThjNDQzNTllNzEiLCJpc3MiOiJzdGFja2l0L3NlcnZpY2VhY2NvdW50IiwiYXVkIjpbInN0YWNraXQiLCJhcGkiXSwic3RhY2tpdC9zZXJ2aWNlYWNjb3VudC90b2tlbi5zb3VyY2UiOiJvYXV0aDIiLCJhY3QiOnsic3ViIjoiMDJhZWY1MTYtMzE3Zi00ZWMxLWExZGYtMWFjYmQ0ZDQ5ZmUzIn0sInN0YWNraXQvc2VydmljZWFjY291bnQvbmFtZXNwYWNlIjoiYXBpIiwic3RhY2tpdC9wcm9qZWN0L3Byb2plY3QuaWQiOiJkYWNjNzgzMC04NDNlLTRjNWUtODZmZi1hYTBmYjUxZDYzNmYiLCJhenAiOiIwMmFlZjUxNi0zMTdmLTRlYzEtYTFkZi0xYWNiZDRkNDlmZTMiLCJzdGFja2l0L3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC51aWQiOiJmNDUwMDliMi02NDMzLTQzYzEtYjZjNy02MThjNDQzNTllNzEiLCJleHAiOjE3MjQwNjI5MDcsImlhdCI6MTcyNDA1OTMwNywiZW1haWwiOiJzZXJ2aWNlLWFjY291bnQtMi10ajlzcnQxQHNhLnN0YWNraXQuY2xvdWQiLCJqdGkiOiIzNzU1NTE4My0wMWI5LTQyNzAtYmRjMS02OWI0ZmNmZDVlZTkifQ.auBvvsIesFMAlWOCPCPC77DrrHF7gSKZwKs_Zry5KFvu2bpZZC1BcSXOc8b9eh0SzANI9M9aGJBhOzOm39-ZZ5XOQ-6_y1aWuEenYQ6kT5D3GzCUTMDzSi1lcZ4IG5nFMa_AAlVEN_7AMv7LHGtz49bWLJnAgeTo1cvof-OgP4mCQ5O6E0iyAq-5u8V8NJL7HIZy7BDe4J1mjfYhwKagrN7QFWu4fhN4TNS7d922X_6V489BhjRFRYjLW_qDnv912JorbGRz_XwNy_dPA81EkdMyKE0BJUezguJUEKEG2_JEi9O64Flcoi6x8cFHYhaDuMMSLipzePaHdyk2lQtH7Q"
|
||||
headers := make(map[string][]string)
|
||||
headers["Authorization"] = []string{headerValue}
|
||||
request := Request{Header: headers}
|
||||
|
||||
auditClaims, authenticationPrincipal, audiences, authenticationInfo, err :=
|
||||
AuditAttributesFromAuthorizationHeader(&request)
|
||||
|
||||
auditClaimsMap := auditClaims.AsMap()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "stackit/serviceaccount", auditClaimsMap["iss"])
|
||||
assert.Equal(t, "f45009b2-6433-43c1-b6c7-618c44359e71", auditClaimsMap["sub"])
|
||||
assert.Equal(t, []interface{}{"stackit", "api"}, auditClaimsMap["aud"].([]interface{}))
|
||||
assert.Equal(t, "37555183-01b9-4270-bdc1-69b4fcfd5ee9", auditClaimsMap["jti"])
|
||||
|
||||
principal := fmt.Sprintf("%s/%s",
|
||||
url.QueryEscape("f45009b2-6433-43c1-b6c7-618c44359e71"),
|
||||
url.QueryEscape("stackit/serviceaccount"))
|
||||
assert.Equal(t, principal, authenticationPrincipal)
|
||||
|
||||
assert.Equal(t, []string{"stackit", "api"}, audiences)
|
||||
|
||||
assert.Equal(t, "f45009b2-6433-43c1-b6c7-618c44359e71", authenticationInfo.PrincipalId)
|
||||
assert.Equal(t, "service-account-2-tj9srt1@sa.stackit.cloud", authenticationInfo.PrincipalEmail)
|
||||
|
||||
assert.Equal(t,
|
||||
"projects/dacc7830-843e-4c5e-86ff-aa0fb51d636f/serviceAccounts/f45009b2-6433-43c1-b6c7-618c44359e71",
|
||||
*authenticationInfo.ServiceAccountName)
|
||||
assert.NotNil(t, authenticationInfo.ServiceAccountDelegationInfo)
|
||||
|
||||
serviceAccountDelegationInfo := authenticationInfo.ServiceAccountDelegationInfo
|
||||
assert.Equal(t, "02aef516-317f-4ec1-a1df-1acbd4d49fe3", serviceAccountDelegationInfo[0].GetIdpPrincipal().PrincipalId)
|
||||
assert.Equal(t, "do-not-reply@stackit.cloud", serviceAccountDelegationInfo[0].GetIdpPrincipal().PrincipalEmail)
|
||||
})
|
||||
|
||||
t.Run("impersonated token of impersonated access token", func(t *testing.T) {
|
||||
headerValue := "Bearer eyJraWQiOiJaVFJqWlRNek5tSmlNRGt3TldJMU5USTRZVGxpT1RjMllUWXlZVE16WldNIiwiYWxnIjoiUlM1MTIifQ.eyJzdWIiOiIxNzM0YjRiNi0xZDVlLTQ4MTktOWI1MC0yOTkxN2ExYjlhZDUiLCJpc3MiOiJzdGFja2l0L3NlcnZpY2VhY2NvdW50IiwiYXVkIjpbInN0YWNraXQiLCJhcGkiXSwic3RhY2tpdC9zZXJ2aWNlYWNjb3VudC90b2tlbi5zb3VyY2UiOiJvYXV0aDIiLCJhY3QiOnsic3ViIjoiZjQ1MDA5YjItNjQzMy00M2MxLWI2YzctNjE4YzQ0MzU5ZTcxIiwiYWN0Ijp7InN1YiI6IjAyYWVmNTE2LTMxN2YtNGVjMS1hMWRmLTFhY2JkNGQ0OWZlMyJ9fSwic3RhY2tpdC9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJhcGkiLCJzdGFja2l0L3Byb2plY3QvcHJvamVjdC5pZCI6ImRhY2M3ODMwLTg0M2UtNGM1ZS04NmZmLWFhMGZiNTFkNjM2ZiIsImF6cCI6ImY0NTAwOWIyLTY0MzMtNDNjMS1iNmM3LTYxOGM0NDM1OWU3MSIsInN0YWNraXQvc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6IjE3MzRiNGI2LTFkNWUtNDgxOS05YjUwLTI5OTE3YTFiOWFkNSIsImV4cCI6MTcyNDA2Mjk2MywiaWF0IjoxNzI0MDU5MzYzLCJlbWFpbCI6InNlcnZpY2UtYWNjb3VudC0zLWZnaHN4dzFAc2Euc3RhY2tpdC5jbG91ZCIsImp0aSI6IjFmN2YxZWZjLTMzNDktNDExYS1hNWQ3LTIyNTVlMGE1YThhZSJ9.c1ae17bAtyOdmwXQbK37W-NTyOxo7iER5aHS_C0fU1qKl2BjOz708GLjH-_vxx9eKPeYznfI21_xlTaAvuG4Aco9f5YDK7fooTVHnDaOSSggqcEaDzDPrNXhhKEDxotJeq9zRMVCEStcbirjTounnLbuULRbO5GSY5jo-8n2UKxSZ2j5G_SjFHajdJwmzwvOttp08tdL8ck1uDdgVNBfcm0VIdb6WmgrCIUq5rmoa-cRPkdEurNtIEgEB_9U0Xh-SpmmsvFsWWeNIKz0e_5RCIyJonm_wMkGmblGegemkYL76ypeMNXTQsly1RozDIePfzHuZOWbySHSCd-vKQa2kw"
|
||||
headers := make(map[string][]string)
|
||||
headers["Authorization"] = []string{headerValue}
|
||||
request := Request{Header: headers}
|
||||
|
||||
auditClaims, authenticationPrincipal, audiences, authenticationInfo, err :=
|
||||
AuditAttributesFromAuthorizationHeader(&request)
|
||||
|
||||
auditClaimsMap := auditClaims.AsMap()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "stackit/serviceaccount", auditClaimsMap["iss"])
|
||||
assert.Equal(t, "1734b4b6-1d5e-4819-9b50-29917a1b9ad5", auditClaimsMap["sub"])
|
||||
assert.Equal(t, []interface{}{"stackit", "api"}, auditClaimsMap["aud"].([]interface{}))
|
||||
assert.Equal(t, "1f7f1efc-3349-411a-a5d7-2255e0a5a8ae", auditClaimsMap["jti"])
|
||||
|
||||
principal := fmt.Sprintf("%s/%s",
|
||||
url.QueryEscape("1734b4b6-1d5e-4819-9b50-29917a1b9ad5"),
|
||||
url.QueryEscape("stackit/serviceaccount"))
|
||||
assert.Equal(t, principal, authenticationPrincipal)
|
||||
|
||||
assert.Equal(t, []string{"stackit", "api"}, audiences)
|
||||
|
||||
assert.Equal(t, "1734b4b6-1d5e-4819-9b50-29917a1b9ad5", authenticationInfo.PrincipalId)
|
||||
assert.Equal(t, "service-account-3-fghsxw1@sa.stackit.cloud", authenticationInfo.PrincipalEmail)
|
||||
|
||||
assert.Equal(t,
|
||||
"projects/dacc7830-843e-4c5e-86ff-aa0fb51d636f/serviceAccounts/1734b4b6-1d5e-4819-9b50-29917a1b9ad5",
|
||||
*authenticationInfo.ServiceAccountName)
|
||||
assert.NotNil(t, authenticationInfo.ServiceAccountDelegationInfo)
|
||||
|
||||
serviceAccountDelegationInfo := authenticationInfo.ServiceAccountDelegationInfo
|
||||
assert.Equal(t, "f45009b2-6433-43c1-b6c7-618c44359e71", serviceAccountDelegationInfo[0].GetIdpPrincipal().PrincipalId)
|
||||
assert.Equal(t, "do-not-reply@stackit.cloud", serviceAccountDelegationInfo[0].GetIdpPrincipal().PrincipalEmail)
|
||||
assert.Equal(t, "02aef516-317f-4ec1-a1df-1acbd4d49fe3", serviceAccountDelegationInfo[1].GetIdpPrincipal().PrincipalId)
|
||||
assert.Equal(t, "do-not-reply@stackit.cloud", serviceAccountDelegationInfo[1].GetIdpPrincipal().PrincipalEmail)
|
||||
})
|
||||
|
||||
t.Run("user token", func(t *testing.T) {
|
||||
headerValue := "Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjFlOGJlZjc1LWRmY2QtNGE3My1hMzkxLTU0YTdhZjU3YTdkNiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsic3RhY2tpdC1wb3J0YWwtbG9naW4tZGV2LWNsaWVudC1pZCJdLCJjbGllbnRfaWQiOiJzdGFja2l0LXBvcnRhbC1sb2dpbi1kZXYtY2xpZW50LWlkIiwiZW1haWwiOiJDaHJpc3RpYW4uU2NoYWlibGVAbm92YXRlYy1nbWJoLmRlIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImV4cCI6MTcyMjU5MDM2NywiaWF0IjoxNzIyNTg2NzY3LCJpc3MiOiJodHRwczovL2FjY291bnRzLmRldi5zdGFja2l0LmNsb3VkIiwianRpIjoiZDczYTY3YWMtZDFlYy00YjU1LTk5ZDQtZTk1MzI3NWYwMjJhIiwibmJmIjoxNzIyNTg2NzY3LCJzY29wZSI6Im9wZW5pZCBlbWFpbCIsInN1YiI6ImNkOTRmMDFhLWRmMmUtNDQ1Ni05MDJlLTQ4ZjVlNTdmMGI2MyJ9.ajhjYbC5l5g7un9NSheoAwBT83YcZM91rH4DJxPTDsB78HzIVrmaKTPrK3AI_E1THlD2Z3_ot9nFr_eX7XcwWp_ZBlataKmakdXlAmeb4xSMGNYefIfzV_3w9ZZAZ66yoeTrtn8dUx5ezquenCYpctB1NcccmK4U09V0kNcq9dFcfF3Sg9YilF3orUCR0ql1d9RnOs3EiFZuUpdBEkyoVsAdSh2P-PRbNViR_FgCcAJem97TsN5CQc9RlvKYe4sYKgqQoqa2GDVi9Niiw3fe1V8SCnROYcpkOzBBWdvuzFMBUjln3uOogYVOz93xkmImV6jidgyQ70fLt-eDUmZZfg"
|
||||
headers := make(map[string][]string)
|
||||
headers["Authorization"] = []string{headerValue}
|
||||
request := Request{Header: headers}
|
||||
|
||||
auditClaims, authenticationPrincipal, audiences, authenticationInfo, err :=
|
||||
AuditAttributesFromAuthorizationHeader(&request)
|
||||
|
||||
auditClaimsMap := auditClaims.AsMap()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "https://accounts.dev.stackit.cloud", auditClaimsMap["iss"])
|
||||
assert.Equal(t, "cd94f01a-df2e-4456-902e-48f5e57f0b63", auditClaimsMap["sub"])
|
||||
assert.Equal(t, []interface{}{"stackit-portal-login-dev-client-id"}, auditClaimsMap["aud"].([]interface{}))
|
||||
assert.Equal(t, "d73a67ac-d1ec-4b55-99d4-e953275f022a", auditClaimsMap["jti"])
|
||||
|
||||
principal := fmt.Sprintf("%s/%s",
|
||||
url.QueryEscape("cd94f01a-df2e-4456-902e-48f5e57f0b63"),
|
||||
url.QueryEscape("https://accounts.dev.stackit.cloud"))
|
||||
assert.Equal(t, principal, authenticationPrincipal)
|
||||
|
||||
assert.Equal(t, []string{"stackit-portal-login-dev-client-id"}, audiences)
|
||||
|
||||
assert.Equal(t, "cd94f01a-df2e-4456-902e-48f5e57f0b63", authenticationInfo.PrincipalId)
|
||||
assert.Equal(t, "Christian.Schaible@novatec-gmbh.de", authenticationInfo.PrincipalEmail)
|
||||
|
||||
assert.Nil(t, authenticationInfo.ServiceAccountName)
|
||||
assert.Nil(t, authenticationInfo.ServiceAccountDelegationInfo)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_NewAuditLogEntry(t *testing.T) {
|
||||
|
||||
t.Run("minimum attributes set", func(t *testing.T) {
|
||||
userAgent := "userAgent"
|
||||
requestHeaders := make(map[string][]string)
|
||||
requestHeaders["Authorization"] = []string{"Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjFlOGJlZjc1LWRmY2QtNGE3My1hMzkxLTU0YTdhZjU3YTdkNiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsic3RhY2tpdC1wb3J0YWwtbG9naW4tZGV2LWNsaWVudC1pZCJdLCJjbGllbnRfaWQiOiJzdGFja2l0LXBvcnRhbC1sb2dpbi1kZXYtY2xpZW50LWlkIiwiZW1haWwiOiJDaHJpc3RpYW4uU2NoYWlibGVAbm92YXRlYy1nbWJoLmRlIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImV4cCI6MTcyMjU5MDM2NywiaWF0IjoxNzIyNTg2NzY3LCJpc3MiOiJodHRwczovL2FjY291bnRzLmRldi5zdGFja2l0LmNsb3VkIiwianRpIjoiZDczYTY3YWMtZDFlYy00YjU1LTk5ZDQtZTk1MzI3NWYwMjJhIiwibmJmIjoxNzIyNTg2NzY3LCJzY29wZSI6Im9wZW5pZCBlbWFpbCIsInN1YiI6ImNkOTRmMDFhLWRmMmUtNDQ1Ni05MDJlLTQ4ZjVlNTdmMGI2MyJ9.ajhjYbC5l5g7un9NSheoAwBT83YcZM91rH4DJxPTDsB78HzIVrmaKTPrK3AI_E1THlD2Z3_ot9nFr_eX7XcwWp_ZBlataKmakdXlAmeb4xSMGNYefIfzV_3w9ZZAZ66yoeTrtn8dUx5ezquenCYpctB1NcccmK4U09V0kNcq9dFcfF3Sg9YilF3orUCR0ql1d9RnOs3EiFZuUpdBEkyoVsAdSh2P-PRbNViR_FgCcAJem97TsN5CQc9RlvKYe4sYKgqQoqa2GDVi9Niiw3fe1V8SCnROYcpkOzBBWdvuzFMBUjln3uOogYVOz93xkmImV6jidgyQ70fLt-eDUmZZfg"}
|
||||
requestHeaders["User-Agent"] = []string{userAgent}
|
||||
requestHeaders["Custom"] = []string{"customHeader"}
|
||||
|
||||
request := Request{
|
||||
Method: "GET",
|
||||
URL: RequestUrl{Path: "/audit/new"},
|
||||
Host: "localhost:8080",
|
||||
Proto: "HTTP/1.1",
|
||||
Scheme: "http",
|
||||
Header: requestHeaders,
|
||||
}
|
||||
|
||||
clientIp := "127.0.0.1"
|
||||
correlationId := uuid.NewString()
|
||||
auditRequest := AuditRequest{
|
||||
Request: &request,
|
||||
RequestClientIP: clientIp,
|
||||
RequestCorrelationId: &correlationId,
|
||||
RequestId: nil,
|
||||
RequestTime: nil,
|
||||
}
|
||||
|
||||
statusCode := 200
|
||||
auditResponse := AuditResponse{
|
||||
ResponseBodyBytes: nil,
|
||||
ResponseStatusCode: statusCode,
|
||||
ResponseHeaders: nil,
|
||||
ResponseNumItems: nil,
|
||||
ResponseTime: nil,
|
||||
}
|
||||
|
||||
objectId := uuid.NewString()
|
||||
logName := fmt.Sprintf("projects/%s/logs/%s", objectId, EventTypeAdminActivity)
|
||||
serviceName := "resource-manager"
|
||||
operationName := fmt.Sprintf("stackit.%s.v2.projects.updated", serviceName)
|
||||
resourceName := fmt.Sprintf("projects/%s", objectId)
|
||||
auditTime := time.Now().UTC()
|
||||
insertId := fmt.Sprintf("%d/%s/%s/%d", auditTime.UnixNano(), "eu01", "1", 1)
|
||||
|
||||
severity := auditV1.LogSeverity_LOG_SEVERITY_DEFAULT
|
||||
auditMetadata := AuditMetadata{
|
||||
AuditInsertId: insertId,
|
||||
AuditLabels: nil,
|
||||
AuditLogName: logName,
|
||||
AuditLogSeverity: severity,
|
||||
AuditOperationName: operationName,
|
||||
AuditPermission: nil,
|
||||
AuditPermissionGranted: nil,
|
||||
AuditResourceName: resourceName,
|
||||
AuditServiceName: serviceName,
|
||||
AuditTime: nil,
|
||||
}
|
||||
|
||||
logEntry, _ := NewAuditLogEntry(
|
||||
auditRequest,
|
||||
auditResponse,
|
||||
nil,
|
||||
auditMetadata,
|
||||
nil,
|
||||
nil)
|
||||
|
||||
assert.Equal(t, logName, logEntry.LogName)
|
||||
assert.Equal(t, insertId, logEntry.InsertId)
|
||||
assert.Equal(t, &correlationId, logEntry.CorrelationId)
|
||||
assert.Equal(t, severity, logEntry.Severity)
|
||||
assert.NoError(t, logEntry.Timestamp.CheckValid())
|
||||
assert.Nil(t, logEntry.Labels)
|
||||
assert.Nil(t, logEntry.TraceParent)
|
||||
assert.Nil(t, logEntry.TraceState)
|
||||
|
||||
payload := logEntry.ProtoPayload
|
||||
assert.NotNil(t, payload)
|
||||
assert.Equal(t, serviceName, payload.ServiceName)
|
||||
assert.Equal(t, operationName, payload.OperationName)
|
||||
assert.Equal(t, resourceName, payload.ResourceName)
|
||||
assert.Equal(t, int32(statusCode), payload.ResponseMetadata.StatusCode.Value)
|
||||
assert.Nil(t, payload.ResponseMetadata.ErrorMessage)
|
||||
assert.Nil(t, payload.ResponseMetadata.ErrorDetails)
|
||||
assert.Nil(t, payload.ResponseMetadata.ResponseAttributes.Size)
|
||||
assert.NotNil(t, payload.ResponseMetadata.ResponseAttributes.Time)
|
||||
assert.Nil(t, payload.ResponseMetadata.ResponseAttributes.NumResponseItems)
|
||||
assert.Nil(t, payload.ResponseMetadata.ResponseAttributes.Headers)
|
||||
|
||||
assert.Nil(t, payload.Request)
|
||||
assert.Nil(t, payload.Response)
|
||||
assert.Nil(t, payload.Metadata)
|
||||
|
||||
authenticationInfo := payload.AuthenticationInfo
|
||||
assert.NotNil(t, authenticationInfo)
|
||||
assert.Equal(t, "cd94f01a-df2e-4456-902e-48f5e57f0b63", authenticationInfo.PrincipalId)
|
||||
assert.Equal(t, "Christian.Schaible@novatec-gmbh.de", authenticationInfo.PrincipalEmail)
|
||||
assert.Nil(t, authenticationInfo.ServiceAccountName)
|
||||
assert.Nil(t, authenticationInfo.ServiceAccountDelegationInfo)
|
||||
|
||||
assert.Nil(t, payload.AuthorizationInfo)
|
||||
|
||||
requestMetadata := payload.RequestMetadata
|
||||
assert.NotNil(t, requestMetadata)
|
||||
assert.Equal(t, clientIp, requestMetadata.CallerIp)
|
||||
assert.Equal(t, userAgent, requestMetadata.CallerSuppliedUserAgent)
|
||||
|
||||
// Don't verify explicitly - trust on other unit test
|
||||
assert.NotNil(t, userAgent, requestMetadata.RequestAttributes)
|
||||
})
|
||||
|
||||
t.Run("all attributes set", func(t *testing.T) {
|
||||
userAgent := "userAgent"
|
||||
requestHeaders := make(map[string][]string)
|
||||
requestHeaders["Authorization"] = []string{"Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjFlOGJlZjc1LWRmY2QtNGE3My1hMzkxLTU0YTdhZjU3YTdkNiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsic3RhY2tpdC1wb3J0YWwtbG9naW4tZGV2LWNsaWVudC1pZCJdLCJjbGllbnRfaWQiOiJzdGFja2l0LXBvcnRhbC1sb2dpbi1kZXYtY2xpZW50LWlkIiwiZW1haWwiOiJDaHJpc3RpYW4uU2NoYWlibGVAbm92YXRlYy1nbWJoLmRlIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImV4cCI6MTcyMjU5MDM2NywiaWF0IjoxNzIyNTg2NzY3LCJpc3MiOiJodHRwczovL2FjY291bnRzLmRldi5zdGFja2l0LmNsb3VkIiwianRpIjoiZDczYTY3YWMtZDFlYy00YjU1LTk5ZDQtZTk1MzI3NWYwMjJhIiwibmJmIjoxNzIyNTg2NzY3LCJzY29wZSI6Im9wZW5pZCBlbWFpbCIsInN1YiI6ImNkOTRmMDFhLWRmMmUtNDQ1Ni05MDJlLTQ4ZjVlNTdmMGI2MyJ9.ajhjYbC5l5g7un9NSheoAwBT83YcZM91rH4DJxPTDsB78HzIVrmaKTPrK3AI_E1THlD2Z3_ot9nFr_eX7XcwWp_ZBlataKmakdXlAmeb4xSMGNYefIfzV_3w9ZZAZ66yoeTrtn8dUx5ezquenCYpctB1NcccmK4U09V0kNcq9dFcfF3Sg9YilF3orUCR0ql1d9RnOs3EiFZuUpdBEkyoVsAdSh2P-PRbNViR_FgCcAJem97TsN5CQc9RlvKYe4sYKgqQoqa2GDVi9Niiw3fe1V8SCnROYcpkOzBBWdvuzFMBUjln3uOogYVOz93xkmImV6jidgyQ70fLt-eDUmZZfg"}
|
||||
requestHeaders["User-Agent"] = []string{userAgent}
|
||||
requestHeaders["Custom"] = []string{"customHeader"}
|
||||
|
||||
requestBody := make(map[string]interface{})
|
||||
requestBody["key"] = "request"
|
||||
requestBodyBytes, _ := json.Marshal(requestBody)
|
||||
request := Request{
|
||||
Method: "GET",
|
||||
URL: RequestUrl{Path: "/audit/new", RawQuery: "topic=project"},
|
||||
Host: "localhost:8080",
|
||||
Proto: "HTTP/1.1",
|
||||
Scheme: "http",
|
||||
Header: requestHeaders,
|
||||
Body: &requestBodyBytes,
|
||||
}
|
||||
|
||||
clientIp := "127.0.0.1"
|
||||
correlationId := uuid.NewString()
|
||||
requestId := uuid.NewString()
|
||||
requestTime := time.Now().UTC()
|
||||
auditRequest := AuditRequest{
|
||||
Request: &request,
|
||||
RequestClientIP: clientIp,
|
||||
RequestCorrelationId: &correlationId,
|
||||
RequestId: &requestId,
|
||||
RequestTime: &requestTime,
|
||||
}
|
||||
|
||||
response := make(map[string]interface{})
|
||||
response["key"] = "value"
|
||||
responseBody, _ := json.Marshal(response)
|
||||
responseHeader := http.Header{}
|
||||
responseHeader.Set("Content-Type", "application/json")
|
||||
responseHeaderMap := make(map[string]string)
|
||||
responseHeaderMap["Content-Type"] = "application/json"
|
||||
responseNumItems := int64(1)
|
||||
responseStatusCode := 400
|
||||
responseTime := time.Now().UTC()
|
||||
|
||||
auditResponse := AuditResponse{
|
||||
ResponseBodyBytes: &responseBody,
|
||||
ResponseStatusCode: responseStatusCode,
|
||||
ResponseHeaders: responseHeader,
|
||||
ResponseNumItems: &responseNumItems,
|
||||
ResponseTime: &responseTime,
|
||||
}
|
||||
|
||||
auditTime := time.Now().UTC()
|
||||
|
||||
objectId := uuid.NewString()
|
||||
logName := fmt.Sprintf("projects/%s/logs/%s", objectId, EventTypeAdminActivity)
|
||||
serviceName := "resource-manager"
|
||||
operationName := fmt.Sprintf("stackit.%s.v2.projects.updated", serviceName)
|
||||
resourceName := fmt.Sprintf("projects/%s", objectId)
|
||||
insertId := fmt.Sprintf("%d/%s/%s/%d", auditTime.UnixNano(), "eu01", "1", 1)
|
||||
permission := "resource-manager.project.edit"
|
||||
permissionGranted := true
|
||||
labels := make(map[string]string)
|
||||
labels["label"] = "value"
|
||||
|
||||
severity := auditV1.LogSeverity_LOG_SEVERITY_DEFAULT
|
||||
|
||||
auditMetadata := AuditMetadata{
|
||||
AuditInsertId: insertId,
|
||||
AuditLabels: &labels,
|
||||
AuditLogName: logName,
|
||||
AuditLogSeverity: severity,
|
||||
AuditOperationName: operationName,
|
||||
AuditPermission: &permission,
|
||||
AuditPermissionGranted: &permissionGranted,
|
||||
AuditResourceName: resourceName,
|
||||
AuditServiceName: serviceName,
|
||||
AuditTime: &auditTime,
|
||||
}
|
||||
|
||||
eventMetadata := map[string]interface{}{"key": "value"}
|
||||
|
||||
traceParent := "traceParent"
|
||||
traceState := "traceState"
|
||||
logEntry, _ := NewAuditLogEntry(
|
||||
auditRequest,
|
||||
auditResponse,
|
||||
&eventMetadata,
|
||||
auditMetadata,
|
||||
&traceParent,
|
||||
&traceState)
|
||||
|
||||
assert.Equal(t, logName, logEntry.LogName)
|
||||
assert.Equal(t, insertId, logEntry.InsertId)
|
||||
assert.Equal(t, labels, logEntry.Labels)
|
||||
assert.Equal(t, correlationId, *logEntry.CorrelationId)
|
||||
assert.Equal(t, timestamppb.New(auditTime), logEntry.Timestamp)
|
||||
assert.Equal(t, severity, logEntry.Severity)
|
||||
assert.Equal(t, &traceParent, logEntry.TraceParent)
|
||||
assert.Equal(t, &traceState, logEntry.TraceState)
|
||||
assert.NotNil(t, logEntry.ProtoPayload)
|
||||
|
||||
payload := logEntry.ProtoPayload
|
||||
assert.NotNil(t, payload)
|
||||
assert.Equal(t, serviceName, payload.ServiceName)
|
||||
assert.Equal(t, operationName, payload.OperationName)
|
||||
assert.Equal(t, resourceName, payload.ResourceName)
|
||||
assert.Equal(t, int32(responseStatusCode), payload.ResponseMetadata.StatusCode.Value)
|
||||
assert.Equal(t, "Client error", *payload.ResponseMetadata.ErrorMessage)
|
||||
assert.Nil(t, payload.ResponseMetadata.ErrorDetails)
|
||||
assert.Equal(t, wrapperspb.Int64(int64(len(responseBody))), payload.ResponseMetadata.ResponseAttributes.Size)
|
||||
assert.Equal(t, timestamppb.New(responseTime), payload.ResponseMetadata.ResponseAttributes.Time)
|
||||
assert.Equal(t, wrapperspb.Int64(responseNumItems), payload.ResponseMetadata.ResponseAttributes.NumResponseItems)
|
||||
assert.Equal(t, responseHeaderMap, payload.ResponseMetadata.ResponseAttributes.Headers)
|
||||
|
||||
expectedRequestBody, _ := structpb.NewStruct(requestBody)
|
||||
assert.Equal(t, expectedRequestBody, payload.Request)
|
||||
expectedResponseBody, _ := structpb.NewStruct(response)
|
||||
assert.Equal(t, expectedResponseBody, payload.Response)
|
||||
expectedEventMetadata, _ := structpb.NewStruct(eventMetadata)
|
||||
assert.Equal(t, expectedEventMetadata, payload.Metadata)
|
||||
|
||||
authenticationInfo := payload.AuthenticationInfo
|
||||
assert.NotNil(t, authenticationInfo)
|
||||
assert.Equal(t, "cd94f01a-df2e-4456-902e-48f5e57f0b63", authenticationInfo.PrincipalId)
|
||||
assert.Equal(t, "Christian.Schaible@novatec-gmbh.de", authenticationInfo.PrincipalEmail)
|
||||
assert.Nil(t, authenticationInfo.ServiceAccountName)
|
||||
assert.Nil(t, authenticationInfo.ServiceAccountDelegationInfo)
|
||||
|
||||
authorizationInfo := payload.AuthorizationInfo
|
||||
assert.NotNil(t, authorizationInfo)
|
||||
assert.Equal(t, 1, len(authorizationInfo))
|
||||
assert.Equal(t, permission, *authorizationInfo[0].Permission)
|
||||
assert.Equal(t, permissionGranted, *authorizationInfo[0].Granted)
|
||||
assert.Equal(t, resourceName, authorizationInfo[0].Resource)
|
||||
|
||||
requestMetadata := payload.RequestMetadata
|
||||
assert.NotNil(t, requestMetadata)
|
||||
assert.Equal(t, clientIp, requestMetadata.CallerIp)
|
||||
assert.Equal(t, userAgent, requestMetadata.CallerSuppliedUserAgent)
|
||||
|
||||
// Don't verify explicitly - trust on other unit test
|
||||
assert.NotNil(t, userAgent, requestMetadata.RequestAttributes)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_NewInsertId(t *testing.T) {
|
||||
insertTime := time.Now().UTC()
|
||||
location := "eu01"
|
||||
workerId := uuid.NewString()
|
||||
var eventSequenceNumber uint64 = 1
|
||||
|
||||
insertId := NewInsertId(insertTime, location, workerId, eventSequenceNumber)
|
||||
expectedId := fmt.Sprintf("%d/%s/%s/%d", insertTime.UnixNano(), location, workerId, eventSequenceNumber)
|
||||
assert.Equal(t, expectedId, insertId)
|
||||
}
|
||||
|
||||
func Test_NewNewAuditRoutingIdentifier(t *testing.T) {
|
||||
objectId := uuid.NewString()
|
||||
singularType := SingularTypeProject
|
||||
|
||||
routingIdentifier := NewAuditRoutingIdentifier(objectId, singularType)
|
||||
assert.Equal(t, objectId, routingIdentifier.Identifier)
|
||||
assert.Equal(t, singularType, routingIdentifier.Type)
|
||||
}
|
||||
|
||||
func Test_OperationNameFromUrlPath(t *testing.T) {
|
||||
|
||||
t.Run("empty path", func(t *testing.T) {
|
||||
operationName := OperationNameFromUrlPath("", "GET")
|
||||
assert.Equal(t, "", operationName)
|
||||
})
|
||||
|
||||
t.Run("root path", func(t *testing.T) {
|
||||
operationName := OperationNameFromUrlPath("/", "GET")
|
||||
assert.Equal(t, "", operationName)
|
||||
})
|
||||
|
||||
t.Run("path without version", func(t *testing.T) {
|
||||
operationName := OperationNameFromUrlPath("/projects", "GET")
|
||||
assert.Equal(t, "projects.read", operationName)
|
||||
})
|
||||
|
||||
t.Run("path with uuid without version", func(t *testing.T) {
|
||||
operationName := OperationNameFromUrlPath("/projects/ac51bbd2-cb23-441b-a2ee-5393189695aa", "GET")
|
||||
assert.Equal(t, "projects.read", operationName)
|
||||
})
|
||||
|
||||
t.Run("path with uuid and version", func(t *testing.T) {
|
||||
operationName := OperationNameFromUrlPath("/v2/projects/ac51bbd2-cb23-441b-a2ee-5393189695aa", "GET")
|
||||
assert.Equal(t, "v2.projects.read", operationName)
|
||||
})
|
||||
|
||||
t.Run("concatenated path", func(t *testing.T) {
|
||||
operationName := OperationNameFromUrlPath("/v2/organizations/ac51bbd2-cb23-441b-a2ee-5393189695aa/folders/167fc176-9d8e-477b-a56c-b50d7b26adcf/projects/0a2a4f9b-4e67-4562-ad02-c2d200e05aa6/audit/policy", "GET")
|
||||
assert.Equal(t, "v2.organizations.folders.projects.audit.policy.read", operationName)
|
||||
})
|
||||
|
||||
t.Run("path with query params", func(t *testing.T) {
|
||||
operationName := OperationNameFromUrlPath("/v2/organizations/ac51bbd2-cb23-441b-a2ee-5393189695aa/audit/policy?since=2024-08-27", "GET")
|
||||
assert.Equal(t, "v2.organizations.audit.policy.read", operationName)
|
||||
})
|
||||
|
||||
t.Run("path trailing slash", func(t *testing.T) {
|
||||
operationName := OperationNameFromUrlPath("/projects/ac51bbd2-cb23-441b-a2ee-5393189695aa/", "GET")
|
||||
assert.Equal(t, "projects.read", operationName)
|
||||
})
|
||||
|
||||
t.Run("path trailing slash and query params", func(t *testing.T) {
|
||||
operationName := OperationNameFromUrlPath("/projects/ac51bbd2-cb23-441b-a2ee-5393189695aa/?changeDate=2024-10-13", "GET")
|
||||
assert.Equal(t, "projects.read", operationName)
|
||||
})
|
||||
|
||||
t.Run("http method post", func(t *testing.T) {
|
||||
operationName := OperationNameFromUrlPath("/projects", "POST")
|
||||
assert.Equal(t, "projects.create", operationName)
|
||||
})
|
||||
|
||||
t.Run("http method put", func(t *testing.T) {
|
||||
operationName := OperationNameFromUrlPath("/projects", "PUT")
|
||||
assert.Equal(t, "projects.update", operationName)
|
||||
})
|
||||
|
||||
t.Run("http method patch", func(t *testing.T) {
|
||||
operationName := OperationNameFromUrlPath("/projects", "PATCH")
|
||||
assert.Equal(t, "projects.update", operationName)
|
||||
})
|
||||
|
||||
t.Run("http method delete", func(t *testing.T) {
|
||||
operationName := OperationNameFromUrlPath("/projects", "DELETE")
|
||||
assert.Equal(t, "projects.delete", operationName)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_OperationNameFromGrpcMethod(t *testing.T) {
|
||||
|
||||
t.Run("empty path", func(t *testing.T) {
|
||||
operationName := OperationNameFromGrpcMethod("")
|
||||
assert.Equal(t, "", operationName)
|
||||
})
|
||||
|
||||
t.Run("root path", func(t *testing.T) {
|
||||
operationName := OperationNameFromGrpcMethod("/")
|
||||
assert.Equal(t, "", operationName)
|
||||
})
|
||||
|
||||
t.Run("path without version", func(t *testing.T) {
|
||||
operationName := OperationNameFromGrpcMethod("/example.ExampleService/ManualAuditEvent")
|
||||
assert.Equal(t, "example.exampleservice.manualauditevent", operationName)
|
||||
})
|
||||
|
||||
t.Run("path with version", func(t *testing.T) {
|
||||
operationName := OperationNameFromGrpcMethod("/example.v1.ExampleService/ManualAuditEvent")
|
||||
assert.Equal(t, "example.v1.exampleservice.manualauditevent", operationName)
|
||||
})
|
||||
|
||||
t.Run("path trailing slash", func(t *testing.T) {
|
||||
operationName := OperationNameFromGrpcMethod("/example.v1.ExampleService/ManualAuditEvent/")
|
||||
assert.Equal(t, "example.v1.exampleservice.manualauditevent", operationName)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_GetObjectIdAndTypeFromUrlPath(t *testing.T) {
|
||||
|
||||
t.Run("object id and type not in url", func(t *testing.T) {
|
||||
objectId, singularType, pluralType, err := GetObjectIdAndTypeFromUrlPath("/v2/projects/audit")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", objectId)
|
||||
assert.Nil(t, singularType)
|
||||
assert.Nil(t, pluralType)
|
||||
})
|
||||
|
||||
t.Run("object id and type in url", func(t *testing.T) {
|
||||
objectId, singularType, pluralType, err := GetObjectIdAndTypeFromUrlPath("/v2/projects/f17d4064-9b65-4334-b6a7-8fed96340124")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "f17d4064-9b65-4334-b6a7-8fed96340124", objectId)
|
||||
assert.Equal(t, SingularTypeProject, *singularType)
|
||||
assert.Equal(t, PluralTypeProject, *pluralType)
|
||||
})
|
||||
|
||||
t.Run("multiple object ids and types in url", func(t *testing.T) {
|
||||
objectId, singularType, pluralType, err := GetObjectIdAndTypeFromUrlPath("/v2/organization/8ee58bec-d496-4bb9-af8d-72fda4d78b6b/projects/f17d4064-9b65-4334-b6a7-8fed96340124")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "f17d4064-9b65-4334-b6a7-8fed96340124", objectId)
|
||||
assert.Equal(t, SingularTypeProject, *singularType)
|
||||
assert.Equal(t, PluralTypeProject, *pluralType)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_ToArrayMap(t *testing.T) {
|
||||
|
||||
t.Run("empty map", func(t *testing.T) {
|
||||
result := ToArrayMap(map[string]string{})
|
||||
assert.Equal(t, map[string][]string{}, result)
|
||||
})
|
||||
|
||||
t.Run("empty map", func(t *testing.T) {
|
||||
result := ToArrayMap(map[string]string{"key1": "value1", "key2": "value2"})
|
||||
assert.Equal(t, map[string][]string{
|
||||
"key1": {"value1"},
|
||||
"key2": {"value2"},
|
||||
}, result)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_StringAttributeFromMetadata(t *testing.T) {
|
||||
|
||||
metadata := map[string][]string{"key1": {"value1"}, "key2": {"value2"}}
|
||||
|
||||
t.Run("not found", func(t *testing.T) {
|
||||
attribute := StringAttributeFromMetadata(metadata, "key3")
|
||||
assert.Equal(t, "", attribute)
|
||||
})
|
||||
|
||||
t.Run("found", func(t *testing.T) {
|
||||
attribute := StringAttributeFromMetadata(metadata, "key2")
|
||||
assert.Equal(t, "value2", attribute)
|
||||
})
|
||||
}
|
||||
45
audit/utils/sequence_generator.go
Normal file
45
audit/utils/sequence_generator.go
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
package utils
|
||||
|
||||
import "sync"
|
||||
|
||||
// SequenceNumberGenerator can be used to generate increasing numbers.
|
||||
type SequenceNumberGenerator interface {
|
||||
|
||||
// Next returns the next number
|
||||
Next() uint64
|
||||
|
||||
// Revert can be used to decrease the number (e.g. in case of an error)
|
||||
Revert()
|
||||
}
|
||||
|
||||
// DefaultSequenceNumberGenerator is a mutex protected implementation of SequenceNumberGenerator
|
||||
type DefaultSequenceNumberGenerator struct {
|
||||
sequenceNumber uint64
|
||||
sequenceNumberLock sync.Mutex
|
||||
}
|
||||
|
||||
// NewDefaultSequenceNumberGenerator returns an instance of DefaultSequenceNumberGenerator as pointer
|
||||
// of SequenceNumberGenerator.
|
||||
func NewDefaultSequenceNumberGenerator() *SequenceNumberGenerator {
|
||||
var generator SequenceNumberGenerator = &DefaultSequenceNumberGenerator{
|
||||
sequenceNumber: 0,
|
||||
sequenceNumberLock: sync.Mutex{},
|
||||
}
|
||||
return &generator
|
||||
}
|
||||
|
||||
// Next implements SequenceNumberGenerator.Next
|
||||
func (g *DefaultSequenceNumberGenerator) Next() uint64 {
|
||||
g.sequenceNumberLock.Lock()
|
||||
defer g.sequenceNumberLock.Unlock()
|
||||
next := g.sequenceNumber
|
||||
g.sequenceNumber += 1
|
||||
return next
|
||||
}
|
||||
|
||||
// Revert implements SequenceNumberGenerator.Revert
|
||||
func (g *DefaultSequenceNumberGenerator) Revert() {
|
||||
g.sequenceNumberLock.Lock()
|
||||
defer g.sequenceNumberLock.Unlock()
|
||||
g.sequenceNumber -= 1
|
||||
}
|
||||
22
audit/utils/sequence_generator_test.go
Normal file
22
audit/utils/sequence_generator_test.go
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_DefaultSequenceNumberGenerator(t *testing.T) {
|
||||
|
||||
t.Run("next", func(t *testing.T) {
|
||||
var sequenceGenerator = NewDefaultSequenceNumberGenerator()
|
||||
assert.Equal(t, uint64(0), (*sequenceGenerator).Next())
|
||||
})
|
||||
|
||||
t.Run("revert", func(t *testing.T) {
|
||||
var sequenceGenerator = NewDefaultSequenceNumberGenerator()
|
||||
assert.Equal(t, uint64(0), (*sequenceGenerator).Next())
|
||||
assert.Equal(t, uint64(1), (*sequenceGenerator).Next())
|
||||
(*sequenceGenerator).Revert()
|
||||
assert.Equal(t, uint64(1), (*sequenceGenerator).Next())
|
||||
})
|
||||
}
|
||||
4
go.mod
4
go.mod
|
|
@ -10,6 +10,8 @@ require (
|
|||
github.com/hashicorp/go-retryablehttp v0.7.7
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/testcontainers/testcontainers-go v0.33.0
|
||||
go.opentelemetry.io/otel v1.24.0
|
||||
go.opentelemetry.io/otel/trace v1.24.0
|
||||
google.golang.org/protobuf v1.34.2
|
||||
)
|
||||
|
||||
|
|
@ -58,9 +60,7 @@ require (
|
|||
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.3 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
|
||||
go.opentelemetry.io/otel v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.24.0 // indirect
|
||||
golang.org/x/crypto v0.24.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect
|
||||
golang.org/x/net v0.26.0 // indirect
|
||||
|
|
|
|||
Loading…
Reference in a new issue