audit-go/audit/api/api_legacy.go
2024-07-19 10:02:56 +02:00

364 lines
12 KiB
Go

package api
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/google/uuid"
"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"
)
// 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(*RoutingIdentifier) (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.MessagingApi
topicNameResolver *TopicNameResolver
validator *ProtobufValidator
}
// NewLegacyAuditApi can be used to initialize the audit log api with LegacyAuditLogConnectionDetails.
//
// Note: The NewLegacyAuditApi method will be deprecated and replaced with "newRoutableAuditApi" once the new audit log routing is implemented
func NewLegacyAuditApi(
messagingApi *messaging.MessagingApi,
topicNameConfig LegacyTopicNameConfig,
validator ProtobufValidator,
) (*AuditApi, error) {
if messagingApi == nil {
return nil, errors.New("messaging api nil")
}
// Topic resolver
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.AuditEvent,
visibility auditV1.Visibility,
routingIdentifier *RoutingIdentifier,
objectIdentifier *auditV1.ObjectIdentifier,
) error {
cloudEvent, err := a.ValidateAndSerialize(event, visibility, routingIdentifier, objectIdentifier)
if err != nil {
return err
}
return a.Send(ctx, routingIdentifier, 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.AuditEvent,
visibility auditV1.Visibility,
routingIdentifier *RoutingIdentifier,
objectIdentifier *auditV1.ObjectIdentifier,
) (*CloudEvent, error) {
routableEvent, err := validateAndSerializePartially(a.validator, event, visibility, routingIdentifier, objectIdentifier)
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.EventSource,
id: uuid.NewString(),
time: event.EventTimeStamp.AsTime(),
dataContentType: "application/cloudevents+protobuf",
dataType: fmt.Sprintf("%v", routableEvent.ProtoReflect().Descriptor().FullName()),
data: legacyBytes,
}
return &message, nil
}
// Send implements AuditApi.Send
func (a *LegacyAuditApi) Send(
ctx context.Context,
routingIdentifier *RoutingIdentifier,
cloudEvent *CloudEvent,
) error {
return send(a.topicNameResolver, a.messagingApi, ctx, routingIdentifier, cloudEvent)
}
// convertAndSerializeIntoLegacyFormat converts the protobuf events into the json serialized legacy audit log format
func (a *LegacyAuditApi) convertAndSerializeIntoLegacyFormat(
event *auditV1.AuditEvent,
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 {
if principal != nil {
p := LegacyAuditEventPrincipal{
Id: principal.GetFirstPartyPrincipal().Id,
Email: &principal.GetFirstPartyPrincipal().PrincipalEmail,
}
principals = append(principals, p)
}
}
serviceAccountDelegationInfo = &LegacyAuditEventServiceAccountDelegationInfo{Principals: principals}
}
// Request
var request LegacyAuditEventRequest
if event.Request == nil {
request = LegacyAuditEventRequest{
Endpoint: "none",
}
} else {
var parameters map[string]interface{} = nil
if event.Request.Parameters != nil {
parameters = event.Request.Parameters.AsMap()
}
var body map[string]interface{} = nil
if event.Request.Body != nil {
body = event.Request.Body.AsMap()
}
var headers map[string]interface{} = nil
if event.Request.Headers != nil {
headers = map[string]interface{}{}
for _, header := range event.Request.Headers {
if header != nil {
headers[header.Key] = header.Value
}
}
}
request = LegacyAuditEventRequest{
Endpoint: event.Request.Endpoint,
Parameters: &parameters,
Body: &body,
Headers: &headers,
}
}
// Context and event type
var messageContext *LegacyAuditEventContext
var eventType string
switch ref := routableEvent.GetResourceReference().(type) {
case *auditV1.RoutableAuditEvent_ObjectIdentifier:
eventType = "ADMIN_ACTIVITY"
if ref.ObjectIdentifier.Type == auditV1.ObjectType_OBJECT_TYPE_ORGANIZATION {
messageContext = &LegacyAuditEventContext{
OrganizationId: nil,
FolderId: nil,
ProjectId: nil,
}
} else if ref.ObjectIdentifier.Type == auditV1.ObjectType_OBJECT_TYPE_FOLDER {
messageContext = &LegacyAuditEventContext{
OrganizationId: nil,
FolderId: nil,
ProjectId: nil,
}
} else if ref.ObjectIdentifier.Type == auditV1.ObjectType_OBJECT_TYPE_PROJECT {
messageContext = &LegacyAuditEventContext{
OrganizationId: nil,
FolderId: nil,
ProjectId: nil,
}
} else {
return nil, ErrUnsupportedObjectIdentifierType
}
case *auditV1.RoutableAuditEvent_ObjectName:
eventType = "SYSTEM_EVENT"
messageContext = nil
default:
return nil, ErrUnsupportedResourceReferenceType
}
// Details
var details map[string]interface{} = nil
if event.Details != nil {
details = event.Details.AsMap()
}
// Result
var result map[string]interface{} = nil
if event.Result != nil {
result = event.Result.AsMap()
}
// Instantiate the legacy event - missing values are filled with defaults
legacyAuditEvent := LegacyAuditEvent{
Severity: "INFO",
Visibility: routableEvent.Visibility.String(),
EventType: eventType,
EventTimeStamp: event.EventTimeStamp.AsTime(),
EventName: event.EventName,
SourceIpAddress: sourceIpAddress,
UserAgent: userAgent,
Initiator: LegacyAuditEventPrincipal{
Id: event.Initiator.Id,
Email: event.Initiator.Email,
},
ServiceAccountDelegationInfo: serviceAccountDelegationInfo,
Request: request,
Context: messageContext,
ResourceId: event.ResourceId,
ResourceName: event.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"`
}