diff --git a/.schema/version.schema.json b/.schema/version.schema.json index 0a3fad47d3..fa395b396d 100644 --- a/.schema/version.schema.json +++ b/.schema/version.schema.json @@ -1,244 +1,216 @@ { - "$id": "https://github.com/ory/oathkeeper/.schema/version.schema.json", - "$schema": "http://json-schema.org/draft-07/schema#", - "oneOf": [ - { - "allOf": [ - { - "properties": { - "version": { - "const": "v0.40.3" - } - }, - "required": [ - "version" - ] - }, - { - "$ref": "https://raw.githubusercontent.com/ory/oathkeeper/v0.40.3/.schema/config.schema.json" - } - ] + "$id": "https://github.com/ory/oathkeeper/.schema/version.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "oneOf": [ + { + "allOf": [ + { + "properties": { + "version": { + "const": "v0.40.3" + } + }, + "required": ["version"] }, { - "allOf": [ - { - "properties": { - "version": { - "const": "v0.40.2" - } - }, - "required": [ - "version" - ] - }, - { - "$ref": "https://raw.githubusercontent.com/ory/oathkeeper/v0.40.2/.schema/config.schema.json" - } - ] + "$ref": "https://raw.githubusercontent.com/ory/oathkeeper/v0.40.3/.schema/config.schema.json" + } + ] + }, + { + "allOf": [ + { + "properties": { + "version": { + "const": "v0.40.2" + } + }, + "required": ["version"] }, { - "allOf": [ - { - "properties": { - "version": { - "const": "v0.40.1" - } - }, - "required": [ - "version" - ] - }, - { - "$ref": "https://raw.githubusercontent.com/ory/oathkeeper/v0.40.1/spec/config.schema.json" - } - ] + "$ref": "https://raw.githubusercontent.com/ory/oathkeeper/v0.40.2/.schema/config.schema.json" + } + ] + }, + { + "allOf": [ + { + "properties": { + "version": { + "const": "v0.40.1" + } + }, + "required": ["version"] }, { - "allOf": [ - { - "properties": { - "version": { - "const": "v0.40.0" - } - }, - "required": [ - "version" - ] - }, - { - "$ref": "https://raw.githubusercontent.com/ory/oathkeeper/v0.40.0/spec/config.schema.json" - } - ] + "$ref": "https://raw.githubusercontent.com/ory/oathkeeper/v0.40.1/spec/config.schema.json" + } + ] + }, + { + "allOf": [ + { + "properties": { + "version": { + "const": "v0.40.0" + } + }, + "required": ["version"] }, { - "allOf": [ - { - "properties": { - "version": { - "const": "v0.38.4-beta.1" - } - }, - "required": [ - "version" - ] - }, - { - "$ref": "https://raw.githubusercontent.com/ory/oathkeeper/v0.38.4-beta.1/.schema/config.schema.json" - } - ] + "$ref": "https://raw.githubusercontent.com/ory/oathkeeper/v0.40.0/spec/config.schema.json" + } + ] + }, + { + "allOf": [ + { + "properties": { + "version": { + "const": "v0.38.4-beta.1" + } + }, + "required": ["version"] }, { - "allOf": [ - { - "properties": { - "version": { - "const": "v0.38.5-beta.1" - } - }, - "required": [ - "version" - ] - }, - { - "$ref": "https://raw.githubusercontent.com/ory/oathkeeper/v0.38.5-beta.1/.schema/config.schema.json" - } - ] + "$ref": "https://raw.githubusercontent.com/ory/oathkeeper/v0.38.4-beta.1/.schema/config.schema.json" + } + ] + }, + { + "allOf": [ + { + "properties": { + "version": { + "const": "v0.38.5-beta.1" + } + }, + "required": ["version"] }, { - "allOf": [ - { - "properties": { - "version": { - "const": "v0.38.9-beta.1" - } - }, - "required": [ - "version" - ] - }, - { - "$ref": "https://raw.githubusercontent.com/ory/oathkeeper/v0.38.9-beta.1/.schema/config.schema.json" - } - ] + "$ref": "https://raw.githubusercontent.com/ory/oathkeeper/v0.38.5-beta.1/.schema/config.schema.json" + } + ] + }, + { + "allOf": [ + { + "properties": { + "version": { + "const": "v0.38.9-beta.1" + } + }, + "required": ["version"] }, { - "allOf": [ - { - "properties": { - "version": { - "const": "v0.38.14-beta.1" - } - }, - "required": [ - "version" - ] - }, - { - "$ref": "https://raw.githubusercontent.com/ory/oathkeeper/v0.38.14-beta.1/.schema/config.schema.json" - } - ] + "$ref": "https://raw.githubusercontent.com/ory/oathkeeper/v0.38.9-beta.1/.schema/config.schema.json" + } + ] + }, + { + "allOf": [ + { + "properties": { + "version": { + "const": "v0.38.14-beta.1" + } + }, + "required": ["version"] }, { - "allOf": [ - { - "properties": { - "version": { - "const": "v0.38.15-beta.1" - } - }, - "required": [ - "version" - ] - }, - { - "$ref": "https://raw.githubusercontent.com/ory/oathkeeper/v0.38.15-beta.1/.schema/config.schema.json" - } - ] + "$ref": "https://raw.githubusercontent.com/ory/oathkeeper/v0.38.14-beta.1/.schema/config.schema.json" + } + ] + }, + { + "allOf": [ + { + "properties": { + "version": { + "const": "v0.38.15-beta.1" + } + }, + "required": ["version"] }, { - "allOf": [ - { - "properties": { - "version": { - "const": "v0.38.17-beta.1" - } - }, - "required": [ - "version" - ] - }, - { - "$ref": "https://raw.githubusercontent.com/ory/oathkeeper/v0.38.17-beta.1/.schema/config.schema.json" - } - ] + "$ref": "https://raw.githubusercontent.com/ory/oathkeeper/v0.38.15-beta.1/.schema/config.schema.json" + } + ] + }, + { + "allOf": [ + { + "properties": { + "version": { + "const": "v0.38.17-beta.1" + } + }, + "required": ["version"] }, { - "allOf": [ - { - "properties": { - "version": { - "const": "v0.38.19-beta.1" - } - }, - "required": [ - "version" - ] - }, - { - "$ref": "https://raw.githubusercontent.com/ory/oathkeeper/v0.38.19-beta.1/.schema/config.schema.json" - } - ] + "$ref": "https://raw.githubusercontent.com/ory/oathkeeper/v0.38.17-beta.1/.schema/config.schema.json" + } + ] + }, + { + "allOf": [ + { + "properties": { + "version": { + "const": "v0.38.19-beta.1" + } + }, + "required": ["version"] }, { - "allOf": [ - { - "properties": { - "version": { - "const": "v0.38.20-beta.1" - } - }, - "required": [ - "version" - ] - }, - { - "$ref": "https://raw.githubusercontent.com/ory/oathkeeper/v0.38.20-beta.1/.schema/config.schema.json" - } - ] + "$ref": "https://raw.githubusercontent.com/ory/oathkeeper/v0.38.19-beta.1/.schema/config.schema.json" + } + ] + }, + { + "allOf": [ + { + "properties": { + "version": { + "const": "v0.38.20-beta.1" + } + }, + "required": ["version"] }, { - "allOf": [ - { - "oneOf": [ - { - "properties": { - "version": { - "type": "string", - "maxLength": 0 - } - }, - "required": [ - "version" - ] - }, - { - "not": { - "properties": { - "version": {} - }, - "required": [ - "version" - ] - } - } - ] - }, - { - "$ref": "#/oneOf/0/allOf/1" + "$ref": "https://raw.githubusercontent.com/ory/oathkeeper/v0.38.20-beta.1/.schema/config.schema.json" + } + ] + }, + { + "allOf": [ + { + "oneOf": [ + { + "properties": { + "version": { + "type": "string", + "maxLength": 0 } - ] + }, + "required": ["version"] + }, + { + "not": { + "properties": { + "version": {} + }, + "required": ["version"] + } + } + ] + }, + { + "$ref": "#/oneOf/0/allOf/1" } - ], - "title": "All Versions of the ORY Oathkeeper Configuration", - "type": "object" -} \ No newline at end of file + ] + } + ], + "title": "All Versions of the ORY Oathkeeper Configuration", + "type": "object" +} diff --git a/api/decision.go b/api/decision.go index d1cfbb37dd..ed3230dbff 100644 --- a/api/decision.go +++ b/api/decision.go @@ -83,7 +83,7 @@ func (h *DecisionHandler) decisions(w http.ResponseWriter, r *http.Request) { fields["subject"] = sess.Subject } - rl, err := h.r.RuleMatcher().Match(r.Context(), r.Method, r.URL, rule.ProtocolHTTP) + rl, err := h.r.RuleMatcher().Match(r.Context(), r.Method, r.URL, r.Header, rule.ProtocolHTTP) if err != nil { h.r.Logger().WithError(err). WithFields(fields). diff --git a/api/decision_test.go b/api/decision_test.go index d4c6d07236..c2828ab40b 100644 --- a/api/decision_test.go +++ b/api/decision_test.go @@ -352,7 +352,7 @@ func (*decisionHandlerRegistryMock) Logger() *logrusx.Logger { return logrusx.New("", "") } -func (m *decisionHandlerRegistryMock) Match(ctx context.Context, method string, u *url.URL, _ rule.Protocol) (*rule.Rule, error) { +func (m *decisionHandlerRegistryMock) Match(ctx context.Context, method string, u *url.URL, _ http.Header, _ rule.Protocol) (*rule.Rule, error) { args := m.Called(ctx, method, u) return args.Get(0).(*rule.Rule), args.Error(1) } diff --git a/middleware/grpc_middleware.go b/middleware/grpc_middleware.go index 45fec6380d..0bd96cce8d 100644 --- a/middleware/grpc_middleware.go +++ b/middleware/grpc_middleware.go @@ -89,7 +89,7 @@ func (m *middleware) unaryInterceptor(ctx context.Context, req interface{}, info log.Debug("matching HTTP request build from gRPC") - r, err := m.RuleMatcher().Match(traceCtx, httpReq.Method, httpReq.URL, rule.ProtocolGRPC) + r, err := m.RuleMatcher().Match(traceCtx, httpReq.Method, httpReq.URL, httpReq.Header, rule.ProtocolGRPC) if err != nil { log.WithError(err).Warn("could not find a matching rule") span.SetAttributes(attribute.String("oathkeeper.verdict", "denied")) @@ -138,7 +138,7 @@ func (m *middleware) streamInterceptor( log.Debug("matching HTTP request build from gRPC") - r, err := m.RuleMatcher().Match(ctx, httpReq.Method, httpReq.URL, rule.ProtocolGRPC) + r, err := m.RuleMatcher().Match(ctx, httpReq.Method, httpReq.URL, httpReq.Header, rule.ProtocolGRPC) if err != nil { log.WithError(err).Warn("could not find a matching rule") span.SetAttributes(attribute.String("oathkeeper.verdict", "denied")) diff --git a/proxy/proxy.go b/proxy/proxy.go index 3f9bea36bf..e85a397ab1 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -108,7 +108,7 @@ func (d *Proxy) RoundTrip(r *http.Request) (*http.Response, error) { func (d *Proxy) Rewrite(r *httputil.ProxyRequest) { EnrichRequestedURL(r) - rl, err := d.r.RuleMatcher().Match(r.Out.Context(), r.Out.Method, r.Out.URL, rule.ProtocolHTTP) + rl, err := d.r.RuleMatcher().Match(r.Out.Context(), r.Out.Method, r.Out.URL, r.Out.Header, rule.ProtocolHTTP) if err != nil { *r.Out = *r.Out.WithContext(context.WithValue(r.Out.Context(), director, err)) return diff --git a/rule/matcher.go b/rule/matcher.go index 69d7b2e75e..6d37368bdb 100644 --- a/rule/matcher.go +++ b/rule/matcher.go @@ -5,6 +5,7 @@ package rule import ( "context" + "net/http" "net/url" ) @@ -12,7 +13,7 @@ type ( Protocol int Matcher interface { - Match(ctx context.Context, method string, u *url.URL, protocol Protocol) (*Rule, error) + Match(ctx context.Context, method string, u *url.URL, headers http.Header, protocol Protocol) (*Rule, error) } ) diff --git a/rule/matcher_test.go b/rule/matcher_test.go index 28ee2d051e..70a17486d9 100644 --- a/rule/matcher_test.go +++ b/rule/matcher_test.go @@ -6,6 +6,7 @@ package rule import ( "context" "fmt" + "net/http" "net/url" "testing" @@ -49,6 +50,15 @@ var testRules = []Rule{ Mutators: []Handler{{Handler: "id_token"}}, Upstream: Upstream{URL: "http://localhost:3333/", StripPath: "/foo", PreserveHost: false}, }, + { + ID: "foo4", + Match: &Match{URL: "https://localhost:343/", Methods: []string{"PATCH"}, Headers: http.Header{"Content-Type": {"application/some-app.v2+json"}}}, + Description: "Patch users rule for version 2", + Authorizer: Handler{Handler: "deny"}, + Authenticators: []Handler{{Handler: "oauth2_introspection"}}, + Mutators: []Handler{{Handler: "id_token"}}, + Upstream: Upstream{URL: "http://localhost:3333/", StripPath: "/foo", PreserveHost: false}, + }, { ID: "grpc1", Match: &MatchGRPC{Authority: ".example.com", FullMethod: "grpc.api/Call"}, @@ -88,6 +98,15 @@ var testRulesGlob = []Rule{ Mutators: []Handler{{Handler: "id_token"}}, Upstream: Upstream{URL: "http://localhost:3333/", StripPath: "/foo", PreserveHost: false}, }, + { + ID: "foo4", + Match: &Match{URL: "https://localhost:343/<{baz*,bar*}>", Methods: []string{"PATCH"}, Headers: http.Header{"Content-Type": {"application/some-app.v2+json"}}}, + Description: "Patch users rule with version 2", + Authorizer: Handler{Handler: "deny"}, + Authenticators: []Handler{{Handler: "oauth2_introspection"}}, + Mutators: []Handler{{Handler: "id_token"}}, + Upstream: Upstream{URL: "http://localhost:3333/", StripPath: "/foo", PreserveHost: false}, + }, { ID: "grpc1", Match: &MatchGRPC{Authority: "<{baz*,bar*}>.example.com", FullMethod: "grpc.api/Call"}, @@ -97,6 +116,15 @@ var testRulesGlob = []Rule{ Mutators: []Handler{{Handler: "id_token", Config: []byte(`{"issuer":"anything"}`)}}, Upstream: Upstream{URL: "http://bar.example.com/", PreserveHost: false}, }, + { + ID: "grpc2", + Match: &MatchGRPC{Authority: "<{baz*,bar*}>.example.com", FullMethod: "grpc.api/CallWithHeader", Headers: http.Header{"Content-Type": {"application/some-app.v2+json"}}}, + Description: "gRPC Rule with version 2", + Authorizer: Handler{Handler: "allow", Config: []byte(`{"type":"any"}`)}, + Authenticators: []Handler{{Handler: "anonymous", Config: []byte(`{"name":"anonymous1"}`)}}, + Mutators: []Handler{{Handler: "id_token", Config: []byte(`{"issuer":"anything"}`)}}, + Upstream: Upstream{URL: "http://bar.example.com/", PreserveHost: false}, + }, } func TestMatcher(t *testing.T) { @@ -105,8 +133,8 @@ func TestMatcher(t *testing.T) { Repository } - var testMatcher = func(t *testing.T, matcher Matcher, method string, url string, protocol Protocol, expectErr bool, expect *Rule) { - r, err := matcher.Match(context.Background(), method, mustParseURL(t, url), protocol) + var testMatcher = func(t *testing.T, matcher Matcher, method string, url string, headers http.Header, protocol Protocol, expectErr bool, expect *Rule) { + r, err := matcher.Match(context.Background(), method, mustParseURL(t, url), headers, protocol) if expectErr { require.Error(t, err) } else { @@ -121,24 +149,24 @@ func TestMatcher(t *testing.T) { } { t.Run(fmt.Sprintf("regexp matcher=%s", name), func(t *testing.T) { t.Run("case=empty", func(t *testing.T) { - testMatcher(t, matcher, "GET", "https://localhost:34/baz", ProtocolHTTP, true, nil) - testMatcher(t, matcher, "POST", "https://localhost:1234/foo", ProtocolHTTP, true, nil) - testMatcher(t, matcher, "DELETE", "https://localhost:1234/foo", ProtocolHTTP, true, nil) + testMatcher(t, matcher, "GET", "https://localhost:34/baz", http.Header{}, ProtocolHTTP, true, nil) + testMatcher(t, matcher, "POST", "https://localhost:1234/foo", http.Header{}, ProtocolHTTP, true, nil) + testMatcher(t, matcher, "DELETE", "https://localhost:1234/foo", http.Header{}, ProtocolHTTP, true, nil) }) require.NoError(t, matcher.Set(context.Background(), testRules)) t.Run("case=created", func(t *testing.T) { - testMatcher(t, matcher, "GET", "https://localhost:34/baz", ProtocolHTTP, false, &testRules[1]) - testMatcher(t, matcher, "GET", "https://localhost:34/baz", ProtocolGRPC, true, nil) - testMatcher(t, matcher, "POST", "https://localhost:1234/foo", ProtocolHTTP, false, &testRules[0]) - testMatcher(t, matcher, "POST", "https://localhost:1234/foo", ProtocolGRPC, true, nil) - testMatcher(t, matcher, "DELETE", "https://localhost:1234/foo", ProtocolHTTP, true, nil) - testMatcher(t, matcher, "POST", "grpc://bar.example.com/grpc.api/Call", ProtocolGRPC, false, &testRules[3]) + testMatcher(t, matcher, "GET", "https://localhost:34/baz", http.Header{}, ProtocolHTTP, false, &testRules[1]) + testMatcher(t, matcher, "GET", "https://localhost:34/baz", http.Header{}, ProtocolGRPC, true, nil) + testMatcher(t, matcher, "POST", "https://localhost:1234/foo", http.Header{}, ProtocolHTTP, false, &testRules[0]) + testMatcher(t, matcher, "POST", "https://localhost:1234/foo", http.Header{}, ProtocolGRPC, true, nil) + testMatcher(t, matcher, "DELETE", "https://localhost:1234/foo", http.Header{}, ProtocolHTTP, true, nil) + testMatcher(t, matcher, "POST", "grpc://bar.example.com/grpc.api/Call", http.Header{}, ProtocolGRPC, false, &testRules[4]) }) t.Run("case=cache", func(t *testing.T) { - r, err := matcher.Match(context.Background(), "GET", mustParseURL(t, "https://localhost:34/baz"), ProtocolHTTP) + r, err := matcher.Match(context.Background(), "GET", mustParseURL(t, "https://localhost:34/baz"), http.Header{}, ProtocolHTTP) require.NoError(t, err) got, err := matcher.Get(context.Background(), r.ID) require.NoError(t, err) @@ -146,38 +174,42 @@ func TestMatcher(t *testing.T) { }) t.Run("case=nil url", func(t *testing.T) { - _, err := matcher.Match(context.Background(), "GET", nil, ProtocolHTTP) + _, err := matcher.Match(context.Background(), "GET", nil, http.Header{}, ProtocolHTTP) require.Error(t, err) }) require.NoError(t, matcher.Set(context.Background(), testRules[1:])) t.Run("case=updated", func(t *testing.T) { - testMatcher(t, matcher, "GET", "https://localhost:34/baz", ProtocolHTTP, false, &testRules[1]) - testMatcher(t, matcher, "POST", "https://localhost:1234/foo", ProtocolHTTP, true, nil) - testMatcher(t, matcher, "DELETE", "https://localhost:1234/foo", ProtocolHTTP, true, nil) + testMatcher(t, matcher, "GET", "https://localhost:34/baz", http.Header{}, ProtocolHTTP, false, &testRules[1]) + testMatcher(t, matcher, "POST", "https://localhost:1234/foo", http.Header{}, ProtocolHTTP, true, nil) + testMatcher(t, matcher, "DELETE", "https://localhost:1234/foo", http.Header{}, ProtocolHTTP, true, nil) + testMatcher(t, matcher, "PATCH", "https://localhost:343/bar", http.Header{"Content-Type": {"application/some-app.v1+json"}}, ProtocolHTTP, true, nil) + testMatcher(t, matcher, "PATCH", "https://localhost:343/bar", http.Header{"Content-Type": {"application/some-app.v2+json"}}, ProtocolHTTP, false, &testRules[3]) }) }) t.Run(fmt.Sprintf("glob matcher=%s", name), func(t *testing.T) { require.NoError(t, matcher.SetMatchingStrategy(context.Background(), configuration.Glob)) require.NoError(t, matcher.Set(context.Background(), []Rule{})) t.Run("case=empty", func(t *testing.T) { - testMatcher(t, matcher, "GET", "https://localhost:34/baz", ProtocolHTTP, true, nil) - testMatcher(t, matcher, "POST", "https://localhost:1234/foo", ProtocolHTTP, true, nil) - testMatcher(t, matcher, "DELETE", "https://localhost:1234/foo", ProtocolHTTP, true, nil) + testMatcher(t, matcher, "GET", "https://localhost:34/baz", http.Header{}, ProtocolHTTP, true, nil) + testMatcher(t, matcher, "POST", "https://localhost:1234/foo", http.Header{}, ProtocolHTTP, true, nil) + testMatcher(t, matcher, "DELETE", "https://localhost:1234/foo", http.Header{}, ProtocolHTTP, true, nil) }) require.NoError(t, matcher.Set(context.Background(), testRulesGlob)) t.Run("case=created", func(t *testing.T) { - testMatcher(t, matcher, "GET", "https://localhost:34/baz", ProtocolHTTP, false, &testRulesGlob[1]) - testMatcher(t, matcher, "POST", "https://localhost:1234/foo", ProtocolHTTP, false, &testRulesGlob[0]) - testMatcher(t, matcher, "DELETE", "https://localhost:1234/foo", ProtocolHTTP, true, nil) - testMatcher(t, matcher, "POST", "grpc://bar.example.com/grpc.api/Call", ProtocolGRPC, false, &testRulesGlob[3]) + testMatcher(t, matcher, "GET", "https://localhost:34/baz", http.Header{}, ProtocolHTTP, false, &testRulesGlob[1]) + testMatcher(t, matcher, "POST", "https://localhost:1234/foo", http.Header{}, ProtocolHTTP, false, &testRulesGlob[0]) + testMatcher(t, matcher, "DELETE", "https://localhost:1234/foo", http.Header{}, ProtocolHTTP, true, nil) + testMatcher(t, matcher, "POST", "grpc://bar.example.com/grpc.api/Call", http.Header{}, ProtocolGRPC, false, &testRulesGlob[4]) + testMatcher(t, matcher, "POST", "grpc://bar.example.com/grpc.api/CallWithHeader", http.Header{"Content-Type": []string{"application/some-app.v1+json"}}, ProtocolGRPC, true, nil) + testMatcher(t, matcher, "POST", "grpc://bar.example.com/grpc.api/CallWithHeader", http.Header{"Content-Type": []string{"application/some-app.v2+json"}}, ProtocolGRPC, false, &testRulesGlob[5]) }) t.Run("case=cache", func(t *testing.T) { - r, err := matcher.Match(context.Background(), "GET", mustParseURL(t, "https://localhost:34/baz"), ProtocolHTTP) + r, err := matcher.Match(context.Background(), "GET", mustParseURL(t, "https://localhost:34/baz"), http.Header{}, ProtocolHTTP) require.NoError(t, err) got, err := matcher.Get(context.Background(), r.ID) require.NoError(t, err) @@ -187,9 +219,11 @@ func TestMatcher(t *testing.T) { require.NoError(t, matcher.Set(context.Background(), testRulesGlob[1:])) t.Run("case=updated", func(t *testing.T) { - testMatcher(t, matcher, "GET", "https://localhost:34/baz", ProtocolHTTP, false, &testRulesGlob[1]) - testMatcher(t, matcher, "POST", "https://localhost:1234/foo", ProtocolHTTP, true, nil) - testMatcher(t, matcher, "DELETE", "https://localhost:1234/foo", ProtocolHTTP, true, nil) + testMatcher(t, matcher, "GET", "https://localhost:34/baz", http.Header{}, ProtocolHTTP, false, &testRulesGlob[1]) + testMatcher(t, matcher, "POST", "https://localhost:1234/foo", http.Header{}, ProtocolHTTP, true, nil) + testMatcher(t, matcher, "DELETE", "https://localhost:1234/foo", http.Header{}, ProtocolHTTP, true, nil) + testMatcher(t, matcher, "PATCH", "https://localhost:343/bar", http.Header{"Content-Type": []string{"application/some-app.v1+json"}}, ProtocolHTTP, true, nil) + testMatcher(t, matcher, "PATCH", "https://localhost:343/bar", http.Header{"Content-Type": []string{"application/some-app.v2+json"}}, ProtocolHTTP, false, &testRulesGlob[3]) }) }) } diff --git a/rule/repository_memory.go b/rule/repository_memory.go index 3dc90c1b03..6c343f4c32 100644 --- a/rule/repository_memory.go +++ b/rule/repository_memory.go @@ -110,7 +110,7 @@ func (m *RepositoryMemory) Set(ctx context.Context, rules []Rule) error { return nil } -func (m *RepositoryMemory) Match(ctx context.Context, method string, u *url.URL, protocol Protocol) (*Rule, error) { +func (m *RepositoryMemory) Match(ctx context.Context, method string, u *url.URL, headers http.Header, protocol Protocol) (*Rule, error) { if u == nil { return nil, errors.WithStack(errors.New("nil URL provided")) } @@ -121,7 +121,7 @@ func (m *RepositoryMemory) Match(ctx context.Context, method string, u *url.URL, var rules []*Rule for k := range m.rules { r := &m.rules[k] - if matched, err := r.IsMatching(m.matchingStrategy, method, u, protocol); err != nil { + if matched, err := r.IsMatching(m.matchingStrategy, method, u, headers, protocol); err != nil { return nil, errors.WithStack(err) } else if matched { rules = append(rules, r) @@ -129,7 +129,7 @@ func (m *RepositoryMemory) Match(ctx context.Context, method string, u *url.URL, } for k := range m.invalidRules { r := &m.invalidRules[k] - if matched, err := r.IsMatching(m.matchingStrategy, method, u, protocol); err != nil { + if matched, err := r.IsMatching(m.matchingStrategy, method, u, headers, protocol); err != nil { return nil, errors.WithStack(err) } else if matched { rules = append(rules, r) diff --git a/rule/rule.go b/rule/rule.go index 6a4b851e66..dbe9819435 100644 --- a/rule/rule.go +++ b/rule/rule.go @@ -6,6 +6,7 @@ package rule import ( "encoding/json" "fmt" + "net/http" "net/url" "strings" @@ -19,7 +20,7 @@ type Match struct { // An array of HTTP methods (e.g. GET, POST, PUT, DELETE, ...). When ORY Oathkeeper searches for rules // to decide what to do with an incoming request to the proxy server, it compares the HTTP method of the incoming // request with the HTTP methods of each rules. If a match is found, the rule is considered a partial match. - // If the matchesUrl field is satisfied as well, the rule is considered a full match. + // If the matchesUrl and matchesHeaders fields are satisfied as well, the rule is considered a full match. Methods []string `json:"methods"` // This field represents the URL pattern this rule matches. When ORY Oathkeeper searches for rules @@ -34,22 +35,33 @@ type Match struct { // The following regexp example matches all paths of the domain `mydomain.com`: `https://mydomain.com/<.*>`. // The glob equivalent of the above regexp example is `https://mydomain.com/<*>`. URL string `json:"url"` + + // A map of HTTP headers. When ORY Oathkeeper searches for rules + // to decide what to do with an incoming request to the proxy server, it compares the HTTP headers of the incoming + // request with the HTTP headers of each rules. If a match is found, the rule is considered a partial match. + // For headers with values in array format (e.g. User-Agent headers), the rule header value must match at all + // of the request header values. + // If the matchesUrl and matchesMethods fields are satisfied as well, the rule is considered a full match. + Headers http.Header `json:"headers"` } -func (m *Match) GetURL() string { return m.URL } -func (m *Match) GetMethods() []string { return m.Methods } -func (m *Match) Protocol() Protocol { return ProtocolHTTP } +func (m *Match) GetURL() string { return m.URL } +func (m *Match) GetMethods() []string { return m.Methods } +func (m *Match) Protocol() Protocol { return ProtocolHTTP } +func (m *Match) GetHeaders() http.Header { return m.Headers } type MatchGRPC struct { - Authority string `json:"authority"` - FullMethod string `json:"full_method"` + Authority string `json:"authority"` + FullMethod string `json:"full_method"` + Headers http.Header `json:"headers"` } func (m *MatchGRPC) GetURL() string { return fmt.Sprintf("grpc://%s/%s", m.Authority, m.FullMethod) } -func (m *MatchGRPC) GetMethods() []string { return []string{"POST"} } -func (m *MatchGRPC) Protocol() Protocol { return ProtocolGRPC } +func (m *MatchGRPC) GetMethods() []string { return []string{"POST"} } +func (m *MatchGRPC) Protocol() Protocol { return ProtocolGRPC } +func (m *MatchGRPC) GetHeaders() http.Header { return m.Headers } type Handler struct { // Handler identifies the implementation which will be used to handle this specific request. Please read the user @@ -82,6 +94,7 @@ type URLProvider interface { GetURL() string GetMethods() []string Protocol() Protocol + GetHeaders() http.Header } // Rule is a single rule that will get checked on every HTTP request. @@ -202,22 +215,27 @@ func (r *Rule) GetID() string { return r.ID } -// IsMatching checks whether the provided url and method match the rule. +// IsMatching checks whether the provided url, method and headers match the rule. // An error will be returned if a regexp matching strategy is selected and regexp timeout occurs. -func (r *Rule) IsMatching(strategy configuration.MatchingStrategy, method string, u *url.URL, protocol Protocol) (bool, error) { +func (r *Rule) IsMatching(strategy configuration.MatchingStrategy, method string, u *url.URL, headers http.Header, protocol Protocol) (bool, error) { if r.Match == nil { return false, errors.New("no Match configured (was nil)") } if !stringInSlice(method, r.Match.GetMethods()) { return false, nil } - if err := ensureMatchingEngine(r, strategy); err != nil { - return false, err - } if r.Match.Protocol() != protocol { return false, nil } + if !matchHeaders(headers, r.Match.GetHeaders()) { + return false, nil + } + + if err := ensureMatchingEngine(r, strategy); err != nil { + return false, err + } + matchAgainst := fmt.Sprintf("%s://%s%s", u.Scheme, u.Host, u.Path) return r.matchingEngine.IsMatching(r.Match.GetURL(), matchAgainst) } @@ -257,6 +275,39 @@ func ensureMatchingEngine(rule *Rule, strategy configuration.MatchingStrategy) e return errors.Wrap(ErrUnknownMatchingStrategy, string(strategy)) } +func matchHeaders(requestHeaders http.Header, matchHeaders http.Header) bool { + for matcherHeaderKey, matcherHeaderValues := range matchHeaders { + foundMatch := false + for requestHeaderKey, requestHeaderValues := range requestHeaders { + if strings.EqualFold(matcherHeaderKey, requestHeaderKey) { + if slicesEqualFold(requestHeaderValues, matcherHeaderValues) { + foundMatch = true + // Break if we find the matching values. Report match found + break + } + // Break if we find the matching key but value do not match + break + } + } + if !foundMatch { + return false + } + } + return true +} + +func slicesEqualFold(a, b []string) bool { + if len(a) != len(b) { + return false + } + for i, v := range a { + if !strings.EqualFold(v, b[i]) { + return false + } + } + return true +} + // ExtractRegexGroups returns the values matching the rule pattern func (r *Rule) ExtractRegexGroups(strategy configuration.MatchingStrategy, u *url.URL) ([]string, error) { if err := ensureMatchingEngine(r, strategy); err != nil { diff --git a/rule/rule_test.go b/rule/rule_test.go index 568fb6f83d..dee7069b57 100644 --- a/rule/rule_test.go +++ b/rule/rule_test.go @@ -5,6 +5,7 @@ package rule import ( "encoding/json" + "net/http" "net/url" "strconv" "testing" @@ -71,7 +72,7 @@ func TestRule(t *testing.T) { for ind, tcase := range tests { t.Run(strconv.FormatInt(int64(ind), 10), func(t *testing.T) { testFunc := func(rule Rule, strategy configuration.MatchingStrategy) { - matched, err := rule.IsMatching(strategy, tcase.method, mustParse(t, tcase.url), ProtocolHTTP) + matched, err := rule.IsMatching(strategy, tcase.method, mustParse(t, tcase.url), http.Header{}, ProtocolHTTP) assert.Equal(t, tcase.expectedMatch, matched) assert.Equal(t, tcase.expectedErr, err) } @@ -123,7 +124,7 @@ func TestRule1(t *testing.T) { } for ind, tcase := range tests { t.Run(strconv.FormatInt(int64(ind), 10), func(t *testing.T) { - matched, err := r.IsMatching(configuration.Regexp, tcase.method, mustParse(t, tcase.url), ProtocolHTTP) + matched, err := r.IsMatching(configuration.Regexp, tcase.method, mustParse(t, tcase.url), http.Header{}, ProtocolHTTP) assert.Equal(t, tcase.expectedMatch, matched) assert.Equal(t, tcase.expectedErr, err) }) @@ -165,7 +166,103 @@ func TestRuleWithCustomMethod(t *testing.T) { } for ind, tcase := range tests { t.Run(strconv.FormatInt(int64(ind), 10), func(t *testing.T) { - matched, err := r.IsMatching(configuration.Regexp, tcase.method, mustParse(t, tcase.url), ProtocolHTTP) + matched, err := r.IsMatching(configuration.Regexp, tcase.method, mustParse(t, tcase.url), http.Header{}, ProtocolHTTP) + assert.Equal(t, tcase.expectedMatch, matched) + assert.Equal(t, tcase.expectedErr, err) + }) + } +} + +func TestRuleWithHeaders(t *testing.T) { + r := &Rule{ + Match: &Match{ + Methods: []string{"DELETE"}, + URL: "https://localhost/users/<(?!admin).*>", + Headers: http.Header{ + "Content-Type": {"application+v2.json"}, + "x-custom-header": {"foo"}, + }, + }, + } + + var tests = []struct { + method string + url string + headers http.Header + expectedMatch bool + expectedErr error + }{ + { + method: "DELETE", + url: "https://localhost/users/foo", + headers: http.Header{}, + expectedMatch: false, + expectedErr: nil, + }, + { + method: "DELETE", + url: "https://localhost/users/foo", + headers: http.Header{ + "Content-Type": {"application+v2.json"}, + }, + expectedMatch: false, + expectedErr: nil, + }, + { + method: "DELETE", + url: "https://localhost/users/foo", + headers: http.Header{ + "Content-Type": {"application+v2.json"}, + }, + expectedMatch: false, + expectedErr: nil, + }, + { + method: "DELETE", + url: "https://localhost/users/foo", + headers: http.Header{ + "Content-Type": {"application+v2.json"}, + "x-custom-header": {"bar"}, + }, + expectedMatch: false, + expectedErr: nil, + }, + { + method: "DELETE", + url: "https://localhost/users/foo", + headers: http.Header{ + "Content-Type": {"application+v1.json"}, + "x-custom-header": {"foo"}, + }, + expectedMatch: false, + expectedErr: nil, + }, + { + method: "DELETE", + url: "https://localhost/users/foo", + headers: http.Header{ + "Content-Type": {"application+v2.json"}, + "x-custom-header": {"foo"}, + "x-irrelevant-header": {"something", "not", "important"}, + }, + expectedMatch: true, + expectedErr: nil, + }, + { + method: "DELETE", + url: "https://localhost/users/foo", + headers: http.Header{ + "Content-Type": {"application+v2.json", "application+v1.json"}, + "x-custom-header": {"foo", "bar"}, + "x-irrelevant-header": {"something", "not", "important"}, + }, + expectedMatch: false, + expectedErr: nil, + }, + } + for ind, tcase := range tests { + t.Run(strconv.FormatInt(int64(ind), 10), func(t *testing.T) { + matched, err := r.IsMatching(configuration.Regexp, tcase.method, mustParse(t, tcase.url), tcase.headers, ProtocolHTTP) assert.Equal(t, tcase.expectedMatch, matched) assert.Equal(t, tcase.expectedErr, err) })