311 lines
9.4 KiB
Go
311 lines
9.4 KiB
Go
package audit
|
|
|
|
import (
|
|
"RenewCMS/internal/domain/user"
|
|
"errors"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
var ErrInvalidFilter = errors.New("invalid activity filter")
|
|
var ErrInvalidRetention = errors.New("invalid activity retention period")
|
|
var ErrInvalidEvent = errors.New("invalid activity event")
|
|
var ErrActorNotAllowed = errors.New("activity of another actor is not allowed")
|
|
|
|
type ActorType string
|
|
|
|
const (
|
|
ActorUser ActorType = "user"
|
|
ActorAPIKey ActorType = "api_key"
|
|
ActorSystem ActorType = "system"
|
|
ActorAnonymous ActorType = "anonymous"
|
|
)
|
|
|
|
type Category string
|
|
|
|
const (
|
|
CategoryArticles Category = "articles"
|
|
CategoryMedia Category = "media"
|
|
CategoryUsers Category = "users"
|
|
CategoryAPIKeys Category = "api_keys"
|
|
CategoryAuth Category = "auth"
|
|
CategorySystem Category = "system"
|
|
CategorySites Category = "sites"
|
|
CategoryAuthors Category = "authors"
|
|
)
|
|
|
|
type Outcome string
|
|
|
|
const (
|
|
OutcomeSuccess Outcome = "success"
|
|
OutcomeFailed Outcome = "failed"
|
|
)
|
|
|
|
type Source string
|
|
|
|
const (
|
|
SourceWeb Source = "web"
|
|
SourceAPIKey Source = "api_key"
|
|
SourceSystem Source = "system"
|
|
)
|
|
|
|
type ChangeKind string
|
|
|
|
const (
|
|
ChangeText ChangeKind = "text"
|
|
ChangeBody ChangeKind = "body"
|
|
)
|
|
|
|
const (
|
|
ActionLogin = "auth.login"
|
|
ActionLoginFailed = "auth.login_failed"
|
|
ActionLogout = "auth.logout"
|
|
ActionPasswordResetRequest = "auth.password_reset_requested"
|
|
ActionPasswordResetDone = "auth.password_reset_completed"
|
|
ActionAPIKeyCreate = "api_key.create"
|
|
ActionAPIKeyRevoke = "api_key.revoke"
|
|
ActionRetentionChanged = "system.activity_retention_changed"
|
|
ActionCleanup = "system.activity_cleanup"
|
|
ActionArticleCreate = "article.create"
|
|
ActionArticleEdit = "article.edit"
|
|
ActionArticlePublish = "article.publish"
|
|
ActionArticleUnpublish = "article.unpublish"
|
|
ActionArticleDelete = "article.delete"
|
|
ActionSiteCreate = "site.create"
|
|
ActionSiteEdit = "site.edit"
|
|
ActionSiteSetDefault = "site.set_default"
|
|
ActionSiteDelete = "site.delete"
|
|
ActionAuthorCreate = "author.create"
|
|
ActionAuthorEdit = "author.edit"
|
|
ActionAuthorDelete = "author.delete"
|
|
ActionMediaUpload = "media.upload"
|
|
ActionMediaRename = "media.rename"
|
|
ActionMediaDelete = "media.delete"
|
|
ActionMediaMerge = "media.merge"
|
|
ActionMediaPurge = "media.purge"
|
|
ActionVariantCreate = "media.variant_create"
|
|
ActionVariantUpdate = "media.variant_update"
|
|
ActionVariantDelete = "media.variant_delete"
|
|
ActionVariantEnable = "media.variant_enable"
|
|
ActionVariantDisable = "media.variant_disable"
|
|
ActionVariantPurge = "media.variant_purge"
|
|
ActionVariantGenerateAll = "media.variant_generate_all"
|
|
ActionVariantPurgeImage = "media.variant_purge_image"
|
|
ActionVariantPurgeAll = "media.variant_purge_all"
|
|
ActionVariantPurgeUnused = "media.variant_purge_unused"
|
|
ActionUserRegister = "user.register"
|
|
ActionUserVerify = "user.verify"
|
|
ActionUserUpdateProfile = "user.update_profile"
|
|
ActionInviteCreate = "invite.create"
|
|
ActionInviteCancel = "invite.cancel"
|
|
ActionInviteRenew = "invite.renew"
|
|
ActionInviteAccept = "invite.accept"
|
|
ActionMediaPurgeScheduled = "system.media_purge"
|
|
ActionUpdateChecked = "system.update_checked"
|
|
ActionUpdateDownloaded = "system.update_downloaded"
|
|
ActionRestartRequested = "system.restart_requested"
|
|
)
|
|
|
|
const (
|
|
TargetAPIKey = "api_key"
|
|
TargetUser = "user"
|
|
TargetArticle = "article"
|
|
TargetImage = "image"
|
|
TargetVariant = "image_variant"
|
|
TargetInvite = "invite"
|
|
TargetRelease = "release"
|
|
TargetSite = "site"
|
|
TargetAuthor = "author"
|
|
)
|
|
|
|
const (
|
|
SystemActorName = "System"
|
|
ReasonWrongLogin = "Wrong username or password."
|
|
MaxUserAgentLength = 255
|
|
ParamRemoved = "removed"
|
|
ParamFrom = "from"
|
|
ParamTo = "to"
|
|
ParamMaxGBFrom = "max_gb_from"
|
|
ParamMaxGBTo = "max_gb_to"
|
|
ParamMerged = "merged"
|
|
ParamArticlesUpdated = "articles_updated"
|
|
ParamFailedGroups = "failed_groups"
|
|
ParamUpdateAvailable = "update_available"
|
|
ParamKind = "kind"
|
|
ParamPurgedFiles = "purged_files"
|
|
ParamPurgedBytes = "purged_bytes"
|
|
ParamFailedFiles = "failed_files"
|
|
ParamOlderThanDays = "older_than_days"
|
|
ParamGenerated = "generated"
|
|
ParamFailed = "failed"
|
|
ParamCauses = "causes"
|
|
ParamWidth = "width"
|
|
ParamHeight = "height"
|
|
ParamFit = "fit"
|
|
ParamFormat = "format"
|
|
ParamQuality = "quality"
|
|
ParamTag = "tag"
|
|
FieldTitle = "title"
|
|
FieldOnline = "online"
|
|
FieldBody = "body"
|
|
FieldFilename = "filename"
|
|
FieldDisplayName = "display_name"
|
|
FieldWidth = "width"
|
|
FieldHeight = "height"
|
|
FieldFit = "fit"
|
|
FieldFormat = "format"
|
|
FieldQuality = "quality"
|
|
FieldName = "name"
|
|
FieldBaseURL = "base_url"
|
|
FieldDefaultRobots = "default_robots"
|
|
FieldLocale = "locale"
|
|
FieldSocialImage = "default_social_image"
|
|
FieldSite = "site"
|
|
FieldSlug = "slug"
|
|
FieldBio = "bio"
|
|
FieldWebsite = "website"
|
|
FieldSocialLinks = "social_links"
|
|
FieldAvatar = "avatar"
|
|
FieldUser = "user"
|
|
FieldAuthors = "authors"
|
|
FieldCanonicalURL = "canonical_url"
|
|
FieldNoindex = "noindex"
|
|
FieldCoverImage = "cover_image"
|
|
FieldArticleSocialImage = "social_image"
|
|
FieldExcerpt = "excerpt"
|
|
FieldSEOTitle = "seo_title"
|
|
FieldSEODescription = "seo_description"
|
|
FieldSocialTitle = "social_title"
|
|
FieldSocialDescription = "social_description"
|
|
FieldCoverImageAlt = "cover_image_alt"
|
|
FieldSiteName = "site_name"
|
|
FieldTitleTemplate = "title_template"
|
|
FieldDefaultDescription = "default_description"
|
|
maxNameLength = 255
|
|
maxActionLength = 64
|
|
maxFailureReasonSize = 255
|
|
)
|
|
|
|
func ActorTypes() []ActorType {
|
|
return []ActorType{ActorUser, ActorAPIKey, ActorSystem, ActorAnonymous}
|
|
}
|
|
|
|
func Categories() []Category {
|
|
return []Category{CategoryArticles, CategoryMedia, CategoryUsers, CategoryAPIKeys, CategoryAuth, CategorySystem, CategorySites, CategoryAuthors}
|
|
}
|
|
|
|
func Outcomes() []Outcome {
|
|
return []Outcome{OutcomeSuccess, OutcomeFailed}
|
|
}
|
|
|
|
func Sources() []Source {
|
|
return []Source{SourceWeb, SourceAPIKey, SourceSystem}
|
|
}
|
|
|
|
func ChangeKinds() []ChangeKind {
|
|
return []ChangeKind{ChangeText, ChangeBody}
|
|
}
|
|
|
|
type Actor struct {
|
|
Type ActorType `json:"type"`
|
|
ID *uint32 `json:"id"`
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
type Target struct {
|
|
Type string `json:"type"`
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
type Change struct {
|
|
Field string `json:"field"`
|
|
Kind ChangeKind `json:"kind"`
|
|
Before *string `json:"before,omitempty"`
|
|
After *string `json:"after,omitempty"`
|
|
Delta *int `json:"delta,omitempty"`
|
|
}
|
|
|
|
type Event struct {
|
|
ID uint64 `json:"id"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
Action string `json:"action"`
|
|
Category Category `json:"category"`
|
|
Actor Actor `json:"actor"`
|
|
Target Target `json:"target"`
|
|
Outcome Outcome `json:"outcome"`
|
|
FailureReason *string `json:"failure_reason"`
|
|
Security bool `json:"security"`
|
|
Source Source `json:"source"`
|
|
IP *string `json:"ip"`
|
|
UserAgent *string `json:"user_agent"`
|
|
Params map[string]any `json:"params"`
|
|
Changes []Change `json:"changes"`
|
|
}
|
|
|
|
func NewEvent(action string, category Category) Event {
|
|
return Event{
|
|
Action: action,
|
|
Category: category,
|
|
Outcome: OutcomeSuccess,
|
|
Params: map[string]any{},
|
|
Changes: []Change{},
|
|
}
|
|
}
|
|
|
|
func SystemActor() Actor {
|
|
return Actor{Type: ActorSystem, Name: SystemActorName}
|
|
}
|
|
|
|
func UserActor(u user.User) Actor {
|
|
id := u.ID
|
|
name := strings.TrimSpace(u.DisplayName)
|
|
if name == "" {
|
|
name = u.Username
|
|
}
|
|
return Actor{Type: ActorUser, ID: &id, Name: name}
|
|
}
|
|
|
|
func AnonymousActor(name string) Actor {
|
|
return Actor{Type: ActorAnonymous, Name: name}
|
|
}
|
|
|
|
func APIKeyActor(id uint32, name string) Actor {
|
|
return Actor{Type: ActorAPIKey, ID: &id, Name: name}
|
|
}
|
|
|
|
func APIKeyTarget(id uint32, name string) Target {
|
|
return Target{Type: TargetAPIKey, ID: strconv.FormatUint(uint64(id), 10), Name: name}
|
|
}
|
|
|
|
func (e Event) Validate() error {
|
|
if e.Action == "" || len(e.Action) > maxActionLength {
|
|
return ErrInvalidEvent
|
|
}
|
|
if !slices.Contains(Categories(), e.Category) || !slices.Contains(Outcomes(), e.Outcome) || !slices.Contains(Sources(), e.Source) || !slices.Contains(ActorTypes(), e.Actor.Type) {
|
|
return ErrInvalidEvent
|
|
}
|
|
if e.FailureReason != nil && len(*e.FailureReason) > maxFailureReasonSize {
|
|
return ErrInvalidEvent
|
|
}
|
|
for _, change := range e.Changes {
|
|
if change.Kind != ChangeText && change.Kind != ChangeBody {
|
|
return ErrInvalidEvent
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (e Event) Truncated() Event {
|
|
e.Actor.Name = truncate(e.Actor.Name, maxNameLength)
|
|
e.Target.Name = truncate(e.Target.Name, maxNameLength)
|
|
return e
|
|
}
|
|
|
|
func truncate(value string, limit int) string {
|
|
runes := []rune(value)
|
|
return string(runes[:min(len(runes), limit)])
|
|
}
|