add basic audit log

new-sql-view
William Perron 12 months ago
parent 638083a755
commit b040694c2a
Signed by: wperron
GPG Key ID: BFDB4EF72D73C5F2

@ -11,6 +11,11 @@ func (s *Store) AddAbsence(ctx context.Context, session time.Time, userId string
return fmt.Errorf("not a monday") return fmt.Errorf("not a monday")
} }
defer s.Audit(&AuditableEvent{
userId: userId,
eventType: EventAbsence,
})
tx, err := s.db.Begin() tx, err := s.db.Begin()
if err != nil { if err != nil {
return fmt.Errorf("failed to begin transaction: %w", err) return fmt.Errorf("failed to begin transaction: %w", err)

@ -0,0 +1,103 @@
package themis
import (
"context"
"database/sql"
"errors"
"fmt"
"time"
"github.com/rs/zerolog/log"
)
type EventType int
const (
EventFlush EventType = iota
EventClaim
EventUnclaim
EventAbsence
)
func (et EventType) String() string {
switch et {
case EventFlush:
return "FLUSH"
case EventClaim:
return "CLAIM"
case EventUnclaim:
return "UNCLAIM"
case EventAbsence:
return "ABSENT"
default:
return ""
}
}
func EventTypeFromString(ev string) (EventType, error) {
switch ev {
case "FLUSH":
return EventFlush, nil
case "CLAIM":
return EventClaim, nil
case "UNCLAIM":
return EventUnclaim, nil
case "ABSENT":
return EventAbsence, nil
default:
return EventType(9999), errors.New("no such event type")
}
}
type AuditableEvent struct {
userId string
eventType EventType
Timestamp time.Time
}
// Audit writes to the audit table, returns nothing because it is meant to be
// used in a defered statement on functions that write to the database.
func (s *Store) Audit(ev *AuditableEvent) {
ctx := context.Background()
tx, err := s.db.Begin()
if err != nil {
log.Error().Err(err).Msg("failed to start transaction")
}
defer tx.Commit() //nolint:errcheck
stmt, err := s.db.PrepareContext(ctx, "INSERT INTO audit_log (userid, event_type, ts) VALUES (?, ?, ?)")
if err != nil {
log.Error().Err(err).Msg("failed to prepare audit log insert")
}
if _, err := stmt.ExecContext(ctx, ev.userId, ev.eventType.String(), time.Now()); err != nil {
log.Error().Err(err).Msg("failed to insert audit log")
}
}
func (s *Store) LastOf(ctx context.Context, t EventType) (AuditableEvent, error) {
stmt, err := s.db.PrepareContext(ctx, `SELECT userid, event_type, ts FROM audit_log WHERE event_type = ? ORDER BY ts DESC LIMIT 1`)
if err != nil {
return AuditableEvent{}, fmt.Errorf("failed to get last event of type %s: %w", t.String(), err)
}
row := stmt.QueryRowContext(ctx, t.String())
ev := AuditableEvent{}
var rawEventType string
err = row.Scan(&ev.userId, &rawEventType, &ev.Timestamp)
if err == sql.ErrNoRows {
return AuditableEvent{}, errors.New("")
}
if err != nil {
return AuditableEvent{}, fmt.Errorf("failed to scan row: %w", err)
}
ev.eventType, err = EventTypeFromString(rawEventType)
if err != nil {
return AuditableEvent{}, fmt.Errorf("failed to parse event type %s: %w", rawEventType, err)
}
return ev, nil
}

@ -675,7 +675,7 @@ func registerHandlers(sess *discordgo.Session, handlers map[string]Handler) {
sub := i.ModalSubmitData().Components[0].(*discordgo.ActionsRow).Components[0].(*discordgo.TextInput).Value sub := i.ModalSubmitData().Components[0].(*discordgo.ActionsRow).Components[0].(*discordgo.TextInput).Value
sub = strings.ToLower(sub) sub = strings.ToLower(sub)
if sub == "y" || sub == "ye" || sub == "yes" { if sub == "y" || sub == "ye" || sub == "yes" {
err := store.Flush(context.Background()) err := store.Flush(context.Background(), i.Member.User.ID)
msg := "Flushed all claims!" msg := "Flushed all claims!"
if err != nil { if err != nil {
log.Error().Err(err).Msg("failed to flush claims") log.Error().Err(err).Msg("failed to flush claims")

@ -0,0 +1,6 @@
CREATE TABLE IF NOT EXISTS audit_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
event_type TEXT,
userid TEXT,
ts TIMESTAMP
);

@ -58,6 +58,11 @@ func (s *Store) Close() error {
} }
func (s *Store) Claim(ctx context.Context, userId, player, province string, claimType ClaimType) (int, error) { func (s *Store) Claim(ctx context.Context, userId, player, province string, claimType ClaimType) (int, error) {
defer s.Audit(&AuditableEvent{
userId: userId,
eventType: EventClaim,
})
tx, err := s.db.Begin() tx, err := s.db.Begin()
if err != nil { if err != nil {
return 0, fmt.Errorf("failed to begin transaction: %w", err) return 0, fmt.Errorf("failed to begin transaction: %w", err)
@ -237,6 +242,11 @@ func (s *Store) DescribeClaim(ctx context.Context, ID int) (ClaimDetail, error)
} }
func (s *Store) DeleteClaim(ctx context.Context, ID int, userId string) error { func (s *Store) DeleteClaim(ctx context.Context, ID int, userId string) error {
defer s.Audit(&AuditableEvent{
userId: userId,
eventType: EventUnclaim,
})
stmt, err := s.db.PrepareContext(ctx, "DELETE FROM claims WHERE id = ? AND userid = ?") stmt, err := s.db.PrepareContext(ctx, "DELETE FROM claims WHERE id = ? AND userid = ?")
if err != nil { if err != nil {
return fmt.Errorf("failed to prepare query: %w", err) return fmt.Errorf("failed to prepare query: %w", err)
@ -272,7 +282,12 @@ func (s *Store) CountClaims(ctx context.Context) (total, uniquePlayers int, err
return total, uniquePlayers, nil return total, uniquePlayers, nil
} }
func (s *Store) Flush(ctx context.Context) error { func (s *Store) Flush(ctx context.Context, userId string) error {
defer s.Audit(&AuditableEvent{
userId: userId,
eventType: EventFlush,
})
_, err := s.db.ExecContext(ctx, "DELETE FROM claims;") _, err := s.db.ExecContext(ctx, "DELETE FROM claims;")
if err != nil { if err != nil {
return fmt.Errorf("failed to execute delete query: %w", err) return fmt.Errorf("failed to execute delete query: %w", err)

@ -212,7 +212,7 @@ func TestFlush(t *testing.T) {
store.Claim(context.TODO(), "000000000000000001", "foo", "Iberia", CLAIM_TYPE_REGION) store.Claim(context.TODO(), "000000000000000001", "foo", "Iberia", CLAIM_TYPE_REGION)
store.Claim(context.TODO(), "000000000000000001", "foo", "Ragusa", CLAIM_TYPE_TRADE) store.Claim(context.TODO(), "000000000000000001", "foo", "Ragusa", CLAIM_TYPE_TRADE)
assert.NoError(t, store.Flush(context.TODO())) assert.NoError(t, store.Flush(context.TODO(), "bob"))
claims, err := store.ListClaims(context.TODO()) claims, err := store.ListClaims(context.TODO())
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, 0, len(claims)) assert.Equal(t, 0, len(claims))

Loading…
Cancel
Save