mirror of
https://dev.azure.com/schwarzit/schwarzit.stackit-public/_git/audit-go
synced 2026-02-08 00:57:24 +00:00
443 lines
14 KiB
Go
443 lines
14 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"github.com/google/uuid"
|
|
"net/url"
|
|
"time"
|
|
|
|
"dev.azure.com/schwarzit/schwarzit.stackit-core-platform/common-audit.git/audit/messaging"
|
|
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-core-platform/common-audit.git/gen/go/audit/v1"
|
|
|
|
"google.golang.org/protobuf/proto"
|
|
)
|
|
|
|
var ErrUnsupportedSeverity = errors.New("unsupported severity level")
|
|
|
|
// LegacyTopicNameResolver implements TopicNameResolver.
|
|
// A hard-coded topic name is used, routing identifiers are ignored.
|
|
type LegacyTopicNameResolver struct {
|
|
topicName string
|
|
}
|
|
|
|
// Resolve implements TopicNameResolver.Resolve
|
|
func (r *LegacyTopicNameResolver) Resolve(*RoutableIdentifier) (string, error) {
|
|
return r.topicName, nil
|
|
}
|
|
|
|
// LegacyTopicNameConfig provides topic name information required for the topic name resolution.
|
|
type LegacyTopicNameConfig struct {
|
|
TopicName string
|
|
}
|
|
|
|
// LegacyAuditApi is an implementation of AuditApi to send events to the legacy audit log system.
|
|
//
|
|
// Note: The implementation will be deprecated and replaced with the "routableAuditApi" once the new audit log routing is implemented
|
|
type LegacyAuditApi struct {
|
|
messagingApi *messaging.Api
|
|
topicNameResolver *TopicNameResolver
|
|
validator *ProtobufValidator
|
|
}
|
|
|
|
// NewLegacyAuditApi can be used to initialize the audit log api.
|
|
//
|
|
// Note: The NewLegacyAuditApi method will be deprecated and replaced with "newRoutableAuditApi" once the new audit log routing is implemented
|
|
func NewLegacyAuditApi(
|
|
messagingApi *messaging.Api,
|
|
topicNameConfig LegacyTopicNameConfig,
|
|
validator ProtobufValidator,
|
|
) (*AuditApi, error) {
|
|
|
|
if messagingApi == nil {
|
|
return nil, ErrMessagingApiNil
|
|
}
|
|
|
|
// Topic resolver
|
|
if topicNameConfig.TopicName == "" {
|
|
return nil, errors.New("topic name is required")
|
|
}
|
|
var topicNameResolver TopicNameResolver = &LegacyTopicNameResolver{topicName: topicNameConfig.TopicName}
|
|
|
|
// Audit api
|
|
var auditApi AuditApi = &LegacyAuditApi{
|
|
messagingApi: messagingApi,
|
|
topicNameResolver: &topicNameResolver,
|
|
validator: &validator,
|
|
}
|
|
|
|
return &auditApi, nil
|
|
}
|
|
|
|
// Log implements AuditApi.Log
|
|
func (a *LegacyAuditApi) Log(
|
|
ctx context.Context,
|
|
event *auditV1.AuditLogEntry,
|
|
visibility auditV1.Visibility,
|
|
routableIdentifier *RoutableIdentifier,
|
|
) error {
|
|
|
|
return a.LogWithTrace(ctx, event, visibility, routableIdentifier, nil, nil)
|
|
}
|
|
|
|
// LogWithTrace implements AuditApi.LogWithTrace
|
|
func (a *LegacyAuditApi) LogWithTrace(
|
|
ctx context.Context,
|
|
event *auditV1.AuditLogEntry,
|
|
visibility auditV1.Visibility,
|
|
routableIdentifier *RoutableIdentifier,
|
|
traceParent *string,
|
|
traceState *string,
|
|
) error {
|
|
|
|
cloudEvent, err := a.ValidateAndSerializeWithTrace(event, visibility, routableIdentifier, traceParent, traceState)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return a.Send(ctx, routableIdentifier, cloudEvent)
|
|
}
|
|
|
|
// ValidateAndSerialize implements AuditApi.ValidateAndSerialize.
|
|
// It serializes the event into the byte representation of the legacy audit log system.
|
|
func (a *LegacyAuditApi) ValidateAndSerialize(
|
|
event *auditV1.AuditLogEntry,
|
|
visibility auditV1.Visibility,
|
|
routableIdentifier *RoutableIdentifier,
|
|
) (*CloudEvent, error) {
|
|
return a.ValidateAndSerializeWithTrace(event, visibility, routableIdentifier, nil, nil)
|
|
}
|
|
|
|
// ValidateAndSerializeWithTrace implements AuditApi.ValidateAndSerializeWithTrace.
|
|
// It serializes the event into the byte representation of the legacy audit log system.
|
|
func (a *LegacyAuditApi) ValidateAndSerializeWithTrace(
|
|
event *auditV1.AuditLogEntry,
|
|
visibility auditV1.Visibility,
|
|
routableIdentifier *RoutableIdentifier,
|
|
traceParent *string,
|
|
traceState *string,
|
|
) (*CloudEvent, error) {
|
|
|
|
routableEvent, err := validateAndSerializePartially(a.validator, event, visibility, routableIdentifier)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Do nothing with the serialized data in the legacy solution
|
|
_, err = proto.Marshal(routableEvent)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Convert attributes
|
|
legacyBytes, err := a.convertAndSerializeIntoLegacyFormat(event, routableEvent)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
message := CloudEvent{
|
|
SpecVersion: "1.0",
|
|
Source: event.ProtoPayload.ServiceName,
|
|
// TODO what is the correct id?
|
|
Id: uuid.NewString(),
|
|
Time: event.ProtoPayload.RequestMetadata.RequestAttributes.Time.AsTime(),
|
|
DataContentType: ContentTypeCloudEventsProtobuf,
|
|
DataType: fmt.Sprintf("%v", routableEvent.ProtoReflect().Descriptor().FullName()),
|
|
// TODO check if this is correct
|
|
Subject: event.ProtoPayload.ResourceName,
|
|
Data: legacyBytes,
|
|
TraceParent: traceParent,
|
|
TraceState: traceState,
|
|
}
|
|
return &message, nil
|
|
}
|
|
|
|
// Send implements AuditApi.Send
|
|
func (a *LegacyAuditApi) Send(
|
|
ctx context.Context,
|
|
routableIdentifier *RoutableIdentifier,
|
|
cloudEvent *CloudEvent,
|
|
) error {
|
|
|
|
return send(a.topicNameResolver, a.messagingApi, ctx, routableIdentifier, cloudEvent)
|
|
}
|
|
|
|
// convertAndSerializeIntoLegacyFormat converts the protobuf events into the json serialized legacy audit log format
|
|
func (a *LegacyAuditApi) convertAndSerializeIntoLegacyFormat(
|
|
event *auditV1.AuditLogEntry,
|
|
routableEvent *auditV1.RoutableAuditEvent,
|
|
) ([]byte, error) {
|
|
|
|
// Source IP & User agent
|
|
var sourceIpAddress string
|
|
var userAgent string
|
|
if event.ProtoPayload == nil || event.ProtoPayload.RequestMetadata == nil {
|
|
sourceIpAddress = "0.0.0.0"
|
|
userAgent = "none"
|
|
} else {
|
|
sourceIpAddress = event.ProtoPayload.RequestMetadata.CallerIp
|
|
userAgent = event.ProtoPayload.RequestMetadata.CallerSuppliedUserAgent
|
|
}
|
|
|
|
// Principals
|
|
var serviceAccountDelegationInfo *LegacyAuditEventServiceAccountDelegationInfo = nil
|
|
if len(event.ProtoPayload.AuthenticationInfo.ServiceAccountDelegationInfo) > 0 {
|
|
var principals []LegacyAuditEventPrincipal
|
|
for _, principal := range event.ProtoPayload.AuthenticationInfo.ServiceAccountDelegationInfo {
|
|
switch principalValue := principal.Authority.(type) {
|
|
case *auditV1.ServiceAccountDelegationInfo_IdpPrincipal_:
|
|
principals = append(principals, LegacyAuditEventPrincipal{
|
|
Id: principalValue.IdpPrincipal.PrincipalId,
|
|
Email: &principalValue.IdpPrincipal.PrincipalEmail,
|
|
})
|
|
case *auditV1.ServiceAccountDelegationInfo_SystemPrincipal_:
|
|
principals = append(principals, LegacyAuditEventPrincipal{
|
|
Id: "system",
|
|
})
|
|
default:
|
|
return nil, errors.New("unsupported principal type")
|
|
}
|
|
}
|
|
serviceAccountDelegationInfo = &LegacyAuditEventServiceAccountDelegationInfo{Principals: principals}
|
|
}
|
|
|
|
var request LegacyAuditEventRequest
|
|
if event.ProtoPayload.RequestMetadata.RequestAttributes == nil {
|
|
request = LegacyAuditEventRequest{
|
|
Endpoint: "none",
|
|
}
|
|
} else {
|
|
var parameters map[string]interface{} = nil
|
|
if event.ProtoPayload.RequestMetadata.RequestAttributes.Path != "" &&
|
|
event.ProtoPayload.RequestMetadata.RequestAttributes.Query != nil &&
|
|
*event.ProtoPayload.RequestMetadata.RequestAttributes.Query != "" {
|
|
parameters = map[string]interface{}{}
|
|
|
|
parsedUrl, err := url.Parse(fmt.Sprintf("%s?%s",
|
|
event.ProtoPayload.RequestMetadata.RequestAttributes.Path,
|
|
*event.ProtoPayload.RequestMetadata.RequestAttributes.Query))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for k, v := range parsedUrl.Query() {
|
|
parameters[k] = v
|
|
}
|
|
}
|
|
|
|
var body map[string]interface{} = nil
|
|
if event.ProtoPayload.Request != nil {
|
|
body = event.ProtoPayload.Request.AsMap()
|
|
}
|
|
var headers map[string]interface{} = nil
|
|
if event.ProtoPayload.RequestMetadata.RequestAttributes.Headers != nil {
|
|
headers = map[string]interface{}{}
|
|
for key, value := range event.ProtoPayload.RequestMetadata.RequestAttributes.Headers {
|
|
headers[key] = value
|
|
}
|
|
}
|
|
|
|
request = LegacyAuditEventRequest{
|
|
Endpoint: event.ProtoPayload.RequestMetadata.RequestAttributes.Path,
|
|
Parameters: ¶meters,
|
|
Body: &body,
|
|
Headers: &headers,
|
|
}
|
|
}
|
|
|
|
if routableEvent.ObjectIdentifier == nil {
|
|
return nil, ErrObjectIdentifierNil
|
|
}
|
|
|
|
// Context and event type
|
|
var messageContext *LegacyAuditEventContext
|
|
var eventType string
|
|
switch routableEvent.ObjectIdentifier.Type {
|
|
case string(SingularTypeProject):
|
|
eventType = "ADMIN_ACTIVITY"
|
|
messageContext = &LegacyAuditEventContext{
|
|
OrganizationId: nil,
|
|
FolderId: nil,
|
|
ProjectId: &routableEvent.ObjectIdentifier.Identifier,
|
|
}
|
|
case string(SingularTypeFolder):
|
|
eventType = "ADMIN_ACTIVITY"
|
|
messageContext = &LegacyAuditEventContext{
|
|
OrganizationId: nil,
|
|
FolderId: &routableEvent.ObjectIdentifier.Identifier,
|
|
ProjectId: nil,
|
|
}
|
|
case string(SingularTypeOrganization):
|
|
eventType = "ADMIN_ACTIVITY"
|
|
messageContext = &LegacyAuditEventContext{
|
|
OrganizationId: &routableEvent.ObjectIdentifier.Identifier,
|
|
FolderId: nil,
|
|
ProjectId: nil,
|
|
}
|
|
case string(SingularTypeSystem):
|
|
eventType = "SYSTEM_EVENT"
|
|
messageContext = nil
|
|
default:
|
|
return nil, ErrUnsupportedObjectIdentifierType
|
|
}
|
|
|
|
var visibility string
|
|
switch routableEvent.Visibility {
|
|
case auditV1.Visibility_VISIBILITY_PUBLIC:
|
|
visibility = "PUBLIC"
|
|
case auditV1.Visibility_VISIBILITY_PRIVATE:
|
|
visibility = "PRIVATE"
|
|
}
|
|
|
|
// Details
|
|
var details = event.ProtoPayload.Request.AsMap()
|
|
|
|
// Result
|
|
var result = event.ProtoPayload.Response.AsMap()
|
|
|
|
// Severity
|
|
var severity string
|
|
switch event.Severity {
|
|
case auditV1.LogSeverity_DEFAULT:
|
|
fallthrough
|
|
case auditV1.LogSeverity_DEBUG:
|
|
fallthrough
|
|
case auditV1.LogSeverity_INFO:
|
|
fallthrough
|
|
case auditV1.LogSeverity_NOTICE:
|
|
fallthrough
|
|
case auditV1.LogSeverity_WARNING:
|
|
severity = "INFO"
|
|
case auditV1.LogSeverity_ERROR:
|
|
fallthrough
|
|
case auditV1.LogSeverity_CRITICAL:
|
|
fallthrough
|
|
case auditV1.LogSeverity_ALERT:
|
|
fallthrough
|
|
case auditV1.LogSeverity_EMERGENCY:
|
|
severity = "ERROR"
|
|
default:
|
|
return nil, ErrUnsupportedSeverity
|
|
}
|
|
|
|
// Instantiate the legacy event - missing values are filled with defaults
|
|
legacyAuditEvent := LegacyAuditEvent{
|
|
Severity: severity,
|
|
Visibility: visibility,
|
|
EventType: eventType,
|
|
EventTimeStamp: event.ProtoPayload.RequestMetadata.RequestAttributes.Time.AsTime(),
|
|
EventName: event.ProtoPayload.MethodName,
|
|
SourceIpAddress: sourceIpAddress,
|
|
UserAgent: userAgent,
|
|
Initiator: LegacyAuditEventPrincipal{
|
|
Id: event.ProtoPayload.AuthenticationInfo.PrincipalId,
|
|
Email: &event.ProtoPayload.AuthenticationInfo.PrincipalEmail,
|
|
},
|
|
ServiceAccountDelegationInfo: serviceAccountDelegationInfo,
|
|
Request: request,
|
|
Context: messageContext,
|
|
// TODO clarify
|
|
ResourceId: &event.LogName,
|
|
ResourceName: &event.ProtoPayload.ResourceName,
|
|
CorrelationId: event.CorrelationId,
|
|
Result: &result,
|
|
Details: &details,
|
|
}
|
|
|
|
bytes, err := json.Marshal(legacyAuditEvent)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return bytes, nil
|
|
}
|
|
|
|
// LegacyAuditEvent has the format as follows:
|
|
/*
|
|
{
|
|
"severity": "INFO",
|
|
"visibility": "PUBLIC",
|
|
"eventType": "ADMIN_ACTIVITY",
|
|
"eventTimeStamp": "2019-08-24T14:15:22Z",
|
|
"eventName": "Create organization",
|
|
"sourceIpAddress": "127.0.0.1",
|
|
"userAgent": "CLI",
|
|
"initiator": {
|
|
"id": "string",
|
|
"email": "user@example.com"
|
|
},
|
|
"serviceAccountDelegationInfo": {
|
|
"principals": [
|
|
{
|
|
"id": "string",
|
|
"email": "user@example.com"
|
|
}
|
|
]
|
|
},
|
|
"request": {
|
|
"endpoint": "string",
|
|
"parameters": {},
|
|
"body": {},
|
|
"headers": {
|
|
"Content-Type": "application/json"
|
|
}
|
|
},
|
|
"context": {
|
|
"organizationId": "string",
|
|
"folderId": "string",
|
|
"projectId": "string"
|
|
},
|
|
"resourceId": "string",
|
|
"resourceName": "string",
|
|
"correlationId": "string",
|
|
"result": {},
|
|
"details": {}
|
|
}
|
|
*/
|
|
type LegacyAuditEvent struct {
|
|
Severity string `json:"severity"`
|
|
Visibility string `json:"visibility"`
|
|
EventType string `json:"eventType"`
|
|
EventTimeStamp time.Time `json:"eventTimeStamp"`
|
|
EventName string `json:"eventName"`
|
|
SourceIpAddress string `json:"sourceIpAddress"`
|
|
UserAgent string `json:"userAgent"`
|
|
Initiator LegacyAuditEventPrincipal `json:"initiator"`
|
|
ServiceAccountDelegationInfo *LegacyAuditEventServiceAccountDelegationInfo `json:"serviceAccountDelegationInfo"`
|
|
Request LegacyAuditEventRequest `json:"request"`
|
|
Context *LegacyAuditEventContext `json:"context"`
|
|
ResourceId *string `json:"resourceId"`
|
|
ResourceName *string `json:"resourceName"`
|
|
CorrelationId *string `json:"correlationId"`
|
|
Result *map[string]interface{} `json:"result"`
|
|
Details *map[string]interface{} `json:"details"`
|
|
}
|
|
|
|
// LegacyAuditEventPrincipal is a representation for a principal's id (+optional email) information.
|
|
type LegacyAuditEventPrincipal struct {
|
|
Id string `json:"id"`
|
|
Email *string `json:"email"`
|
|
}
|
|
|
|
// LegacyAuditEventServiceAccountDelegationInfo contains information about service account delegation.
|
|
type LegacyAuditEventServiceAccountDelegationInfo struct {
|
|
Principals []LegacyAuditEventPrincipal `json:"principals"`
|
|
}
|
|
|
|
// LegacyAuditEventRequest contains request information, which mirrors the action of the user and
|
|
// the resulting changes within the system.
|
|
type LegacyAuditEventRequest struct {
|
|
Endpoint string `json:"endpoint"`
|
|
Parameters *map[string]interface{} `json:"parameters"`
|
|
Body *map[string]interface{} `json:"body"`
|
|
Headers *map[string]interface{} `json:"headers"`
|
|
}
|
|
|
|
// LegacyAuditEventContext contains optional context information.
|
|
type LegacyAuditEventContext struct {
|
|
OrganizationId *string `json:"organizationId"`
|
|
FolderId *string `json:"folderId"`
|
|
ProjectId *string `json:"projectId"`
|
|
}
|