package api import ( "encoding/json" "fmt" "net/http" "net/url" "testing" "time" "github.com/google/uuid" "github.com/stretchr/testify/assert" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" "google.golang.org/protobuf/types/known/wrapperspb" auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1" pkgAuditCommon "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/audit/common" ) 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 := pkgAuditCommon.ApiRequest{ Method: "GET", URL: pkgAuditCommon.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 := pkgAuditCommon.ApiRequest{ Method: "GET", URL: pkgAuditCommon.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 := pkgAuditCommon.ApiRequest{ Method: "GET", URL: pkgAuditCommon.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 := pkgAuditCommon.ApiRequest{ Method: "GET", URL: pkgAuditCommon.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 := pkgAuditCommon.ApiRequest{ Method: httpMethod, URL: pkgAuditCommon.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 := pkgAuditCommon.ApiRequest{ Method: "", URL: pkgAuditCommon.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"} headers["grpcgateway-authorization"] = []string{userToken} 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 := pkgAuditCommon.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 := pkgAuditCommon.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 := pkgAuditCommon.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 := pkgAuditCommon.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 := pkgAuditCommon.ApiRequest{Header: headers} auditClaims, authenticationPrincipal, audiences, authenticationInfo, err := AuditAttributesFromAuthorizationHeader(&request) assert.Nil(t, err) auditClaimsMap := auditClaims.AsMap() assert.Len(t, auditClaimsMap, 9) assert.Equal(t, []interface{}{"stackit-resource-manager-dev"}, auditClaimsMap["aud"]) assert.Equal(t, "stackit-resource-manager-dev", auditClaimsMap["client_id"]) assert.Equal(t, "2024-08-23 09:28:46 +0000 UTC", auditClaimsMap["exp"]) assert.Equal(t, "2024-08-23 09:13:46 +0000 UTC", auditClaimsMap["iat"]) assert.Equal(t, "https://accounts.dev.stackit.cloud", auditClaimsMap["iss"]) assert.Equal(t, "e46eba38-dedb-4541-94f3-49f97a934d58", auditClaimsMap["jti"]) assert.Equal(t, "2024-08-23 09:13:46 +0000 UTC", auditClaimsMap["nbf"]) assert.Equal(t, "uaa.none", auditClaimsMap["scope"]) assert.Equal(t, "stackit-resource-manager-dev", auditClaimsMap["sub"]) 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.Nil(t, 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 := pkgAuditCommon.ApiRequest{Header: headers} auditClaims, authenticationPrincipal, audiences, authenticationInfo, err := AuditAttributesFromAuthorizationHeader(&request) assert.Nil(t, err) auditClaimsMap := auditClaims.AsMap() assert.Len(t, auditClaimsMap, 12) assert.Equal(t, []interface{}{"stackit", "api"}, auditClaimsMap["aud"]) assert.Equal(t, "cd94f01a-df2e-4456-902e-48f5e57f0b63", auditClaimsMap["azp"]) assert.Equal(t, "my-service-yifc9e1@sa.stackit.cloud", auditClaimsMap["email"]) assert.Equal(t, "2024-08-03 07:15:43 +0000 UTC", auditClaimsMap["exp"]) assert.Equal(t, "2024-08-02 07:15:43 +0000 UTC", auditClaimsMap["iat"]) assert.Equal(t, "stackit/serviceaccount", auditClaimsMap["iss"]) assert.Equal(t, "84c30a46-1001-436f-859f-89c0ba19be1e", auditClaimsMap["jti"]) assert.Equal(t, "api", auditClaimsMap["stackit/serviceaccount/namespace"]) assert.Equal(t, "10f38b01-534b-47bb-a03a-e294ca2be4de", auditClaimsMap[TokenClaimStackitServiceAccountId]) assert.Equal(t, "legacy", auditClaimsMap["stackit/serviceaccount/token.source"]) assert.Equal(t, "dacc7830-843e-4c5e-86ff-aa0fb51d636f", auditClaimsMap[TokenClaimStackitProjectId]) assert.Equal(t, "10f38b01-534b-47bb-a03a-e294ca2be4de", auditClaimsMap["sub"]) 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("service account access token with underscore in subject", func(t *testing.T) { headers := make(map[string][]string) headers["Authorization"] = []string{serviceAccountTokenUnderscoreSubject} request := pkgAuditCommon.ApiRequest{Header: headers} auditClaims, authenticationPrincipal, audiences, authenticationInfo, err := AuditAttributesFromAuthorizationHeader(&request) assert.Nil(t, err) auditClaimsMap := auditClaims.AsMap() assert.Len(t, auditClaimsMap, 12) assert.Equal(t, []interface{}{"stackit", "api"}, auditClaimsMap["aud"]) assert.Equal(t, "cd94f01a-df2e-4456-902e-48f5e57f0b63", auditClaimsMap["azp"]) assert.Equal(t, "my-service-yifc9e1@sa.stackit.cloud", auditClaimsMap["email"]) assert.Equal(t, "2024-08-03 07:15:43 +0000 UTC", auditClaimsMap["exp"]) assert.Equal(t, "2024-08-02 07:15:43 +0000 UTC", auditClaimsMap["iat"]) assert.Equal(t, "stackit/serviceaccount", auditClaimsMap["iss"]) assert.Equal(t, "84c30a46-1001-436f-859f-89c0ba19be1e", auditClaimsMap["jti"]) assert.Equal(t, "api", auditClaimsMap["stackit/serviceaccount/namespace"]) assert.Equal(t, "10f38b01-534b-47bb-a03a-e294ca2be4de", auditClaimsMap[TokenClaimStackitServiceAccountId]) assert.Equal(t, "legacy", auditClaimsMap["stackit/serviceaccount/token.source"]) assert.Equal(t, "dacc7830-843e-4c5e-86ff-aa0fb51d636f", auditClaimsMap[TokenClaimStackitProjectId]) assert.Equal(t, "10f38b01_534b_47bb_a03a_e294ca2be4de", auditClaimsMap["sub"]) 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 := pkgAuditCommon.ApiRequest{Header: headers} auditClaims, authenticationPrincipal, audiences, authenticationInfo, err := AuditAttributesFromAuthorizationHeader(&request) assert.Nil(t, err) auditClaimsMap := auditClaims.AsMap() assert.Len(t, auditClaimsMap, 13) assert.Equal(t, []interface{}{"stackit", "api"}, auditClaimsMap["aud"]) assert.Equal(t, "02aef516-317f-4ec1-a1df-1acbd4d49fe3", auditClaimsMap["azp"]) assert.Equal(t, "service-account-2-tj9srt1@sa.stackit.cloud", auditClaimsMap["email"]) assert.Equal(t, "2024-08-19 10:21:47 +0000 UTC", auditClaimsMap["exp"]) assert.Equal(t, "2024-08-19 09:21:47 +0000 UTC", auditClaimsMap["iat"]) assert.Equal(t, "stackit/serviceaccount", auditClaimsMap["iss"]) assert.Equal(t, "37555183-01b9-4270-bdc1-69b4fcfd5ee9", auditClaimsMap["jti"]) assert.Equal(t, "api", auditClaimsMap["stackit/serviceaccount/namespace"]) assert.Equal(t, "f45009b2-6433-43c1-b6c7-618c44359e71", auditClaimsMap[TokenClaimStackitServiceAccountId]) assert.Equal(t, "oauth2", auditClaimsMap["stackit/serviceaccount/token.source"]) assert.Equal(t, "dacc7830-843e-4c5e-86ff-aa0fb51d636f", auditClaimsMap[TokenClaimStackitProjectId]) assert.Equal(t, "f45009b2-6433-43c1-b6c7-618c44359e71", auditClaimsMap["sub"]) assert.NotNil(t, auditClaimsMap["act"]) act := auditClaimsMap["act"].(map[string]interface{}) assert.NotNil(t, act) assert.Equal(t, "02aef516-317f-4ec1-a1df-1acbd4d49fe3", act["sub"]) 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 := pkgAuditCommon.ApiRequest{Header: headers} auditClaims, authenticationPrincipal, audiences, authenticationInfo, err := AuditAttributesFromAuthorizationHeader(&request) assert.Nil(t, err) auditClaimsMap := auditClaims.AsMap() assert.Len(t, auditClaimsMap, 13) assert.Equal(t, []interface{}{"stackit", "api"}, auditClaimsMap["aud"]) assert.Equal(t, "f45009b2-6433-43c1-b6c7-618c44359e71", auditClaimsMap["azp"]) assert.Equal(t, "service-account-3-fghsxw1@sa.stackit.cloud", auditClaimsMap["email"]) assert.Equal(t, "2024-08-19 10:22:43 +0000 UTC", auditClaimsMap["exp"]) assert.Equal(t, "2024-08-19 09:22:43 +0000 UTC", auditClaimsMap["iat"]) assert.Equal(t, "stackit/serviceaccount", auditClaimsMap["iss"]) assert.Equal(t, "1f7f1efc-3349-411a-a5d7-2255e0a5a8ae", auditClaimsMap["jti"]) assert.Equal(t, "api", auditClaimsMap["stackit/serviceaccount/namespace"]) assert.Equal(t, "1734b4b6-1d5e-4819-9b50-29917a1b9ad5", auditClaimsMap[TokenClaimStackitServiceAccountId]) assert.Equal(t, "oauth2", auditClaimsMap["stackit/serviceaccount/token.source"]) assert.Equal(t, "dacc7830-843e-4c5e-86ff-aa0fb51d636f", auditClaimsMap[TokenClaimStackitProjectId]) assert.Equal(t, "1734b4b6-1d5e-4819-9b50-29917a1b9ad5", auditClaimsMap["sub"]) assert.NotNil(t, auditClaimsMap["act"]) act := auditClaimsMap["act"].(map[string]interface{}) assert.NotNil(t, act) assert.Equal(t, "f45009b2-6433-43c1-b6c7-618c44359e71", act["sub"]) nestedAct := act["act"].(map[string]interface{}) assert.NotNil(t, nestedAct) assert.Equal(t, "02aef516-317f-4ec1-a1df-1acbd4d49fe3", nestedAct["sub"]) 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 := pkgAuditCommon.ApiRequest{Header: headers} auditClaims, authenticationPrincipal, audiences, authenticationInfo, err := AuditAttributesFromAuthorizationHeader(&request) assert.Nil(t, err) auditClaimsMap := auditClaims.AsMap() assert.Len(t, auditClaimsMap, 11) assert.Equal(t, []interface{}{"stackit-portal-login-dev-client-id"}, auditClaimsMap["aud"]) assert.Equal(t, "stackit-portal-login-dev-client-id", auditClaimsMap["client_id"]) assert.Equal(t, "Christian.Schaible@novatec-gmbh.de", auditClaimsMap["email"]) assert.True(t, auditClaimsMap["email_verified"].(bool)) assert.Equal(t, "2024-08-02 09:19:27 +0000 UTC", auditClaimsMap["exp"]) assert.Equal(t, "2024-08-02 08:19:27 +0000 UTC", auditClaimsMap["iat"]) assert.Equal(t, "https://accounts.dev.stackit.cloud", auditClaimsMap["iss"]) assert.Equal(t, "d73a67ac-d1ec-4b55-99d4-e953275f022a", auditClaimsMap["jti"]) assert.Equal(t, "2024-08-02 08:19:27 +0000 UTC", auditClaimsMap["nbf"]) assert.Equal(t, "openid email", auditClaimsMap["scope"]) assert.Equal(t, "cd94f01a-df2e-4456-902e-48f5e57f0b63", auditClaimsMap["sub"]) 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) }) t.Run("user token with simple aud claim", func(t *testing.T) { headers := make(map[string][]string) headers["Authorization"] = []string{userTokenWithSimpleAudience} request := pkgAuditCommon.ApiRequest{Header: headers} auditClaims, authenticationPrincipal, audiences, authenticationInfo, err := AuditAttributesFromAuthorizationHeader(&request) assert.NoError(t, err) auditClaimsMap := auditClaims.AsMap() assert.Len(t, auditClaimsMap, 9) assert.Equal(t, []interface{}{"https://stackit-service-account-dev.apps.01.cf.eu01.stackit.cloud"}, auditClaimsMap["aud"]) assert.Equal(t, "Lukas.Schmitt@stackit.cloud", auditClaimsMap["email"]) assert.Equal(t, "2024-11-21 09:40:35 +0000 UTC", auditClaimsMap["exp"]) assert.Equal(t, "2024-11-21 08:40:35 +0000 UTC", auditClaimsMap["iat"]) assert.Equal(t, "https://api.dev.stackit.cloud", auditClaimsMap["iss"]) assert.Equal(t, "c2be1651-1e54-4e6e-bac3-ef072b3f0149", auditClaimsMap["jti"]) assert.Equal(t, "2024-11-21 08:40:18 +0000 UTC", auditClaimsMap["nbf"]) assert.Equal(t, "openid email portal-bff", auditClaimsMap["scope"]) assert.Equal(t, "5e426aed-c487-4c48-af25-87f69cf9cdd4", auditClaimsMap["sub"]) principal := fmt.Sprintf("%s/%s", url.QueryEscape("5e426aed-c487-4c48-af25-87f69cf9cdd4"), url.QueryEscape("https://api.dev.stackit.cloud")) assert.Equal(t, principal, authenticationPrincipal) assert.Equal(t, []string{"https://stackit-service-account-dev.apps.01.cf.eu01.stackit.cloud"}, audiences) assert.Equal(t, "5e426aed-c487-4c48-af25-87f69cf9cdd4", authenticationInfo.PrincipalId) assert.Equal(t, "Lukas.Schmitt@stackit.cloud", *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 := pkgAuditCommon.ApiRequest{ Method: "GET", URL: pkgAuditCommon.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, pkgAuditCommon.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 := pkgAuditCommon.ApiRequest{ Method: "GET", URL: pkgAuditCommon.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, pkgAuditCommon.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 := pkgAuditCommon.ObjectTypeProject routingIdentifier := NewAuditRoutingIdentifier(objectId, objectType) assert.Equal(t, objectId, routingIdentifier.Identifier) assert.Equal(t, objectType, routingIdentifier.Type) }