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
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
|
|
} |