diff --git a/docs/docs/CHANGELOG.md b/docs/docs/CHANGELOG.md index f278522..7092a87 100644 --- a/docs/docs/CHANGELOG.md +++ b/docs/docs/CHANGELOG.md @@ -41,6 +41,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add option to set `targetSNI` to special keyword 'auto' to indicate that it should be automatically set to the request Host name ([424](https://github.com/TecharoHQ/anubis/issues/424)). - The Preact challenge has been removed from the default configuration. It will be deprecated in the future. +### Potentially breaking changes + +#### Multiple checks at once has and-like semantics instead of or-like semantics + +Anubis lets you stack multiple checks at once with blocks like this: + +```yaml +name: allow-prometheus +action: ALLOW +user_agent_regex: ^prometheus-probe$ +remote_addresses: + - 192.168.2.0/24 +``` + +Previously, this only returned ALLOW if _any one_ of the conditions matched. This behaviour has changed to only return ALLOW if _all_ of the conditions match. I expect this to have some issues with user configs, however this fix is grave enough that it's worth the risk of breaking configs. If this bites you, please let me know so we can make an escape hatch. + ### Better error messages In order to make it easier for legitimate clients to debug issues with their browser configuration and Anubis, Anubis will emit internal error detail in base 64 so that administrators can chase down issues. Future versions of this may also include a variant that encrypts the error detail messages. diff --git a/lib/policy/checker/checker.go b/lib/policy/checker/checker.go index 1ee276a..31551cd 100644 --- a/lib/policy/checker/checker.go +++ b/lib/policy/checker/checker.go @@ -16,18 +16,24 @@ type Impl interface { type List []Impl +// Check runs each checker in the list against the request. +// It returns true only if *all* checkers return true (AND semantics). +// If any checker returns an error, the function returns false and the error. func (l List) Check(r *http.Request) (bool, error) { for _, c := range l { ok, err := c.Check(r) if err != nil { - return ok, err + // Propagate the error; overall result is false. + return false, err } - if ok { - return ok, nil + if !ok { + // One false means the combined result is false. Short-circuit + // so we don't waste time. + return false, err } } - - return false, nil + // Assume success until a checker says otherwise. + return true, nil } func (l List) Hash() string { diff --git a/lib/policy/checker/checker_test.go b/lib/policy/checker/checker_test.go new file mode 100644 index 0000000..22dd1cf --- /dev/null +++ b/lib/policy/checker/checker_test.go @@ -0,0 +1,57 @@ +package checker + +import ( + "errors" + "net/http" + "testing" +) + +// Mock implements the Impl interface for testing. +type Mock struct { + result bool + err error + hash string +} + +func (m Mock) Check(r *http.Request) (bool, error) { return m.result, m.err } +func (m Mock) Hash() string { return m.hash } + +func TestListCheck_AndSemantics(t *testing.T) { + req, _ := http.NewRequest(http.MethodGet, "http://example.com", nil) + + tests := []struct { + name string + list List + want bool + wantErr bool + }{ + { + name: "all true", + list: List{Mock{true, nil, "a"}, Mock{true, nil, "b"}}, + want: true, + }, + { + name: "one false", + list: List{Mock{true, nil, "a"}, Mock{false, nil, "b"}}, + want: false, + }, + { + name: "error propagates", + list: List{Mock{true, nil, "a"}, Mock{true, errors.New("boom"), "b"}}, + want: false, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.list.Check(req) + if (err != nil) != tt.wantErr { + t.Fatalf("unexpected error state: %v", err) + } + if got != tt.want { + t.Fatalf("expected %v, got %v", tt.want, got) + } + }) + } +}