188 lines
6.6 KiB
Go
188 lines
6.6 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type AuditLog struct {
|
|
ID string `json:"id"`
|
|
Category string `json:"category"`
|
|
Action string `json:"action"`
|
|
ActorGatewayUserID string `json:"actorGatewayUserId,omitempty"`
|
|
ActorUserID string `json:"actorUserId,omitempty"`
|
|
ActorUsername string `json:"actorUsername,omitempty"`
|
|
ActorSource string `json:"actorSource,omitempty"`
|
|
ActorRoles []string `json:"actorRoles,omitempty"`
|
|
TargetType string `json:"targetType"`
|
|
TargetID string `json:"targetId"`
|
|
TargetGatewayUserID string `json:"targetGatewayUserId,omitempty"`
|
|
TargetGatewayTenantID string `json:"targetGatewayTenantId,omitempty"`
|
|
RequestIP string `json:"requestIp,omitempty"`
|
|
UserAgent string `json:"userAgent,omitempty"`
|
|
BeforeState map[string]any `json:"beforeState,omitempty"`
|
|
AfterState map[string]any `json:"afterState,omitempty"`
|
|
Metadata map[string]any `json:"metadata,omitempty"`
|
|
CreatedAt time.Time `json:"createdAt"`
|
|
}
|
|
|
|
type AuditLogInput struct {
|
|
Category string `json:"category"`
|
|
Action string `json:"action"`
|
|
ActorGatewayUserID string `json:"actorGatewayUserId"`
|
|
ActorUserID string `json:"actorUserId"`
|
|
ActorUsername string `json:"actorUsername"`
|
|
ActorSource string `json:"actorSource"`
|
|
ActorRoles []string `json:"actorRoles"`
|
|
TargetType string `json:"targetType"`
|
|
TargetID string `json:"targetId"`
|
|
TargetGatewayUserID string `json:"targetGatewayUserId"`
|
|
TargetGatewayTenantID string `json:"targetGatewayTenantId"`
|
|
RequestIP string `json:"requestIp"`
|
|
UserAgent string `json:"userAgent"`
|
|
BeforeState map[string]any `json:"beforeState"`
|
|
AfterState map[string]any `json:"afterState"`
|
|
Metadata map[string]any `json:"metadata"`
|
|
}
|
|
|
|
type AuditLogFilter struct {
|
|
Category string
|
|
Action string
|
|
TargetType string
|
|
TargetID string
|
|
Limit int
|
|
}
|
|
|
|
func (s *Store) RecordAuditLog(ctx context.Context, input AuditLogInput) (AuditLog, error) {
|
|
return s.RecordAuditLogTx(ctx, s.pool, input)
|
|
}
|
|
|
|
func (s *Store) RecordAuditLogTx(ctx context.Context, tx Tx, input AuditLogInput) (AuditLog, error) {
|
|
input = normalizeAuditLogInput(input)
|
|
actorRolesJSON, _ := json.Marshal(input.ActorRoles)
|
|
beforeJSON, _ := json.Marshal(emptyObjectIfNil(input.BeforeState))
|
|
afterJSON, _ := json.Marshal(emptyObjectIfNil(input.AfterState))
|
|
metadataJSON, _ := json.Marshal(emptyObjectIfNil(input.Metadata))
|
|
return scanAuditLog(tx.QueryRow(ctx, `
|
|
INSERT INTO gateway_audit_logs (
|
|
category, action, actor_gateway_user_id, actor_user_id, actor_username, actor_source,
|
|
actor_roles, target_type, target_id, target_gateway_user_id, target_gateway_tenant_id,
|
|
request_ip, user_agent, before_state, after_state, metadata
|
|
)
|
|
VALUES (
|
|
$1, $2, NULLIF($3, '')::uuid, NULLIF($4, ''), NULLIF($5, ''), NULLIF($6, ''),
|
|
$7::jsonb, $8, $9, NULLIF($10, '')::uuid, NULLIF($11, '')::uuid,
|
|
NULLIF($12, ''), NULLIF($13, ''), $14::jsonb, $15::jsonb, $16::jsonb
|
|
)
|
|
RETURNING id::text, category, action, COALESCE(actor_gateway_user_id::text, ''), COALESCE(actor_user_id, ''),
|
|
COALESCE(actor_username, ''), COALESCE(actor_source, ''), actor_roles,
|
|
target_type, target_id, COALESCE(target_gateway_user_id::text, ''),
|
|
COALESCE(target_gateway_tenant_id::text, ''), COALESCE(request_ip, ''),
|
|
COALESCE(user_agent, ''), before_state, after_state, metadata, created_at`,
|
|
input.Category,
|
|
input.Action,
|
|
input.ActorGatewayUserID,
|
|
input.ActorUserID,
|
|
input.ActorUsername,
|
|
input.ActorSource,
|
|
string(actorRolesJSON),
|
|
input.TargetType,
|
|
input.TargetID,
|
|
input.TargetGatewayUserID,
|
|
input.TargetGatewayTenantID,
|
|
input.RequestIP,
|
|
input.UserAgent,
|
|
string(beforeJSON),
|
|
string(afterJSON),
|
|
string(metadataJSON),
|
|
))
|
|
}
|
|
|
|
func (s *Store) ListAuditLogs(ctx context.Context, filter AuditLogFilter) ([]AuditLog, error) {
|
|
limit := filter.Limit
|
|
if limit <= 0 {
|
|
limit = 100
|
|
}
|
|
if limit > 500 {
|
|
limit = 500
|
|
}
|
|
rows, err := s.pool.Query(ctx, `
|
|
SELECT id::text, category, action, COALESCE(actor_gateway_user_id::text, ''), COALESCE(actor_user_id, ''),
|
|
COALESCE(actor_username, ''), COALESCE(actor_source, ''), actor_roles,
|
|
target_type, target_id, COALESCE(target_gateway_user_id::text, ''),
|
|
COALESCE(target_gateway_tenant_id::text, ''), COALESCE(request_ip, ''),
|
|
COALESCE(user_agent, ''), before_state, after_state, metadata, created_at
|
|
FROM gateway_audit_logs
|
|
WHERE (NULLIF($1, '') IS NULL OR category = $1)
|
|
AND (NULLIF($2, '') IS NULL OR action = $2)
|
|
AND (NULLIF($3, '') IS NULL OR target_type = $3)
|
|
AND (NULLIF($4, '') IS NULL OR target_id = $4)
|
|
ORDER BY created_at DESC
|
|
LIMIT $5`,
|
|
strings.TrimSpace(filter.Category),
|
|
strings.TrimSpace(filter.Action),
|
|
strings.TrimSpace(filter.TargetType),
|
|
strings.TrimSpace(filter.TargetID),
|
|
limit,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := make([]AuditLog, 0)
|
|
for rows.Next() {
|
|
item, err := scanAuditLog(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, item)
|
|
}
|
|
return items, rows.Err()
|
|
}
|
|
|
|
func normalizeAuditLogInput(input AuditLogInput) AuditLogInput {
|
|
input.Category = firstNonEmpty(strings.TrimSpace(input.Category), "system")
|
|
input.Action = strings.TrimSpace(input.Action)
|
|
input.TargetType = strings.TrimSpace(input.TargetType)
|
|
input.TargetID = strings.TrimSpace(input.TargetID)
|
|
input.ActorSource = firstNonEmpty(strings.TrimSpace(input.ActorSource), "gateway")
|
|
return input
|
|
}
|
|
|
|
func scanAuditLog(row scanner) (AuditLog, error) {
|
|
var item AuditLog
|
|
var actorRoles []byte
|
|
var beforeState []byte
|
|
var afterState []byte
|
|
var metadata []byte
|
|
if err := row.Scan(
|
|
&item.ID,
|
|
&item.Category,
|
|
&item.Action,
|
|
&item.ActorGatewayUserID,
|
|
&item.ActorUserID,
|
|
&item.ActorUsername,
|
|
&item.ActorSource,
|
|
&actorRoles,
|
|
&item.TargetType,
|
|
&item.TargetID,
|
|
&item.TargetGatewayUserID,
|
|
&item.TargetGatewayTenantID,
|
|
&item.RequestIP,
|
|
&item.UserAgent,
|
|
&beforeState,
|
|
&afterState,
|
|
&metadata,
|
|
&item.CreatedAt,
|
|
); err != nil {
|
|
return AuditLog{}, err
|
|
}
|
|
item.ActorRoles = decodeStringArray(actorRoles)
|
|
item.BeforeState = decodeObject(beforeState)
|
|
item.AfterState = decodeObject(afterState)
|
|
item.Metadata = decodeObject(metadata)
|
|
return item, nil
|
|
}
|