220 lines
7.1 KiB
Go
220 lines
7.1 KiB
Go
package audit
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.apinb.com/ops/logs/internal/impl"
|
|
"git.apinb.com/ops/logs/internal/models"
|
|
)
|
|
|
|
const (
|
|
RiskNormal = "normal"
|
|
RiskDangerous = "dangerous"
|
|
|
|
ApprovalPending = "pending"
|
|
ApprovalApproved = "approved"
|
|
ApprovalRejected = "rejected"
|
|
)
|
|
|
|
type Record struct {
|
|
TraceID string `json:"trace_id,omitempty"`
|
|
SourceService string `json:"source_service,omitempty"`
|
|
ActorID string `json:"actor_id,omitempty"`
|
|
ActorName string `json:"actor_name,omitempty"`
|
|
Action string `json:"action,omitempty"`
|
|
ObjectType string `json:"object_type,omitempty"`
|
|
ObjectID string `json:"object_id,omitempty"`
|
|
OperationRisk string `json:"operation_risk,omitempty"`
|
|
ApprovalID string `json:"approval_id,omitempty"`
|
|
RequestMethod string `json:"request_method,omitempty"`
|
|
RequestPath string `json:"request_path,omitempty"`
|
|
ClientIP string `json:"client_ip,omitempty"`
|
|
BeforeJSON string `json:"before_json,omitempty"`
|
|
AfterJSON string `json:"after_json,omitempty"`
|
|
Result string `json:"result,omitempty"`
|
|
ErrorMessage string `json:"error_message,omitempty"`
|
|
}
|
|
|
|
type ApprovalRequest struct {
|
|
RequestID string `json:"request_id,omitempty"`
|
|
SourceService string `json:"source_service,omitempty"`
|
|
Action string `json:"action,omitempty"`
|
|
ObjectType string `json:"object_type,omitempty"`
|
|
ObjectID string `json:"object_id,omitempty"`
|
|
RequesterID string `json:"requester_id,omitempty"`
|
|
RequesterName string `json:"requester_name,omitempty"`
|
|
Reason string `json:"reason,omitempty"`
|
|
BeforeJSON string `json:"before_json,omitempty"`
|
|
AfterJSON string `json:"after_json,omitempty"`
|
|
Status string `json:"status,omitempty"`
|
|
ReviewerID string `json:"reviewer_id,omitempty"`
|
|
ReviewerName string `json:"reviewer_name,omitempty"`
|
|
ReviewComment string `json:"review_comment,omitempty"`
|
|
ReviewedAt *time.Time `json:"reviewed_at,omitempty"`
|
|
}
|
|
|
|
func NormalizeRecord(record Record) Record {
|
|
record.TraceID = strings.TrimSpace(record.TraceID)
|
|
record.SourceService = strings.TrimSpace(record.SourceService)
|
|
record.ActorID = strings.TrimSpace(record.ActorID)
|
|
record.ActorName = strings.TrimSpace(record.ActorName)
|
|
record.Action = strings.TrimSpace(record.Action)
|
|
record.ObjectType = strings.TrimSpace(record.ObjectType)
|
|
record.ObjectID = strings.TrimSpace(record.ObjectID)
|
|
record.OperationRisk = strings.TrimSpace(strings.ToLower(record.OperationRisk))
|
|
record.ApprovalID = strings.TrimSpace(record.ApprovalID)
|
|
record.RequestMethod = strings.TrimSpace(strings.ToUpper(record.RequestMethod))
|
|
record.RequestPath = strings.TrimSpace(record.RequestPath)
|
|
record.ClientIP = strings.TrimSpace(record.ClientIP)
|
|
record.Result = strings.TrimSpace(record.Result)
|
|
if record.Result == "" {
|
|
record.Result = "success"
|
|
}
|
|
if record.OperationRisk == "" {
|
|
if IsDangerousOperation(record.Action, record.ObjectType) {
|
|
record.OperationRisk = RiskDangerous
|
|
} else {
|
|
record.OperationRisk = RiskNormal
|
|
}
|
|
}
|
|
return record
|
|
}
|
|
|
|
func ValidateRecord(record Record) error {
|
|
record = NormalizeRecord(record)
|
|
if record.SourceService == "" {
|
|
return errors.New("source_service is required")
|
|
}
|
|
if record.ActorID == "" {
|
|
return errors.New("actor_id is required")
|
|
}
|
|
if record.Action == "" {
|
|
return errors.New("action is required")
|
|
}
|
|
if record.ObjectType == "" {
|
|
return errors.New("object_type is required")
|
|
}
|
|
if record.ObjectID == "" {
|
|
return errors.New("object_id is required")
|
|
}
|
|
if record.OperationRisk != RiskNormal && record.OperationRisk != RiskDangerous {
|
|
return errors.New("operation_risk must be normal or dangerous")
|
|
}
|
|
if record.OperationRisk == RiskDangerous && record.ApprovalID == "" {
|
|
return errors.New("approval_id is required for dangerous operation")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func IsDangerousOperation(action, objectType string) bool {
|
|
key := strings.ToLower(strings.TrimSpace(action) + " " + strings.TrimSpace(objectType))
|
|
dangerWords := []string{
|
|
"notification_policy",
|
|
"notification policy",
|
|
"silence_policy",
|
|
"suppression",
|
|
"escalation_policy",
|
|
"automation_script",
|
|
"script.execute",
|
|
"script.rollback",
|
|
}
|
|
for _, word := range dangerWords {
|
|
if strings.Contains(key, word) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func SaveRecord(record Record) (models.AuditLog, error) {
|
|
record = NormalizeRecord(record)
|
|
if err := ValidateRecord(record); err != nil {
|
|
return models.AuditLog{}, err
|
|
}
|
|
row := models.AuditLog{
|
|
TraceID: record.TraceID,
|
|
SourceService: record.SourceService,
|
|
ActorID: record.ActorID,
|
|
ActorName: record.ActorName,
|
|
Action: record.Action,
|
|
ObjectType: record.ObjectType,
|
|
ObjectID: record.ObjectID,
|
|
OperationRisk: record.OperationRisk,
|
|
ApprovalID: record.ApprovalID,
|
|
RequestMethod: record.RequestMethod,
|
|
RequestPath: record.RequestPath,
|
|
ClientIP: record.ClientIP,
|
|
BeforeJSON: record.BeforeJSON,
|
|
AfterJSON: record.AfterJSON,
|
|
Result: record.Result,
|
|
ErrorMessage: record.ErrorMessage,
|
|
}
|
|
if err := impl.DBService.Create(&row).Error; err != nil {
|
|
return models.AuditLog{}, err
|
|
}
|
|
return row, nil
|
|
}
|
|
|
|
func NormalizeApproval(req ApprovalRequest) ApprovalRequest {
|
|
req.RequestID = strings.TrimSpace(req.RequestID)
|
|
req.SourceService = strings.TrimSpace(req.SourceService)
|
|
req.Action = strings.TrimSpace(req.Action)
|
|
req.ObjectType = strings.TrimSpace(req.ObjectType)
|
|
req.ObjectID = strings.TrimSpace(req.ObjectID)
|
|
req.RequesterID = strings.TrimSpace(req.RequesterID)
|
|
req.RequesterName = strings.TrimSpace(req.RequesterName)
|
|
req.Status = strings.TrimSpace(strings.ToLower(req.Status))
|
|
req.ReviewerID = strings.TrimSpace(req.ReviewerID)
|
|
req.ReviewerName = strings.TrimSpace(req.ReviewerName)
|
|
if req.Status == "" {
|
|
req.Status = ApprovalPending
|
|
}
|
|
return req
|
|
}
|
|
|
|
func ValidateApprovalRequest(req ApprovalRequest) error {
|
|
req = NormalizeApproval(req)
|
|
if req.SourceService == "" {
|
|
return errors.New("source_service is required")
|
|
}
|
|
if req.Action == "" {
|
|
return errors.New("action is required")
|
|
}
|
|
if req.ObjectType == "" {
|
|
return errors.New("object_type is required")
|
|
}
|
|
if req.ObjectID == "" {
|
|
return errors.New("object_id is required")
|
|
}
|
|
if req.RequesterID == "" {
|
|
return errors.New("requester_id is required")
|
|
}
|
|
if !IsDangerousOperation(req.Action, req.ObjectType) {
|
|
return errors.New("operation is not classified as dangerous")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func Transition(req ApprovalRequest, nextStatus, reviewerID, comment string) (ApprovalRequest, error) {
|
|
req = NormalizeApproval(req)
|
|
nextStatus = strings.TrimSpace(strings.ToLower(nextStatus))
|
|
reviewerID = strings.TrimSpace(reviewerID)
|
|
if req.Status != ApprovalPending {
|
|
return ApprovalRequest{}, errors.New("only pending approval can be reviewed")
|
|
}
|
|
if nextStatus != ApprovalApproved && nextStatus != ApprovalRejected {
|
|
return ApprovalRequest{}, errors.New("next status must be approved or rejected")
|
|
}
|
|
if reviewerID == "" {
|
|
return ApprovalRequest{}, errors.New("reviewer_id is required")
|
|
}
|
|
now := time.Now()
|
|
req.Status = nextStatus
|
|
req.ReviewerID = reviewerID
|
|
req.ReviewComment = strings.TrimSpace(comment)
|
|
req.ReviewedAt = &now
|
|
return req, nil
|
|
}
|