package api import ( auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1" "encoding/json" "fmt" "github.com/google/uuid" "github.com/stretchr/testify/assert" "google.golang.org/protobuf/encoding/protojson" "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" "testing" "time" ) func Test_GetCalledServiceNameFromRequest(t *testing.T) { t.Run("request is nil", func(t *testing.T) { serviceName := GetCalledServiceNameFromRequest(nil, "resource-manager") assert.Equal(t, "resource-manager", serviceName) }) t.Run("localhost", func(t *testing.T) { request := ApiRequest{Host: "localhost:8080"} serviceName := GetCalledServiceNameFromRequest(&request, "resource-manager") assert.Equal(t, "resource-manager", serviceName) }) t.Run("cf", func(t *testing.T) { request := ApiRequest{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 := ApiRequest{Host: ""} serviceName := GetCalledServiceNameFromRequest(&request, "resource-manager") assert.Equal(t, "resource-manager", serviceName) }) t.Run("ip", func(t *testing.T) { request := ApiRequest{Host: "127.0.0.1"} serviceName := GetCalledServiceNameFromRequest(&request, "resource-manager") assert.Equal(t, "resource-manager", serviceName) }, ) t.Run("ip short", func(t *testing.T) { request := ApiRequest{Host: "::1"} 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"} queryString := "topic=project" request := ApiRequest{ Method: "GET", URL: RequestUrl{Path: "/audit/new", RawQuery: &queryString}, 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 := ApiRequest{ Method: "GET", URL: RequestUrl{Path: "/audit/new"}, 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("with empty query parameters", func(t *testing.T) { emptyQuery := "" request := ApiRequest{ Method: "GET", URL: RequestUrl{Path: "/audit/new", RawQuery: &emptyQuery}, 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 := ApiRequest{ Method: "GET", URL: RequestUrl{Path: "/audit/new", RawQuery: &queryString}, 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 := ApiRequest{ Method: httpMethod, URL: RequestUrl{Path: "/audit/new", RawQuery: &queryString}, 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 := ApiRequest{ Method: "", URL: RequestUrl{Path: "/audit/new", RawQuery: &queryString}, 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["authorization"] = []string{"ey..."} headers["B3"] = []string{"80f198ee56343ba864fe8b2a57d3eff7-e457b5a2e4d86bd1-1-05e3ac9a4f6e3b90"} headers["b3"] = []string{"80f198ee56343ba864fe8b2a57d3eff7-e457b5a2e4d86bd1-1-05e3ac9a4f6e3b90"} headers["Host"] = []string{"localhost:9090"} headers["host"] = []string{"localhost:9090"} headers[":authority"] = []string{"localhost:9090"} 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"]) }) t.Run("skip merge headers mixed", func(t *testing.T) { headers := make(map[string][]string) headers["Custom1"] = []string{"value1", "value2"} headers["Custom2"] = []string{"value3"} headers["STACKIT-MIXED"] = []string{"test"} filteredHeaders := FilterAndMergeHeaders(headers) assert.Equal(t, 2, len(filteredHeaders)) assert.Equal(t, "value1,value2", filteredHeaders["Custom1"]) assert.Equal(t, "value3", filteredHeaders["Custom2"]) }) t.Run("Keep empty and blank header values", func(t *testing.T) { headers := make(map[string][]string) headers["empty"] = []string{""} headers["blank"] = []string{" "} filteredHeaders := FilterAndMergeHeaders(headers) assert.Equal(t, 2, len(filteredHeaders)) assert.Equal(t, "", filteredHeaders["empty"]) assert.Equal(t, " ", filteredHeaders["blank"]) }) } 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 := ApiRequest{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 := ApiRequest{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 := ApiRequest{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 := ApiRequest{Header: headers} _, _, _, _, err := AuditAttributesFromAuthorizationHeader(&request) assert.ErrorIs(t, err, ErrInvalidBearerToken) }) t.Run("client credentials token", func(t *testing.T) { headers := make(map[string][]string) headers["Authorization"] = []string{clientCredentialsToken} request := ApiRequest{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) { headers := make(map[string][]string) headers["Authorization"] = []string{serviceAccountToken} request := ApiRequest{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/service-accounts/10f38b01-534b-47bb-a03a-e294ca2be4de", *authenticationInfo.ServiceAccountName) assert.Nil(t, authenticationInfo.ServiceAccountDelegationInfo) }) t.Run("impersonated token of access token", func(t *testing.T) { headers := make(map[string][]string) headers["Authorization"] = []string{serviceAccountTokenImpersonated} request := ApiRequest{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/service-accounts/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) { headers := make(map[string][]string) headers["Authorization"] = []string{serviceAccountTokenRepeatedlyImpersonated} request := ApiRequest{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/service-accounts/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) { headers := make(map[string][]string) headers["Authorization"] = []string{userToken} request := ApiRequest{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{userToken} requestHeaders["User-Agent"] = []string{userAgent} requestHeaders["Custom"] = []string{"customHeader"} request := ApiRequest{ 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) 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) 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{userToken} requestHeaders["User-Agent"] = []string{userAgent} requestHeaders["Custom"] = []string{"customHeader"} requestBody := make(map[string]interface{}) requestBody["key"] = "request" requestBodyBytes, _ := json.Marshal(requestBody) query := "topic=project" request := ApiRequest{ Method: "GET", URL: RequestUrl{Path: "/audit/new", RawQuery: &query}, 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"} logEntry, _ := NewAuditLogEntry( auditRequest, auditResponse, &eventMetadata, auditMetadata) 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.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() objectType := ObjectTypeProject routingIdentifier := NewAuditRoutingIdentifier(objectId, objectType) assert.Equal(t, objectId, routingIdentifier.Identifier) assert.Equal(t, objectType, 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) }) t.Run("operation name fallback on options", func(t *testing.T) { operationName := OperationNameFromUrlPath("/projects", "OPTIONS") assert.Equal(t, "projects.read", operationName) }) t.Run("operation name fallback on unknown", func(t *testing.T) { operationName := OperationNameFromUrlPath("/projects", "UNKNOWN") assert.Equal(t, "projects.read", 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, objectType, err := GetObjectIdAndTypeFromUrlPath("/v2/projects/audit") assert.NoError(t, err) assert.Equal(t, "", objectId) assert.Nil(t, objectType) }) t.Run("object id and type in url", func(t *testing.T) { objectId, objectType, 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, ObjectTypeProject, *objectType) }) t.Run("multiple object ids and types in url", func(t *testing.T) { objectId, objectType, 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, ObjectTypeProject, *objectType) }) } 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) }) } func Test_ResponseBodyToBytes(t *testing.T) { t.Run( "nil response body", func(t *testing.T) { bytes, err := ResponseBodyToBytes(nil) assert.Nil(t, bytes) assert.Nil(t, err) }, ) t.Run( "bytes", func(t *testing.T) { responseBody := []byte("data") bytes, err := ResponseBodyToBytes(responseBody) assert.Nil(t, err) assert.Equal(t, &responseBody, bytes) }, ) t.Run( "Protobuf message", func(t *testing.T) { protobufMessage := auditV1.ObjectIdentifier{Identifier: uuid.NewString(), Type: string(ObjectTypeProject)} bytes, err := ResponseBodyToBytes(&protobufMessage) assert.Nil(t, err) expected, err := protojson.Marshal(&protobufMessage) assert.Nil(t, err) assert.Equal(t, &expected, bytes) }, ) t.Run( "struct", func(t *testing.T) { type CustomObject struct { Value string } responseBody := CustomObject{Value: "data"} bytes, err := ResponseBodyToBytes(responseBody) assert.Nil(t, err) expected, err := json.Marshal(responseBody) assert.Nil(t, err) assert.Equal(t, &expected, bytes) }, ) t.Run( "map", func(t *testing.T) { responseBody := map[string]interface{}{"value": "data"} bytes, err := ResponseBodyToBytes(responseBody) assert.Nil(t, err) expected, err := json.Marshal(responseBody) assert.Nil(t, err) assert.Equal(t, &expected, bytes) }, ) }