You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

421 lines
11 KiB
Go

package audit
import (
"fmt"
"time"
"github.com/knowfoolery/backend/shared/errors"
"github.com/knowfoolery/backend/shared/types"
)
type AuditLogLevel string
const (
AuditLogLevelInfo AuditLogLevel = "info"
AuditLogLevelWarning AuditLogLevel = "warning"
AuditLogLevelError AuditLogLevel = "error"
AuditLogLevelCritical AuditLogLevel = "critical"
)
type AuditActionType string
const (
AuditActionUserCreated AuditActionType = "user_created"
AuditActionUserUpdated AuditActionType = "user_updated"
AuditActionUserDeleted AuditActionType = "user_deleted"
AuditActionUserSuspended AuditActionType = "user_suspended"
AuditActionUserUnsuspended AuditActionType = "user_unsuspended"
AuditActionQuestionCreated AuditActionType = "question_created"
AuditActionQuestionUpdated AuditActionType = "question_updated"
AuditActionQuestionDeleted AuditActionType = "question_deleted"
AuditActionQuestionPublished AuditActionType = "question_published"
AuditActionQuestionUnpublished AuditActionType = "question_unpublished"
AuditActionThemeCreated AuditActionType = "theme_created"
AuditActionThemeUpdated AuditActionType = "theme_updated"
AuditActionThemeDeleted AuditActionType = "theme_deleted"
AuditActionLeaderboardCreated AuditActionType = "leaderboard_created"
AuditActionLeaderboardUpdated AuditActionType = "leaderboard_updated"
AuditActionLeaderboardDeleted AuditActionType = "leaderboard_deleted"
AuditActionLeaderboardReset AuditActionType = "leaderboard_reset"
AuditActionAdminLogin AuditActionType = "admin_login"
AuditActionAdminLogout AuditActionType = "admin_logout"
AuditActionAdminCreated AuditActionType = "admin_created"
AuditActionAdminRoleChanged AuditActionType = "admin_role_changed"
AuditActionSystemConfigUpdated AuditActionType = "system_config_updated"
AuditActionDataExported AuditActionType = "data_exported"
AuditActionDataImported AuditActionType = "data_imported"
AuditActionSecurityViolation AuditActionType = "security_violation"
)
type AuditLog struct {
id types.AuditLogID
adminID types.UserID
adminName string
sessionID types.AdminSessionID
action AuditActionType
resourceType string
resourceID string
level AuditLogLevel
message string
details map[string]interface{}
ipAddress string
userAgent string
timestamp time.Time
duration time.Duration
success bool
errorMessage string
tags []string
metadata map[string]interface{}
}
func NewAuditLog(
id types.AuditLogID,
adminID types.UserID,
adminName string,
sessionID types.AdminSessionID,
action AuditActionType,
resourceType, resourceID string,
) (*AuditLog, error) {
if id == "" {
return nil, errors.ErrValidationFailed("audit_log_id", "audit log ID cannot be empty")
}
if adminID == "" {
return nil, errors.ErrInvalidPlayerID
}
if adminName == "" {
return nil, errors.ErrValidationFailed("admin_name", "admin name cannot be empty")
}
if action == "" {
return nil, errors.ErrValidationFailed("action", "action cannot be empty")
}
return &AuditLog{
id: id,
adminID: adminID,
adminName: adminName,
sessionID: sessionID,
action: action,
resourceType: resourceType,
resourceID: resourceID,
level: AuditLogLevelInfo,
details: make(map[string]interface{}),
timestamp: time.Now(),
success: true,
tags: make([]string, 0),
metadata: make(map[string]interface{}),
}, nil
}
func (al *AuditLog) ID() types.AuditLogID {
return al.id
}
func (al *AuditLog) AdminID() types.UserID {
return al.adminID
}
func (al *AuditLog) AdminName() string {
return al.adminName
}
func (al *AuditLog) SessionID() types.AdminSessionID {
return al.sessionID
}
func (al *AuditLog) Action() AuditActionType {
return al.action
}
func (al *AuditLog) ResourceType() string {
return al.resourceType
}
func (al *AuditLog) ResourceID() string {
return al.resourceID
}
func (al *AuditLog) Level() AuditLogLevel {
return al.level
}
func (al *AuditLog) Message() string {
return al.message
}
func (al *AuditLog) Details() map[string]interface{} {
result := make(map[string]interface{})
for k, v := range al.details {
result[k] = v
}
return result
}
func (al *AuditLog) IPAddress() string {
return al.ipAddress
}
func (al *AuditLog) UserAgent() string {
return al.userAgent
}
func (al *AuditLog) Timestamp() time.Time {
return al.timestamp
}
func (al *AuditLog) Duration() time.Duration {
return al.duration
}
func (al *AuditLog) Success() bool {
return al.success
}
func (al *AuditLog) ErrorMessage() string {
return al.errorMessage
}
func (al *AuditLog) Tags() []string {
result := make([]string, len(al.tags))
copy(result, al.tags)
return result
}
func (al *AuditLog) Metadata() map[string]interface{} {
result := make(map[string]interface{})
for k, v := range al.metadata {
result[k] = v
}
return result
}
func (al *AuditLog) SetLevel(level AuditLogLevel) error {
switch level {
case AuditLogLevelInfo, AuditLogLevelWarning, AuditLogLevelError, AuditLogLevelCritical:
al.level = level
return nil
default:
return errors.ErrValidationFailed("level", "invalid audit log level")
}
}
func (al *AuditLog) SetMessage(message string) error {
if message == "" {
return errors.ErrValidationFailed("message", "message cannot be empty")
}
al.message = message
return nil
}
func (al *AuditLog) AddDetail(key string, value interface{}) error {
if key == "" {
return errors.ErrValidationFailed("detail_key", "detail key cannot be empty")
}
al.details[key] = value
return nil
}
func (al *AuditLog) RemoveDetail(key string) {
delete(al.details, key)
}
func (al *AuditLog) SetNetworkInfo(ipAddress, userAgent string) {
if ipAddress != "" {
al.ipAddress = ipAddress
}
if userAgent != "" {
al.userAgent = userAgent
}
}
func (al *AuditLog) SetDuration(duration time.Duration) {
if duration >= 0 {
al.duration = duration
}
}
func (al *AuditLog) SetSuccess(success bool) {
al.success = success
if success {
al.errorMessage = ""
if al.level == AuditLogLevelError || al.level == AuditLogLevelCritical {
al.level = AuditLogLevelInfo
}
}
}
func (al *AuditLog) SetError(errorMessage string) {
if errorMessage != "" {
al.success = false
al.errorMessage = errorMessage
if al.level == AuditLogLevelInfo {
al.level = AuditLogLevelError
}
}
}
func (al *AuditLog) AddTag(tag string) error {
if tag == "" {
return errors.ErrValidationFailed("tag", "tag cannot be empty")
}
for _, existingTag := range al.tags {
if existingTag == tag {
return nil
}
}
al.tags = append(al.tags, tag)
return nil
}
func (al *AuditLog) RemoveTag(tag string) {
newTags := make([]string, 0, len(al.tags))
for _, existingTag := range al.tags {
if existingTag != tag {
newTags = append(newTags, existingTag)
}
}
al.tags = newTags
}
func (al *AuditLog) HasTag(tag string) bool {
for _, existingTag := range al.tags {
if existingTag == tag {
return true
}
}
return false
}
func (al *AuditLog) SetMetadata(key string, value interface{}) error {
if key == "" {
return errors.ErrInvalidMetadata
}
al.metadata[key] = value
return nil
}
func (al *AuditLog) RemoveMetadata(key string) {
delete(al.metadata, key)
}
func (al *AuditLog) IsSecuritySensitive() bool {
switch al.action {
case AuditActionAdminLogin, AuditActionAdminLogout, AuditActionAdminCreated,
AuditActionAdminRoleChanged, AuditActionSecurityViolation:
return true
case AuditActionUserSuspended, AuditActionUserUnsuspended:
return true
case AuditActionSystemConfigUpdated:
return true
default:
return al.HasTag("security") || al.HasTag("sensitive")
}
}
func (al *AuditLog) IsHighPriority() bool {
return al.level == AuditLogLevelError || al.level == AuditLogLevelCritical || al.IsSecuritySensitive()
}
func (al *AuditLog) GetAge() time.Duration {
return time.Since(al.timestamp)
}
func (al *AuditLog) FormatSummary() string {
status := "SUCCESS"
if !al.success {
status = "FAILED"
}
return fmt.Sprintf("[%s] %s by %s (%s) - %s: %s",
al.level, al.action, al.adminName, al.adminID, status, al.message)
}
func (al *AuditLog) String() string {
return fmt.Sprintf("AuditLog{ID: %s, Action: %s, AdminID: %s, Resource: %s/%s, Success: %t}",
al.id, al.action, al.adminID, al.resourceType, al.resourceID, al.success)
}
type AuditLogSnapshot struct {
ID types.AuditLogID `json:"id"`
AdminID types.UserID `json:"admin_id"`
AdminName string `json:"admin_name"`
SessionID types.AdminSessionID `json:"session_id"`
Action AuditActionType `json:"action"`
ResourceType string `json:"resource_type"`
ResourceID string `json:"resource_id"`
Level AuditLogLevel `json:"level"`
Message string `json:"message"`
Details map[string]interface{} `json:"details"`
IPAddress string `json:"ip_address"`
UserAgent string `json:"user_agent"`
Timestamp time.Time `json:"timestamp"`
Duration time.Duration `json:"duration"`
Success bool `json:"success"`
ErrorMessage string `json:"error_message"`
Tags []string `json:"tags"`
Metadata map[string]interface{} `json:"metadata"`
}
func (al *AuditLog) ToSnapshot() AuditLogSnapshot {
return AuditLogSnapshot{
ID: al.id,
AdminID: al.adminID,
AdminName: al.adminName,
SessionID: al.sessionID,
Action: al.action,
ResourceType: al.resourceType,
ResourceID: al.resourceID,
Level: al.level,
Message: al.message,
Details: al.Details(),
IPAddress: al.ipAddress,
UserAgent: al.userAgent,
Timestamp: al.timestamp,
Duration: al.duration,
Success: al.success,
ErrorMessage: al.errorMessage,
Tags: al.Tags(),
Metadata: al.Metadata(),
}
}
func (al *AuditLog) FromSnapshot(snapshot AuditLogSnapshot) error {
if snapshot.ID == "" {
return errors.ErrValidationFailed("audit_log_id", "audit log ID cannot be empty")
}
if snapshot.AdminID == "" {
return errors.ErrInvalidPlayerID
}
al.id = snapshot.ID
al.adminID = snapshot.AdminID
al.adminName = snapshot.AdminName
al.sessionID = snapshot.SessionID
al.action = snapshot.Action
al.resourceType = snapshot.ResourceType
al.resourceID = snapshot.ResourceID
al.level = snapshot.Level
al.message = snapshot.Message
al.ipAddress = snapshot.IPAddress
al.userAgent = snapshot.UserAgent
al.timestamp = snapshot.Timestamp
al.duration = snapshot.Duration
al.success = snapshot.Success
al.errorMessage = snapshot.ErrorMessage
al.details = make(map[string]interface{})
for k, v := range snapshot.Details {
al.details[k] = v
}
al.tags = make([]string, len(snapshot.Tags))
copy(al.tags, snapshot.Tags)
al.metadata = make(map[string]interface{})
for k, v := range snapshot.Metadata {
al.metadata[k] = v
}
return nil
}