Files
RenewCMS/internal/infrastructure/persistence/repositories/auditEventRepository.go
T

189 lines
5.5 KiB
Go

package persistence
import (
"RenewCMS/internal/domain/audit"
"RenewCMS/internal/infrastructure/persistence/mappers"
entity "RenewCMS/internal/infrastructure/persistence/models"
"strings"
"time"
"gorm.io/gorm"
)
const likeEscape = " ESCAPE '!'"
type AuditEventRepository struct {
db *gorm.DB
}
var _ audit.Repository = &AuditEventRepository{}
func NewAuditEventRepository(db *gorm.DB) *AuditEventRepository {
return &AuditEventRepository{db}
}
func (a *AuditEventRepository) Append(event audit.Event) (audit.Event, error) {
model, err := mappers.AuditEventToModel(event)
if err != nil {
return audit.Event{}, err
}
model.ID = 0
if err := a.db.Create(&model).Error; err != nil {
return audit.Event{}, err
}
return mappers.AuditEventToDomain(model)
}
func (a *AuditEventRepository) List(filter audit.Filter) (audit.Page, error) {
var total int64
if err := a.filtered(filter).Count(&total).Error; err != nil {
return audit.Page{}, err
}
query := a.filtered(filter).Order("created_at DESC, id DESC")
if filter.Size > 0 {
query = query.Limit(filter.Size).Offset(offsetOf(filter))
}
var found []entity.AuditEvent
if err := query.Find(&found).Error; err != nil {
return audit.Page{}, err
}
items := make([]audit.Event, 0, len(found))
for _, model := range found {
event, err := mappers.AuditEventToDomain(model)
if err != nil {
return audit.Page{}, err
}
items = append(items, event)
}
return audit.Page{Items: items, Total: int(total)}, nil
}
func offsetOf(filter audit.Filter) int {
if filter.Page < 1 {
return 0
}
return (filter.Page - 1) * filter.Size
}
func (a *AuditEventRepository) filtered(filter audit.Filter) *gorm.DB {
query := a.db.Model(&entity.AuditEvent{})
if filter.Actor != nil {
query = query.Where("actor_type = ?", string(filter.Actor.Type))
if filter.Actor.ID != nil {
query = query.Where("actor_id = ?", *filter.Actor.ID)
}
}
if filter.Category != "" {
query = query.Where("category = ?", string(filter.Category))
}
if filter.Outcome != "" {
query = query.Where("outcome = ?", string(filter.Outcome))
}
if filter.Security {
query = query.Where("security = ?", true)
}
if filter.From != nil {
query = query.Where("created_at >= ?", filter.From.UTC())
}
if filter.To != nil {
query = query.Where("created_at < ?", filter.To.UTC())
}
if filter.Query != "" {
pattern := containsPattern(strings.ToLower(filter.Query))
query = query.Where("(LOWER(actor_name) LIKE ?"+likeEscape+" OR LOWER(target_name) LIKE ?"+likeEscape+")", pattern, pattern)
}
return query
}
func (a *AuditEventRepository) Summary(query audit.SummaryQuery) (audit.Summary, error) {
var summary audit.Summary
totals, err := a.countsBetween(query.From, query.To)
if err != nil {
return audit.Summary{}, err
}
summary.Total, summary.Failed, summary.Security = totals.Total, totals.Failed, totals.Security
previous := query.Previous()
previousTotals, err := a.countsBetween(previous.From, previous.To)
if err != nil {
return audit.Summary{}, err
}
summary.PreviousTotal = previousTotals.Total
if summary.ActivePeople.Users, err = a.distinctActors(audit.ActorUser, query.From, query.To); err != nil {
return audit.Summary{}, err
}
if summary.ActivePeople.APIKeys, err = a.distinctActors(audit.ActorAPIKey, query.From, query.To); err != nil {
return audit.Summary{}, err
}
summary.Series, err = a.series(query)
return summary, err
}
type countsRow struct {
Total int
Failed int
Security int
}
func (a *AuditEventRepository) countsBetween(from time.Time, to time.Time) (countsRow, error) {
var row countsRow
err := a.db.Model(&entity.AuditEvent{}).
Select("COUNT(*) AS total, "+
"COALESCE(SUM(CASE WHEN outcome = ? THEN 1 ELSE 0 END), 0) AS failed, "+
"COALESCE(SUM(CASE WHEN security = ? THEN 1 ELSE 0 END), 0) AS security", string(audit.OutcomeFailed), true).
Where("created_at >= ? AND created_at < ?", from.UTC(), to.UTC()).
Scan(&row).Error
return row, err
}
func (a *AuditEventRepository) distinctActors(actorType audit.ActorType, from time.Time, to time.Time) (int, error) {
var count int64
err := a.db.Model(&entity.AuditEvent{}).
Where("actor_type = ? AND created_at >= ? AND created_at < ?", string(actorType), from.UTC(), to.UTC()).
Distinct("actor_id").
Count(&count).Error
return int(count), err
}
func (a *AuditEventRepository) series(query audit.SummaryQuery) (audit.Series, error) {
series := audit.Series{
Total: make([]int, audit.SeriesBuckets),
Active: make([]int, audit.SeriesBuckets),
Failed: make([]int, audit.SeriesBuckets),
Security: make([]int, audit.SeriesBuckets),
}
width := query.To.Sub(query.From) / audit.SeriesBuckets
if width <= 0 {
return series, nil
}
for i := 0; i < audit.SeriesBuckets; i++ {
from := query.From.Add(time.Duration(i) * width)
to := from.Add(width)
if i == audit.SeriesBuckets-1 {
to = query.To
}
counts, err := a.countsBetween(from, to)
if err != nil {
return audit.Series{}, err
}
users, err := a.distinctActors(audit.ActorUser, from, to)
if err != nil {
return audit.Series{}, err
}
keys, err := a.distinctActors(audit.ActorAPIKey, from, to)
if err != nil {
return audit.Series{}, err
}
series.Total[i], series.Failed[i], series.Security[i], series.Active[i] = counts.Total, counts.Failed, counts.Security, users+keys
}
return series, nil
}
func (a *AuditEventRepository) DeleteOlderThan(cutoff time.Time) (int, error) {
result := a.db.Where("created_at < ?", cutoff.UTC()).Delete(&entity.AuditEvent{})
return int(result.RowsAffected), result.Error
}