Gitfed
bastien-mrq/gitfed / internal / store / audit.go
package store

import (
	"encoding/binary"
	"encoding/json"
	"time"

	bolt "go.etcd.io/bbolt"
)

// AuditEvent is one entry in the instance's audit trail, per DESIGN.md §9
// Phase 3 ("Audit log des accès cross-instance").
type AuditEvent struct {
	Time      time.Time `json:"time"`
	Action    string    `json:"action"` // e.g. "ssh-auth", "git-upload-pack", "git-receive-pack", "trust-discover", "trust-approve"
	Principal string    `json:"principal,omitempty"`
	Repo      string    `json:"repo,omitempty"`
	Domain    string    `json:"domain,omitempty"`
	Allowed   bool      `json:"allowed"`
	Detail    string    `json:"detail,omitempty"`
}

// AppendAudit records an event. Entries are stored in insertion order under
// a monotonically increasing sequence key.
func (s *Store) AppendAudit(e AuditEvent) error {
	e.Time = time.Now().UTC()
	return s.db.Update(func(tx *bolt.Tx) error {
		b := tx.Bucket(bucketAudit)
		seq, err := b.NextSequence()
		if err != nil {
			return err
		}
		key := make([]byte, 8)
		binary.BigEndian.PutUint64(key, seq)
		data, err := json.Marshal(e)
		if err != nil {
			return err
		}
		return b.Put(key, data)
	})
}

// ListAudit returns up to limit of the most recent events, newest first.
func (s *Store) ListAudit(limit int) ([]AuditEvent, error) {
	var out []AuditEvent
	err := s.db.View(func(tx *bolt.Tx) error {
		c := tx.Bucket(bucketAudit).Cursor()
		for k, v := c.Last(); k != nil && len(out) < limit; k, v = c.Prev() {
			var e AuditEvent
			if err := json.Unmarshal(v, &e); err != nil {
				return err
			}
			out = append(out, e)
		}
		return nil
	})
	return out, err
}