diff --git a/lib/policy/config/config.go b/lib/policy/config/config.go index 18aceb09..20979e4a 100644 --- a/lib/policy/config/config.go +++ b/lib/policy/config/config.go @@ -329,6 +329,7 @@ type fileConfig struct { OpenGraph openGraphFileConfig `json:"openGraph,omitempty"` Impressum *Impressum `json:"impressum,omitempty"` StatusCodes StatusCodes `json:"status_codes"` + Store *Store `json:"store"` Thresholds []Threshold `json:"thresholds"` } @@ -361,6 +362,12 @@ func (c *fileConfig) Valid() error { } } + if c.Store != nil { + if err := c.Store.Valid(); err != nil { + errs = append(errs, err) + } + } + if len(errs) != 0 { return fmt.Errorf("config is not valid:\n%w", errors.Join(errs...)) } @@ -374,6 +381,9 @@ func Load(fin io.Reader, fname string) (*Config, error) { Challenge: http.StatusOK, Deny: http.StatusOK, }, + Store: &Store{ + Backend: "memory", + }, } if err := yaml.NewYAMLToJSONDecoder(fin).Decode(&c); err != nil { @@ -392,6 +402,7 @@ func Load(fin io.Reader, fname string) (*Config, error) { Override: c.OpenGraph.Override, }, StatusCodes: c.StatusCodes, + Store: c.Store, } if c.OpenGraph.TimeToLive != "" { @@ -457,6 +468,7 @@ type Config struct { Impressum *Impressum OpenGraph OpenGraph StatusCodes StatusCodes + Store *Store } func (c Config) Valid() error { diff --git a/lib/policy/config/config_test.go b/lib/policy/config/config_test.go index 730b3d66..40bb6b43 100644 --- a/lib/policy/config/config_test.go +++ b/lib/policy/config/config_test.go @@ -1,4 +1,4 @@ -package config +package config_test import ( "errors" @@ -8,6 +8,7 @@ import ( "testing" "github.com/TecharoHQ/anubis/data" + . "github.com/TecharoHQ/anubis/lib/policy/config" ) func p[V any](v V) *V { return &v } @@ -325,37 +326,37 @@ func TestConfigValidBad(t *testing.T) { func TestBotConfigZero(t *testing.T) { var b BotConfig if !b.Zero() { - t.Error("zero value BotConfig is not zero value") + t.Error("zero value config.BotConfig is not zero value") } b.Name = "hi" if b.Zero() { - t.Error("BotConfig with name is zero value") + t.Error("config.BotConfig with name is zero value") } b.UserAgentRegex = p(".*") if b.Zero() { - t.Error("BotConfig with user agent regex is zero value") + t.Error("config.BotConfig with user agent regex is zero value") } b.PathRegex = p(".*") if b.Zero() { - t.Error("BotConfig with path regex is zero value") + t.Error("config.BotConfig with path regex is zero value") } b.HeadersRegex = map[string]string{"hi": "there"} if b.Zero() { - t.Error("BotConfig with headers regex is zero value") + t.Error("config.BotConfig with headers regex is zero value") } b.Action = RuleAllow if b.Zero() { - t.Error("BotConfig with action is zero value") + t.Error("config.BotConfig with action is zero value") } b.RemoteAddr = []string{"::/0"} if b.Zero() { - t.Error("BotConfig with remote addresses is zero value") + t.Error("config.BotConfig with remote addresses is zero value") } b.Challenge = &ChallengeRules{ @@ -364,6 +365,6 @@ func TestBotConfigZero(t *testing.T) { Algorithm: DefaultAlgorithm, } if b.Zero() { - t.Error("BotConfig with challenge rules is zero value") + t.Error("config.BotConfig with challenge rules is zero value") } } diff --git a/lib/policy/config/store.go b/lib/policy/config/store.go new file mode 100644 index 00000000..253ef5bd --- /dev/null +++ b/lib/policy/config/store.go @@ -0,0 +1,43 @@ +package config + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/TecharoHQ/anubis/lib/store" +) + +var ( + ErrNoStoreBackend = errors.New("config.Store: no backend defined") + ErrUnknownStoreBackend = errors.New("config.Store: unknown backend") +) + +type Store struct { + Backend string `json:"backend"` + Parameters json.RawMessage `json:"parameters"` +} + +func (s *Store) Valid() error { + var errs []error + + if len(s.Backend) == 0 { + errs = append(errs, ErrNoStoreBackend) + } + + fac, ok := store.Get(s.Backend) + switch ok { + case true: + if err := fac.Valid(s.Parameters); err != nil { + errs = append(errs, err) + } + case false: + errs = append(errs, fmt.Errorf("%w: %q", ErrUnknownStoreBackend, s.Backend)) + } + + if len(errs) != 0 { + return errors.Join(errs...) + } + + return nil +} diff --git a/lib/policy/config/store_test.go b/lib/policy/config/store_test.go new file mode 100644 index 00000000..eebdc6cf --- /dev/null +++ b/lib/policy/config/store_test.go @@ -0,0 +1,44 @@ +package config_test + +import ( + "errors" + "testing" + + "github.com/TecharoHQ/anubis/lib/policy/config" + _ "github.com/TecharoHQ/anubis/lib/store/memory" +) + +func TestStoreValid(t *testing.T) { + for _, tt := range []struct { + name string + input config.Store + err error + }{ + { + name: "no backend", + input: config.Store{}, + err: config.ErrNoStoreBackend, + }, + { + name: "in-memory backend", + input: config.Store{ + Backend: "memory", + }, + }, + { + name: "unknown backend", + input: config.Store{ + Backend: "taco salad", + }, + err: config.ErrUnknownStoreBackend, + }, + } { + t.Run(tt.name, func(t *testing.T) { + if err := tt.input.Valid(); !errors.Is(err, tt.err) { + t.Logf("want: %v", tt.err) + t.Logf("got: %v", err) + t.Error("invalid error returned") + } + }) + } +} diff --git a/lib/policy/policy.go b/lib/policy/policy.go index 9ee6efcb..3cf7909b 100644 --- a/lib/policy/policy.go +++ b/lib/policy/policy.go @@ -11,6 +11,7 @@ import ( "github.com/TecharoHQ/anubis/internal/thoth" "github.com/TecharoHQ/anubis/lib/policy/checker" "github.com/TecharoHQ/anubis/lib/policy/config" + "github.com/TecharoHQ/anubis/lib/store" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" ) @@ -35,9 +36,10 @@ type ParsedConfig struct { OpenGraph config.OpenGraph DefaultDifficulty int StatusCodes config.StatusCodes + Store store.Interface } -func NewParsedConfig(orig *config.Config) *ParsedConfig { +func newParsedConfig(orig *config.Config) *ParsedConfig { return &ParsedConfig{ orig: orig, OpenGraph: orig.OpenGraph, @@ -55,7 +57,7 @@ func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDiffic tc, hasThothClient := thoth.FromContext(ctx) - result := NewParsedConfig(c) + result := newParsedConfig(c) result.DefaultDifficulty = defaultDifficulty for _, b := range c.Bots { @@ -178,6 +180,19 @@ func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDiffic result.Thresholds = append(result.Thresholds, threshold) } + stFac, ok := store.Get(c.Store.Backend) + switch ok { + case true: + store, err := stFac.Build(ctx, c.Store.Parameters) + if err != nil { + validationErrs = append(validationErrs, err) + } + + result.Store = store + case false: + validationErrs = append(validationErrs, config.ErrUnknownStoreBackend) + } + if len(validationErrs) > 0 { return nil, fmt.Errorf("errors validating policy config JSON %s: %w", fname, errors.Join(validationErrs...)) } diff --git a/lib/store/all/all.go b/lib/store/all/all.go new file mode 100644 index 00000000..02c7351c --- /dev/null +++ b/lib/store/all/all.go @@ -0,0 +1,8 @@ +// Package all is a meta-package that imports all store implementations. +// +// This is a HACK to make tests work consistently. +package all + +import ( + _ "github.com/TecharoHQ/anubis/lib/store/memory" +)