audit-go/audit/api/model.go
2024-11-21 16:28:08 +01:00

969 lines
28 KiB
Go

package api
import (
"context"
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
"encoding/json"
"errors"
"fmt"
"github.com/google/uuid"
"github.com/lestrrat-go/jwx/v2/jwt"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/timestamppb"
"google.golang.org/protobuf/types/known/wrapperspb"
"net"
"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 ApiRequest struct {
// Body
//
// Required: false
Body *[]byte
// The (HTTP) request headers / gRPC metadata.
//
// Internal IP-Addresses have to be removed (e.g. in x-forwarded-xxx headers).
//
// Required: true
Header map[string][]string
// The HTTP request `Host` header value.
//
// Required: true
Host string
// Method
//
// Required: true
Method string
// The URL scheme, such as `http`, `https` or `gRPC`.
//
// Required: true
Scheme string
// The network protocol used with the request, such as "http/1.1",
// "spdy/3", "h2", "h2c", "webrtc", "tcp", "udp", "quic". See
// https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids
// for details.
//
// Required: true
Proto string
// The url
//
// Required: true
URL RequestUrl
}
type RequestUrl struct {
// The gRPC / HTTP URL path.
//
// Required: true
Path string
// The HTTP URL query in the format of "name1=value1&name2=value2", as it
// appears in the first line of the HTTP request.
// The input should be escaped to not contain any special characters.
//
// Required: false
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: true
Request *ApiRequest
// 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 an 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 ObjectType as plural
// 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: Optional 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 ObjectType as plural
// 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/7046e7b6-5ae9-441c-99fe-2cd28a5078ec/locations/sx-stoi01/instances/instance-20240723-174217"
// "projects/dd7d1807-54e9-4426-8994-721758b5b554/locations/eu01-m/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,
) (*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, errors.Join(err, 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, errors.Join(err, 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,
}
return &event, nil
}
// GetCalledServiceNameFromRequest extracts the called service name from subdomain name
func GetCalledServiceNameFromRequest(request *ApiRequest, fallbackName string) string {
if request == nil {
return fallbackName
}
var calledServiceName = fallbackName
host := request.Host
ip := net.ParseIP(host)
if ip == nil && !strings.Contains(host, "localhost") {
dotIdx := strings.Index(host, ".")
if dotIdx != -1 {
calledServiceName = host[0:dotIdx]
}
}
return calledServiceName
}
// 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 *ApiRequest,
requestHeaders map[string]string,
requestId *string,
requestScheme string,
requestTime time.Time,
clientIp string,
authenticationPrincipal string,
audiences []string,
auditClaims *structpb.Struct,
) *auditV1.RequestMetadata {
agent := requestHeaders["User-Agent"]
if agent == "" {
agent = requestHeaders["user-agent"]
}
return &auditV1.RequestMetadata{
CallerIp: clientIp,
CallerSuppliedUserAgent: agent,
RequestAttributes: NewRequestAttributes(
request,
requestHeaders,
requestId,
requestScheme,
requestTime,
authenticationPrincipal,
audiences,
auditClaims,
),
}
}
// NewRequestAttributes returns initialized protobuf AttributeContext_Request object.
func NewRequestAttributes(
request *ApiRequest,
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 != nil && *rawQuery != "" {
escapedQuery := url.QueryEscape(*rawQuery)
query = &escapedQuery
}
return &auditV1.AttributeContext_Request{
Id: requestId,
Method: 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 id.
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 *ApiRequest) (*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 ":authority", "Authorization", "B3" and "Host" 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{":authority", "authorization", "b3", "host"}
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
break
}
}
// 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 object type.
func NewAuditRoutingIdentifier(objectId string, objectType ObjectType) *RoutableIdentifier {
return &RoutableIdentifier{
Identifier: objectId,
Type: objectType,
}
}
// 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 *ApiRequest) (
*structpb.Struct,
string,
[]string,
*auditV1.AuthenticationInfo,
error,
) {
var principalId = "none"
var principalEmail = "do-not-reply@stackit.cloud"
emptyClaims, _ := structpb.NewStruct(make(map[string]interface{}))
var auditClaims = emptyClaims
var authenticationPrincipal = "none/none"
var serviceAccountName *string = nil
audiences := make([]string, 0)
var delegationInfo []*auditV1.ServiceAccountDelegationInfo = nil
authorizationHeaders := request.Header["Authorization"]
if len(authorizationHeaders) == 0 {
// fallback for grpc where headers/metadata keys are lowercase
authorizationHeaders = request.Header["authorization"]
}
authorizationHeader := strings.Join(authorizationHeaders, ",")
trimmedAuthorizationHeader := strings.TrimSpace(authorizationHeader)
if len(trimmedAuthorizationHeader) > 0 {
// Parse claims
token, err := parseToken(trimmedAuthorizationHeader)
if err != nil {
return nil, authenticationPrincipal, nil, nil, err
}
filteredClaims, err := parseClaimsFromAuthorizationHeader(token)
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(token)
principalId, principalEmail = extractSubjectAndEmail(token)
// Extract service account delegation info data
actClaim, hasActClaim := token.Get("act")
if hasActClaim {
actMap := map[string]interface{}{"act": actClaim}
delegationInfo = extractServiceAccountDelegationInfo(actMap)
}
// Extract audiences data
audiences = token.Audience()
// Extract project id and service account id
projectId := extractServiceAccountProjectId(token)
serviceAccountId := extractServiceAccountId(token)
// Calculate service account name if project id and service account id are available
if projectId != nil && serviceAccountId != nil {
accountName := fmt.Sprintf("projects/%s/service-accounts/%s", *projectId, *serviceAccountId)
serviceAccountName = &accountName
}
}
authenticationInfo := auditV1.AuthenticationInfo{
PrincipalId: principalId,
PrincipalEmail: principalEmail,
ServiceAccountName: serviceAccountName,
ServiceAccountDelegationInfo: delegationInfo,
}
return auditClaims, authenticationPrincipal, audiences, &authenticationInfo, nil
}
func extractServiceAccountId(token jwt.Token) *string {
projectId, projectIdExists := token.Get("stackit/serviceaccount/service-account.uid")
if projectIdExists {
projectIdString := fmt.Sprintf("%s", projectId)
return &projectIdString
} else {
return nil
}
}
func extractServiceAccountProjectId(token jwt.Token) *string {
projectId, projectIdExists := token.Get("stackit/project/project.id")
if projectIdExists {
projectIdString := fmt.Sprintf("%s", projectId)
return &projectIdString
} else {
return nil
}
}
func extractAuthenticationPrincipal(token jwt.Token) string {
subject := token.Subject()
issuer := token.Issuer()
var principal = "none/none"
if subject != "" && issuer != "" {
principal = fmt.Sprintf("%s/%s", url.QueryEscape(subject), url.QueryEscape(issuer))
}
return principal
}
func parseToken(authorizationHeader string) (jwt.Token, error) {
parts := strings.Split(authorizationHeader, " ")
if len(parts) != 2 {
return nil, ErrInvalidAuthorizationHeaderValue
}
if !strings.EqualFold(parts[0], "Bearer") {
return nil, ErrTokenIsNotBearerToken
}
jwtString := parts[1]
authorizationHeaderParts := strings.Split(jwtString, ".")
if len(authorizationHeaderParts) == 3 {
token, err := jwt.ParseInsecure([]byte(parts[1]))
if err != nil {
return nil, ErrInvalidBearerToken
}
return token, nil
}
return nil, ErrInvalidBearerToken
}
func parseClaimsFromAuthorizationHeader(token jwt.Token) (map[string]interface{}, error) {
claimsMap, err := token.AsMap(context.Background())
if err != nil {
return nil, err
}
claims := map[string]interface{}{}
if len(token.Audience()) > 0 {
var audiences []any
for _, audience := range token.Audience() {
audiences = append(audiences, audience)
}
claims["aud"] = audiences
}
for key := range claimsMap {
if key == "aud" {
continue
}
value := claimsMap[key]
t, isTime := value.(time.Time)
if isTime {
claims[key] = t.String()
} else if value != nil && value != "" {
claims[key] = value
}
}
return claims, nil
}
func extractServiceAccountDelegationInfoDetails(actClaims map[string]interface{}) []*auditV1.ServiceAccountDelegationInfo {
principalId, principalEmail := extractSubjectAndEmailFromActClaims(actClaims)
delegation := auditV1.ServiceAccountDelegationInfo{Authority: &auditV1.ServiceAccountDelegationInfo_IdpPrincipal_{IdpPrincipal: &auditV1.ServiceAccountDelegationInfo_IdpPrincipal{
PrincipalId: principalId,
PrincipalEmail: principalEmail,
ServiceMetadata: nil,
}}}
delegations := []*auditV1.ServiceAccountDelegationInfo{&delegation}
nestedDelegations := extractServiceAccountDelegationInfo(actClaims)
if len(nestedDelegations) > 0 {
return append(delegations, nestedDelegations...)
} else {
return delegations
}
}
func extractServiceAccountDelegationInfo(claims map[string]interface{}) []*auditV1.ServiceAccountDelegationInfo {
actor, hasActor := claims["act"]
if hasActor {
actorMap, hasActorClaim := actor.(map[string]interface{})
if hasActorClaim {
return extractServiceAccountDelegationInfoDetails(actorMap)
}
}
return nil
}
func extractSubjectAndEmailFromActClaims(actClaim map[string]interface{}) (string, string) {
var principalEmail string
principalId := fmt.Sprintf("%s", actClaim["sub"])
principalEmailRaw := actClaim["email"]
if principalEmailRaw == nil {
principalEmail = "do-not-reply@stackit.cloud"
} else {
principalEmail = fmt.Sprintf("%s", principalEmailRaw)
}
return principalId, principalEmail
}
func extractSubjectAndEmail(token jwt.Token) (string, string) {
var principalEmail string
principalId := token.Subject()
emailClaim, hasEmail := token.Get("email")
if !hasEmail {
principalEmail = "do-not-reply@stackit.cloud"
} else {
principalEmail = fmt.Sprintf("%s", emailClaim)
}
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 := 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,
*ObjectType,
error,
) {
// Extract object id and type from request url
objectTypeIdMatches := objectTypeIdPattern.FindStringSubmatch(path)
if len(objectTypeIdMatches) > 0 {
objectType := ObjectTypeFromPluralString(objectTypeIdMatches[1])
err := objectType.IsSupportedType()
if err != nil {
return "", nil, err
}
objectId := objectTypeIdMatches[2]
return objectId, &objectType, nil
}
return "", 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
}
// ResponseBodyToBytes converts a JSON or Protobuf response into a byte array
func ResponseBodyToBytes(response any) (*[]byte, error) {
if response == nil {
return nil, nil
}
responseBytes, isBytes := response.([]byte)
if isBytes {
return &responseBytes, nil
}
responseProtoMessage, isProtoMessage := response.(proto.Message)
if isProtoMessage {
responseJson, err := protojson.Marshal(responseProtoMessage)
if err != nil {
return nil, err
}
return &responseJson, nil
} else {
responseJson, err := json.Marshal(response)
if err != nil {
return nil, err
}
return &responseJson, nil
}
}