diff --git a/.gitignore b/.gitignore
index 73b753d1fc..b281e6b866 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,4 +3,5 @@ vendor
_book
node_modules/
LICENSE.txt
-*-packr.go
\ No newline at end of file
+*-packr.go
+dev
\ No newline at end of file
diff --git a/.schemas/config.schema.json b/.schemas/config.schema.json
index da022c0061..3e16905e4b 100644
--- a/.schemas/config.schema.json
+++ b/.schemas/config.schema.json
@@ -982,6 +982,16 @@
"examples": [
"[\"file://path/to/rules.json\",\"inline://W3siaWQiOiJmb28tcnVsZSIsImF1dGhlbnRpY2F0b3JzIjpbXX1d\",\"https://path-to-my-rules/rules.json\"]"
]
+ },
+ "matching_strategy": {
+ "title": "Matching strategy",
+ "description": "This an optional field describing matching strategy. Currently supported values are 'glob' and 'regexp'.",
+ "type": "string",
+ "default": "regexp",
+ "enum": ["glob", "regexp"],
+ "examples": [
+ "glob", "regexp"
+ ]
}
}
},
diff --git a/README.md b/README.md
index 6fa6932ba1..3e8a233bd2 100644
--- a/README.md
+++ b/README.md
@@ -4,8 +4,8 @@
Chat |
Forums |
Newsletter
- Guide |
API Docs |
+ Guide |
Code Docs
Support this project!
diff --git a/api/decision_test.go b/api/decision_test.go
index 334909491e..6078c6f71e 100644
--- a/api/decision_test.go
+++ b/api/decision_test.go
@@ -21,6 +21,7 @@
package api_test
import (
+ "context"
"fmt"
"net/http"
"net/http/httptest"
@@ -74,42 +75,62 @@ func TestDecisionAPI(t *testing.T) {
Upstream: rule.Upstream{URL: "", StripPath: "/strip-path/", PreserveHost: true},
}
+ ruleNoOpAuthenticatorGLOB := rule.Rule{
+ Match: &rule.Match{Methods: []string{"GET"}, URL: ts.URL + "/authn-noop/<[0-9]*>"},
+ Authenticators: []rule.Handler{{Handler: "noop"}},
+ Authorizer: rule.Handler{Handler: "allow"},
+ Mutators: []rule.Handler{{Handler: "noop"}},
+ Upstream: rule.Upstream{URL: ""},
+ }
+ ruleNoOpAuthenticatorModifyUpstreamGLOB := rule.Rule{
+ Match: &rule.Match{Methods: []string{"GET"}, URL: ts.URL + "/strip-path/authn-noop/<[0-9]*>"},
+ Authenticators: []rule.Handler{{Handler: "noop"}},
+ Authorizer: rule.Handler{Handler: "allow"},
+ Mutators: []rule.Handler{{Handler: "noop"}},
+ Upstream: rule.Upstream{URL: "", StripPath: "/strip-path/", PreserveHost: true},
+ }
+
for k, tc := range []struct {
- url string
- code int
- messages []string
- rules []rule.Rule
- transform func(r *http.Request)
- authz string
- d string
+ url string
+ code int
+ messages []string
+ rulesRegexp []rule.Rule
+ rulesGlob []rule.Rule
+ transform func(r *http.Request)
+ authz string
+ d string
}{
{
- d: "should fail because url does not exist in rule set",
- url: ts.URL + "/decisions" + "/invalid",
- rules: []rule.Rule{},
- code: http.StatusNotFound,
+ d: "should fail because url does not exist in rule set",
+ url: ts.URL + "/decisions" + "/invalid",
+ rulesRegexp: []rule.Rule{},
+ rulesGlob: []rule.Rule{},
+ code: http.StatusNotFound,
},
{
- d: "should fail because url does exist but is matched by two rules",
- url: ts.URL + "/decisions" + "/authn-noop/1234",
- rules: []rule.Rule{ruleNoOpAuthenticator, ruleNoOpAuthenticator},
- code: http.StatusInternalServerError,
+ d: "should fail because url does exist but is matched by two rulesRegexp",
+ url: ts.URL + "/decisions" + "/authn-noop/1234",
+ rulesRegexp: []rule.Rule{ruleNoOpAuthenticator, ruleNoOpAuthenticator},
+ rulesGlob: []rule.Rule{ruleNoOpAuthenticatorGLOB, ruleNoOpAuthenticatorGLOB},
+ code: http.StatusInternalServerError,
},
{
- d: "should pass",
- url: ts.URL + "/decisions" + "/authn-noop/1234",
- rules: []rule.Rule{ruleNoOpAuthenticator},
- code: http.StatusOK,
+ d: "should pass",
+ url: ts.URL + "/decisions" + "/authn-noop/1234",
+ rulesRegexp: []rule.Rule{ruleNoOpAuthenticator},
+ rulesGlob: []rule.Rule{ruleNoOpAuthenticatorGLOB},
+ code: http.StatusOK,
transform: func(r *http.Request) {
r.Header.Add("Authorization", "bearer token")
},
authz: "bearer token",
},
{
- d: "should pass",
- url: ts.URL + "/decisions" + "/strip-path/authn-noop/1234",
- rules: []rule.Rule{ruleNoOpAuthenticatorModifyUpstream},
- code: http.StatusOK,
+ d: "should pass",
+ url: ts.URL + "/decisions" + "/strip-path/authn-noop/1234",
+ rulesRegexp: []rule.Rule{ruleNoOpAuthenticatorModifyUpstream},
+ rulesGlob: []rule.Rule{ruleNoOpAuthenticatorModifyUpstreamGLOB},
+ code: http.StatusOK,
transform: func(r *http.Request) {
r.Header.Add("Authorization", "bearer token")
},
@@ -118,11 +139,16 @@ func TestDecisionAPI(t *testing.T) {
{
d: "should fail because no authorizer was configured",
url: ts.URL + "/decisions" + "/authn-anon/authz-none/cred-none/1234",
- rules: []rule.Rule{{
+ rulesRegexp: []rule.Rule{{
Match: &rule.Match{Methods: []string{"GET"}, URL: ts.URL + "/authn-anon/authz-none/cred-none/<[0-9]+>"},
Authenticators: []rule.Handler{{Handler: "anonymous"}},
Upstream: rule.Upstream{URL: ""},
}},
+ rulesGlob: []rule.Rule{{
+ Match: &rule.Match{Methods: []string{"GET"}, URL: ts.URL + "/authn-anon/authz-none/cred-none/<[0-9]*>"},
+ Authenticators: []rule.Handler{{Handler: "anonymous"}},
+ Upstream: rule.Upstream{URL: ""},
+ }},
transform: func(r *http.Request) {
r.Header.Add("Authorization", "bearer token")
},
@@ -131,77 +157,116 @@ func TestDecisionAPI(t *testing.T) {
{
d: "should fail because no mutator was configured",
url: ts.URL + "/decisions" + "/authn-anon/authz-allow/cred-none/1234",
- rules: []rule.Rule{{
+ rulesRegexp: []rule.Rule{{
Match: &rule.Match{Methods: []string{"GET"}, URL: ts.URL + "/authn-anon/authz-allow/cred-none/<[0-9]+>"},
Authenticators: []rule.Handler{{Handler: "anonymous"}},
Authorizer: rule.Handler{Handler: "allow"},
Upstream: rule.Upstream{URL: ""},
}},
+ rulesGlob: []rule.Rule{{
+ Match: &rule.Match{Methods: []string{"GET"}, URL: ts.URL + "/authn-anon/authz-allow/cred-none/<[0-9]*>"},
+ Authenticators: []rule.Handler{{Handler: "anonymous"}},
+ Authorizer: rule.Handler{Handler: "allow"},
+ Upstream: rule.Upstream{URL: ""},
+ }},
code: http.StatusInternalServerError,
},
{
d: "should pass with anonymous and everything else set to noop",
url: ts.URL + "/decisions" + "/authn-anon/authz-allow/cred-noop/1234",
- rules: []rule.Rule{{
+ rulesRegexp: []rule.Rule{{
Match: &rule.Match{Methods: []string{"GET"}, URL: ts.URL + "/authn-anon/authz-allow/cred-noop/<[0-9]+>"},
Authenticators: []rule.Handler{{Handler: "anonymous"}},
Authorizer: rule.Handler{Handler: "allow"},
Mutators: []rule.Handler{{Handler: "noop"}},
Upstream: rule.Upstream{URL: ""},
}},
+ rulesGlob: []rule.Rule{{
+ Match: &rule.Match{Methods: []string{"GET"}, URL: ts.URL + "/authn-anon/authz-allow/cred-noop/<[0-9]*>"},
+ Authenticators: []rule.Handler{{Handler: "anonymous"}},
+ Authorizer: rule.Handler{Handler: "allow"},
+ Mutators: []rule.Handler{{Handler: "noop"}},
+ Upstream: rule.Upstream{URL: ""},
+ }},
code: http.StatusOK,
authz: "",
},
{
d: "should fail when authorizer fails",
url: ts.URL + "/decisions" + "/authn-anon/authz-deny/cred-noop/1234",
- rules: []rule.Rule{{
+ rulesRegexp: []rule.Rule{{
Match: &rule.Match{Methods: []string{"GET"}, URL: ts.URL + "/authn-anon/authz-deny/cred-noop/<[0-9]+>"},
Authenticators: []rule.Handler{{Handler: "anonymous"}},
Authorizer: rule.Handler{Handler: "deny"},
Mutators: []rule.Handler{{Handler: "noop"}},
Upstream: rule.Upstream{URL: ""},
}},
+ rulesGlob: []rule.Rule{{
+ Match: &rule.Match{Methods: []string{"GET"}, URL: ts.URL + "/authn-anon/authz-deny/cred-noop/<[0-9]*>"},
+ Authenticators: []rule.Handler{{Handler: "anonymous"}},
+ Authorizer: rule.Handler{Handler: "deny"},
+ Mutators: []rule.Handler{{Handler: "noop"}},
+ Upstream: rule.Upstream{URL: ""},
+ }},
code: http.StatusForbidden,
},
{
d: "should fail when authenticator fails",
url: ts.URL + "/decisions" + "/authn-broken/authz-none/cred-none/1234",
- rules: []rule.Rule{{
+ rulesRegexp: []rule.Rule{{
Match: &rule.Match{Methods: []string{"GET"}, URL: ts.URL + "/authn-broken/authz-none/cred-none/<[0-9]+>"},
Authenticators: []rule.Handler{{Handler: "unauthorized"}},
Upstream: rule.Upstream{URL: ""},
}},
+ rulesGlob: []rule.Rule{{
+ Match: &rule.Match{Methods: []string{"GET"}, URL: ts.URL + "/authn-broken/authz-none/cred-none/<[0-9]*>"},
+ Authenticators: []rule.Handler{{Handler: "unauthorized"}},
+ Upstream: rule.Upstream{URL: ""},
+ }},
code: http.StatusUnauthorized,
},
{
d: "should fail when mutator fails",
url: ts.URL + "/decisions" + "/authn-anonymous/authz-allow/cred-broken/1234",
- rules: []rule.Rule{{
+ rulesRegexp: []rule.Rule{{
Match: &rule.Match{Methods: []string{"GET"}, URL: ts.URL + "/authn-anonymous/authz-allow/cred-broken/<[0-9]+>"},
Authenticators: []rule.Handler{{Handler: "anonymous"}},
Authorizer: rule.Handler{Handler: "allow"},
Mutators: []rule.Handler{{Handler: "broken"}},
Upstream: rule.Upstream{URL: ""},
}},
+ rulesGlob: []rule.Rule{{
+ Match: &rule.Match{Methods: []string{"GET"}, URL: ts.URL + "/authn-anonymous/authz-allow/cred-broken/<[0-9]*>"},
+ Authenticators: []rule.Handler{{Handler: "anonymous"}},
+ Authorizer: rule.Handler{Handler: "allow"},
+ Mutators: []rule.Handler{{Handler: "broken"}},
+ Upstream: rule.Upstream{URL: ""},
+ }},
code: http.StatusInternalServerError,
},
{
d: "should fail when one of the mutators fails",
url: ts.URL + "/decisions" + "/authn-anonymous/authz-allow/cred-broken/1234",
- rules: []rule.Rule{{
+ rulesRegexp: []rule.Rule{{
Match: &rule.Match{Methods: []string{"GET"}, URL: ts.URL + "/authn-anonymous/authz-allow/cred-broken/<[0-9]+>"},
Authenticators: []rule.Handler{{Handler: "anonymous"}},
Authorizer: rule.Handler{Handler: "allow"},
Mutators: []rule.Handler{{Handler: "noop"}, {Handler: "broken"}},
Upstream: rule.Upstream{URL: ""},
}},
+ rulesGlob: []rule.Rule{{
+ Match: &rule.Match{Methods: []string{"GET"}, URL: ts.URL + "/authn-anonymous/authz-allow/cred-broken/<[0-9]*>"},
+ Authenticators: []rule.Handler{{Handler: "anonymous"}},
+ Authorizer: rule.Handler{Handler: "allow"},
+ Mutators: []rule.Handler{{Handler: "noop"}, {Handler: "broken"}},
+ Upstream: rule.Upstream{URL: ""},
+ }},
code: http.StatusInternalServerError,
},
{
d: "should fail when authorizer fails and send www_authenticate as defined in the rule",
url: ts.URL + "/decisions" + "/authn-anon/authz-deny/cred-noop/1234",
- rules: []rule.Rule{{
+ rulesRegexp: []rule.Rule{{
Match: &rule.Match{Methods: []string{"GET"}, URL: ts.URL + "/authn-anon/authz-deny/cred-noop/<[0-9]+>"},
Authenticators: []rule.Handler{{Handler: "anonymous"}},
Authorizer: rule.Handler{Handler: "deny"},
@@ -209,24 +274,41 @@ func TestDecisionAPI(t *testing.T) {
Upstream: rule.Upstream{URL: ""},
Errors: []rule.ErrorHandler{{Handler: "www_authenticate"}},
}},
+ rulesGlob: []rule.Rule{{
+ Match: &rule.Match{Methods: []string{"GET"}, URL: ts.URL + "/authn-anon/authz-deny/cred-noop/<[0-9]*>"},
+ Authenticators: []rule.Handler{{Handler: "anonymous"}},
+ Authorizer: rule.Handler{Handler: "deny"},
+ Mutators: []rule.Handler{{Handler: "noop"}},
+ Upstream: rule.Upstream{URL: ""},
+ Errors: []rule.ErrorHandler{{Handler: "www_authenticate"}},
+ }},
code: http.StatusUnauthorized,
},
} {
t.Run(fmt.Sprintf("case=%d/description=%s", k, tc.d), func(t *testing.T) {
- reg.RuleRepository().(*rule.RepositoryMemory).WithRules(tc.rules)
+ testFunc := func(strategy configuration.MatchingStrategy) {
+ require.NoError(t, reg.RuleRepository().SetMatchingStrategy(context.Background(), strategy))
+ req, err := http.NewRequest("GET", tc.url, nil)
+ require.NoError(t, err)
+ if tc.transform != nil {
+ tc.transform(req)
+ }
- req, err := http.NewRequest("GET", tc.url, nil)
- require.NoError(t, err)
- if tc.transform != nil {
- tc.transform(req)
- }
-
- res, err := http.DefaultClient.Do(req)
- require.NoError(t, err)
- defer res.Body.Close()
+ res, err := http.DefaultClient.Do(req)
+ require.NoError(t, err)
+ defer res.Body.Close()
- assert.Equal(t, tc.authz, res.Header.Get("Authorization"))
- assert.Equal(t, tc.code, res.StatusCode)
+ assert.Equal(t, tc.authz, res.Header.Get("Authorization"))
+ assert.Equal(t, tc.code, res.StatusCode)
+ }
+ t.Run("regexp", func(t *testing.T) {
+ reg.RuleRepository().(*rule.RepositoryMemory).WithRules(tc.rulesRegexp)
+ testFunc(configuration.Regexp)
+ })
+ t.Run("glob", func(t *testing.T) {
+ reg.RuleRepository().(*rule.RepositoryMemory).WithRules(tc.rulesRegexp)
+ testFunc(configuration.Glob)
+ })
})
}
}
diff --git a/api/rule_test.go b/api/rule_test.go
index abb4ca0571..4e7a2559b3 100644
--- a/api/rule_test.go
+++ b/api/rule_test.go
@@ -22,18 +22,21 @@ package api_test
import (
"bytes"
+ "context"
"encoding/json"
"net/http/httptest"
"net/url"
"testing"
+ "github.com/ory/oathkeeper/driver/configuration"
"github.com/ory/oathkeeper/x"
+ "github.com/ory/x/pointerx"
+
"github.com/ory/oathkeeper/internal"
"github.com/ory/oathkeeper/internal/httpclient/client"
sdkrule "github.com/ory/oathkeeper/internal/httpclient/client/api"
"github.com/ory/oathkeeper/rule"
- "github.com/ory/x/pointerx"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -56,7 +59,7 @@ func TestHandler(t *testing.T) {
Schemes: []string{u.Scheme},
})
- rules := []rule.Rule{
+ rulesRegexp := []rule.Rule{
{
ID: "foo1",
Match: &rule.Match{
@@ -90,23 +93,66 @@ func TestHandler(t *testing.T) {
},
},
}
-
- reg.RuleRepository().(*rule.RepositoryMemory).WithRules(rules)
+ rulesGlob := []rule.Rule{
+ {
+ ID: "foo1",
+ Match: &rule.Match{
+ URL: "https://localhost:1234/<{foo*,bar*}>>",
+ Methods: []string{"POST"},
+ },
+ Description: "Create users rule",
+ Authorizer: rule.Handler{Handler: "allow", Config: json.RawMessage(`{"type":"any"}`)},
+ Authenticators: []rule.Handler{{Handler: "anonymous", Config: json.RawMessage(`{"name":"anonymous1"}`)}},
+ Mutators: []rule.Handler{{Handler: "id_token", Config: json.RawMessage(`{"issuer":"anything"}`)}},
+ Upstream: rule.Upstream{
+ URL: "http://localhost:1235/",
+ StripPath: "/bar",
+ PreserveHost: true,
+ },
+ },
+ {
+ ID: "foo2",
+ Match: &rule.Match{
+ URL: "https://localhost:34/<{baz*,bar*}>",
+ Methods: []string{"GET"},
+ },
+ Description: "Get users rule",
+ Authorizer: rule.Handler{Handler: "deny", Config: json.RawMessage(`{"type":"any"}`)},
+ Authenticators: []rule.Handler{{Handler: "oauth2_introspection", Config: json.RawMessage(`{"name":"anonymous1"}`)}},
+ Mutators: []rule.Handler{{Handler: "id_token", Config: json.RawMessage(`{"issuer":"anything"}`)}, {Handler: "headers", Config: json.RawMessage(`{"headers":{"X-User":"user"}}`)}},
+ Upstream: rule.Upstream{
+ URL: "http://localhost:333/",
+ StripPath: "/foo",
+ PreserveHost: false,
+ },
+ },
+ }
t.Run("case=create a new rule", func(t *testing.T) {
- results, err := cl.API.ListRules(sdkrule.NewListRulesParams().WithLimit(pointerx.Int64(10)))
- require.NoError(t, err)
- require.Len(t, results.Payload, 2)
- assert.True(t, results.Payload[0].ID != results.Payload[1].ID)
+ testFunc := func(strategy configuration.MatchingStrategy, rules []rule.Rule) {
+ reg.RuleRepository().(*rule.RepositoryMemory).WithRules(rules)
+ require.NoError(t, reg.RuleRepository().SetMatchingStrategy(context.Background(), strategy))
+ results, err := cl.API.ListRules(sdkrule.NewListRulesParams().WithLimit(pointerx.Int64(10)))
+ require.NoError(t, err)
+ require.Len(t, results.Payload, 2)
+ assert.True(t, results.Payload[0].ID != results.Payload[1].ID)
+
+ result, err := cl.API.GetRule(sdkrule.NewGetRuleParams().WithID(rules[1].ID))
+ require.NoError(t, err)
- result, err := cl.API.GetRule(sdkrule.NewGetRuleParams().WithID(rules[1].ID))
- require.NoError(t, err)
+ var b bytes.Buffer
+ var ruleResult rule.Rule
+ require.NoError(t, json.NewEncoder(&b).Encode(result.Payload))
+ require.NoError(t, json.NewDecoder(&b).Decode(&ruleResult))
- var b bytes.Buffer
- var ruleResult rule.Rule
- require.NoError(t, json.NewEncoder(&b).Encode(result.Payload))
- require.NoError(t, json.NewDecoder(&b).Decode(&ruleResult))
+ assert.EqualValues(t, rules[1], ruleResult)
+ }
+ t.Run("regexp", func(t *testing.T) {
+ testFunc(configuration.Regexp, rulesRegexp)
+ })
+ t.Run("glob", func(t *testing.T) {
+ testFunc(configuration.Glob, rulesGlob)
+ })
- assert.EqualValues(t, rules[1], ruleResult)
})
}
diff --git a/docs/.oathkeeper.yaml b/docs/.oathkeeper.yaml
index f6518a8e1c..9246af46ff 100644
--- a/docs/.oathkeeper.yaml
+++ b/docs/.oathkeeper.yaml
@@ -87,6 +87,8 @@ access_rules:
# If the URL Scheme is `http://` or `https://`, the access rules (an array of access rules is expected) will be
# fetched from the provided HTTP(s) location.
- https://path-to-my-rules/rules.json
+ # Optional fields describing matching strategy, defaults to "regexp".
+ matching_strategy: glob
errors:
fallback:
diff --git a/driver/configuration/provider.go b/driver/configuration/provider.go
index 4bf1870517..9400bc6411 100644
--- a/driver/configuration/provider.go
+++ b/driver/configuration/provider.go
@@ -19,6 +19,16 @@ const (
ForbiddenStrategyErrorType = "forbidden"
)
+// MatchingStrategy defines matching strategy such as Regexp or Glob.
+// Empty string defaults to "regexp".
+type MatchingStrategy string
+
+// Possible matching strategies.
+const (
+ Regexp MatchingStrategy = "regexp"
+ Glob MatchingStrategy = "glob"
+)
+
type Provider interface {
CORSEnabled(iface string) bool
CORSOptions(iface string) cors.Options
@@ -33,6 +43,7 @@ type Provider interface {
ProxyIdleTimeout() time.Duration
AccessRuleRepositories() []url.URL
+ AccessRuleMatchingStrategy() MatchingStrategy
ProxyServeAddress() string
APIServeAddress() string
diff --git a/driver/configuration/provider_viper.go b/driver/configuration/provider_viper.go
index c6dd0bad81..42b62d063f 100644
--- a/driver/configuration/provider_viper.go
+++ b/driver/configuration/provider_viper.go
@@ -37,14 +37,15 @@ func init() {
}
const (
- ViperKeyProxyReadTimeout = "serve.proxy.timeout.read"
- ViperKeyProxyWriteTimeout = "serve.proxy.timeout.write"
- ViperKeyProxyIdleTimeout = "serve.proxy.timeout.idle"
- ViperKeyProxyServeAddressHost = "serve.proxy.host"
- ViperKeyProxyServeAddressPort = "serve.proxy.port"
- ViperKeyAPIServeAddressHost = "serve.api.host"
- ViperKeyAPIServeAddressPort = "serve.api.port"
- ViperKeyAccessRuleRepositories = "access_rules.repositories"
+ ViperKeyProxyReadTimeout = "serve.proxy.timeout.read"
+ ViperKeyProxyWriteTimeout = "serve.proxy.timeout.write"
+ ViperKeyProxyIdleTimeout = "serve.proxy.timeout.idle"
+ ViperKeyProxyServeAddressHost = "serve.proxy.host"
+ ViperKeyProxyServeAddressPort = "serve.proxy.port"
+ ViperKeyAPIServeAddressHost = "serve.api.host"
+ ViperKeyAPIServeAddressPort = "serve.api.port"
+ ViperKeyAccessRuleRepositories = "access_rules.repositories"
+ ViperKeyAccessRuleMatchingStrategy = "access_rules.matching_strategy"
)
// Authorizers
@@ -131,6 +132,11 @@ func (v *ViperProvider) AccessRuleRepositories() []url.URL {
return repositories
}
+// AccessRuleMatchingStrategy returns current MatchingStrategy.
+func (v *ViperProvider) AccessRuleMatchingStrategy() MatchingStrategy {
+ return MatchingStrategy(viperx.GetString(v.l, ViperKeyAccessRuleMatchingStrategy, ""))
+}
+
func (v *ViperProvider) CORSEnabled(iface string) bool {
return corsx.IsEnabled(v.l, "serve."+iface)
}
diff --git a/go.mod b/go.mod
index 3d0f178126..9716e0c660 100644
--- a/go.mod
+++ b/go.mod
@@ -8,6 +8,7 @@ require (
github.com/blang/semver v3.5.1+incompatible
github.com/bxcodec/faker v2.0.1+incompatible
github.com/dgrijalva/jwt-go v3.2.0+incompatible
+ github.com/dlclark/regexp2 v1.2.0
github.com/fsnotify/fsnotify v1.4.7
github.com/ghodss/yaml v1.0.0
github.com/go-openapi/errors v0.19.2
@@ -19,6 +20,7 @@ require (
github.com/go-swagger/go-swagger v0.21.1-0.20200107003254-1c98855b472d
github.com/gobuffalo/httptest v1.0.2
github.com/gobuffalo/packr/v2 v2.0.0-rc.15
+ github.com/gobwas/glob v0.2.3
github.com/golang/gddo v0.0.0-20190904175337-72a348e765d2
github.com/golang/mock v1.3.1
github.com/google/uuid v1.1.1
@@ -34,7 +36,7 @@ require (
github.com/ory/gojsonschema v1.2.0
github.com/ory/graceful v0.1.1
github.com/ory/herodot v0.6.2
- github.com/ory/ladon v1.0.1
+ github.com/ory/ladon v1.1.0
github.com/ory/sdk/swagutil v0.0.0-20200202121523-307941feee4b
github.com/ory/viper v1.5.7
github.com/ory/x v0.0.93
diff --git a/go.sum b/go.sum
index ce7633dd33..89f36af130 100644
--- a/go.sum
+++ b/go.sum
@@ -84,6 +84,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
+github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk=
+github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
@@ -349,6 +351,8 @@ github.com/gobuffalo/uuid v2.0.5+incompatible/go.mod h1:ErhIzkRhm0FtRuiE/PeORqcw
github.com/gobuffalo/validate v2.0.3+incompatible/go.mod h1:N+EtDe0J8252BgfzQUChBgfd6L93m9weay53EWFVsMM=
github.com/gobuffalo/x v0.0.0-20181003152136-452098b06085/go.mod h1:WevpGD+5YOreDJznWevcn8NTmQEW5STSBgIkpkjzqXc=
github.com/gobuffalo/x v0.0.0-20181007152206-913e47c59ca7/go.mod h1:9rDPXaB3kXdKWzMc4odGQQdG2e2DIEmANy5aSJ9yesY=
+github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
+github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/gofrs/uuid v3.1.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
@@ -552,8 +556,9 @@ github.com/ory/graceful v0.1.1 h1:zx+8tDObLPrG+7Tc8jKYlXsqWnLtOQA1IZ/FAAKHMXU=
github.com/ory/graceful v0.1.1/go.mod h1:zqu70l95WrKHF4AZ6tXHvAqAvpY6M7g6ttaAVcMm7KU=
github.com/ory/herodot v0.6.2 h1:zOb5MsuMn7AH9/Ewc/EK83yqcNViK1m1l3C2UuP3RcA=
github.com/ory/herodot v0.6.2/go.mod h1:3BOneqcyBsVybCPAJoi92KN2BpJHcmDqAMcAAaJiJow=
-github.com/ory/ladon v1.0.1 h1:zCEfqnv8Ro62id0Z9cVPRPv4ejTu6HHtuPbXmTWBxks=
github.com/ory/ladon v1.0.1/go.mod h1:1VhCA2mBtaMhRUS6VS0d9qrNVDQnFXqSRb5D0NvQUPY=
+github.com/ory/ladon v1.1.0 h1:6tgazU2J3Z3odPs1f0qn729kRXCAtlJROliuWUHedV0=
+github.com/ory/ladon v1.1.0/go.mod h1:25bNc/Glx/8xCH7MbItDxjvviAmFQ+aYxb1V1SE5wlg=
github.com/ory/pagination v0.0.1/go.mod h1:d1ToRROAUleriPhmb2dYbhANhhLwZ8s395m2yJCDFh8=
github.com/ory/sdk/swagutil v0.0.0-20200123152503-0d50960e70bd h1:QrEYSnaOX6kpyBcQGUlExcI4RwCq2S/Wta/zbgT74Kk=
github.com/ory/sdk/swagutil v0.0.0-20200123152503-0d50960e70bd/go.mod h1:Ufg1eAyz+Zt3+oweSZVThG13ewewWCKwBmoNmK8Z0co=
@@ -564,6 +569,7 @@ github.com/ory/sdk/swagutil v0.0.0-20200202121523-307941feee4b/go.mod h1:Ufg1eAy
github.com/ory/viper v1.5.6/go.mod h1:TYmpFpKLxjQwvT4f0QPpkOn4sDXU1kDgAwJpgLYiQ28=
github.com/ory/viper v1.5.7 h1:VeXfcBgTG3SCMlw4hmXkazXPZbyvBTdUjS7Dxm3gOjQ=
github.com/ory/viper v1.5.7/go.mod h1:+Mfm2gCDqtYRn5gVaLsBtXACO59zITETZQ/jQwc9SZo=
+github.com/ory/x v0.0.87/go.mod h1:wrnJRjIfYXFY/AUiuUlcIUpLBDxFtWc+8x6toAeLZXU=
github.com/ory/x v0.0.88/go.mod h1:wrnJRjIfYXFY/AUiuUlcIUpLBDxFtWc+8x6toAeLZXU=
github.com/ory/x v0.0.91 h1:4sySRGI1dExt3FpvXcnenpagoM6oQeEvboQ53/tcY9g=
github.com/ory/x v0.0.91/go.mod h1:lfcTaGXpTZs7IEQAW00r9EtTCOxD//SiP5uWtNiz31g=
@@ -879,8 +885,10 @@ golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgw
golang.org/x/tools v0.0.0-20190617190820-da514acc4774/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190711191110-9a621aea19f8/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
+golang.org/x/tools v0.0.0-20191026034945-b2104f82a97d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191224055732-dd894d0a8a40 h1:UyP2XDSgSc8ldYCxAK735zQxeH3Gd81sK7Iy7AoaVxk=
golang.org/x/tools v0.0.0-20191224055732-dd894d0a8a40/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
@@ -921,6 +929,7 @@ gopkg.in/mail.v2 v2.0.0-20180731213649-a0242b2233b4/go.mod h1:htwXN1Qh09vZJ1NVKx
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/square/go-jose.v2 v2.1.9/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
+gopkg.in/square/go-jose.v2 v2.3.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/square/go-jose.v2 v2.3.1 h1:SK5KegNXmKmqE342YYN2qPHEnUYeoMiXXl1poUlI+o4=
gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
diff --git a/helper/errors.go b/helper/errors.go
index 3ba6b42d58..34bffbf820 100644
--- a/helper/errors.go
+++ b/helper/errors.go
@@ -47,6 +47,12 @@ var (
CodeField: http.StatusInternalServerError,
StatusField: http.StatusText(http.StatusInternalServerError),
}
+ // TODO: discuss the text and status code
+ ErrNonRegexpMatchingStrategy = &herodot.DefaultError{
+ ErrorField: "The matched handler uses Regexp MatchingStrategy which is not selected in the configuration",
+ CodeField: http.StatusInternalServerError,
+ StatusField: http.StatusText(http.StatusInternalServerError),
+ }
ErrMatchesNoRule = &herodot.DefaultError{
ErrorField: "Requested url does not match any rules",
CodeField: http.StatusNotFound,
diff --git a/pipeline/authz/keto_engine_acp_ory.go b/pipeline/authz/keto_engine_acp_ory.go
index 26ef6608ec..b81945511a 100644
--- a/pipeline/authz/keto_engine_acp_ory.go
+++ b/pipeline/authz/keto_engine_acp_ory.go
@@ -94,9 +94,9 @@ func (a *AuthorizerKetoEngineACPORY) Authorize(r *http.Request, session *authn.A
return err
}
- compiled, err := rule.CompileURL()
- if err != nil {
- return errors.WithStack(err)
+ // only Regexp matching strategy is supported for now.
+ if !(a.c.AccessRuleMatchingStrategy() == "" || a.c.AccessRuleMatchingStrategy() == configuration.Regexp) {
+ return helper.ErrNonRegexpMatchingStrategy
}
subject := session.Subject
@@ -115,9 +115,19 @@ func (a *AuthorizerKetoEngineACPORY) Authorize(r *http.Request, session *authn.A
var b bytes.Buffer
u := fmt.Sprintf("%s://%s%s", r.URL.Scheme, r.URL.Host, r.URL.Path)
+
+ action, err := rule.ReplaceAllString(a.c.AccessRuleMatchingStrategy(), u, cf.RequiredAction)
+ if err != nil {
+ return errors.WithStack(err)
+ }
+ resource, err := rule.ReplaceAllString(a.c.AccessRuleMatchingStrategy(), u, cf.RequiredResource)
+ if err != nil {
+ return errors.WithStack(err)
+ }
+
if err := json.NewEncoder(&b).Encode(&AuthorizerKetoEngineACPORYRequestBody{
- Action: compiled.ReplaceAllString(u, cf.RequiredAction),
- Resource: compiled.ReplaceAllString(u, cf.RequiredResource),
+ Action: action,
+ Resource: resource,
Context: a.contextCreator(r),
Subject: subject,
}); err != nil {
@@ -130,10 +140,10 @@ func (a *AuthorizerKetoEngineACPORY) Authorize(r *http.Request, session *authn.A
}
req, err := http.NewRequest("POST", urlx.AppendPaths(baseURL, "/engines/acp/ory", flavor, "/allowed").String(), &b)
- req.Header.Add("Content-Type", "application/json")
if err != nil {
return errors.WithStack(err)
}
+ req.Header.Add("Content-Type", "application/json")
res, err := a.client.Do(req)
if err != nil {
diff --git a/pipeline/rule.go b/pipeline/rule.go
index 469282a0b4..56360d0fe1 100644
--- a/pipeline/rule.go
+++ b/pipeline/rule.go
@@ -1,8 +1,12 @@
package pipeline
-import "regexp"
+import (
+ "github.com/ory/oathkeeper/driver/configuration"
+)
type Rule interface {
GetID() string
- CompileURL() (*regexp.Regexp, error)
+ // ReplaceAllString searches the input string and replaces each match (with the rule's pattern)
+ // found with the replacement text.
+ ReplaceAllString(strategy configuration.MatchingStrategy, input, replacement string) (string, error)
}
diff --git a/proxy/proxy_test.go b/proxy/proxy_test.go
index 3f467fcbc1..eab4cb8887 100644
--- a/proxy/proxy_test.go
+++ b/proxy/proxy_test.go
@@ -21,6 +21,7 @@
package proxy_test
import (
+ "context"
"fmt"
"io/ioutil"
"net/http"
@@ -82,6 +83,20 @@ func TestProxy(t *testing.T) {
Mutators: []rule.Handler{{Handler: "noop"}},
Upstream: rule.Upstream{URL: backend.URL, StripPath: "/strip-path/", PreserveHost: true},
}
+ ruleNoOpAuthenticatorGlob := rule.Rule{
+ Match: &rule.Match{Methods: []string{"GET"}, URL: ts.URL + "/authn-noop/<[0-9]*>"},
+ Authenticators: []rule.Handler{{Handler: "noop"}},
+ Authorizer: rule.Handler{Handler: "allow"},
+ Mutators: []rule.Handler{{Handler: "noop"}},
+ Upstream: rule.Upstream{URL: backend.URL},
+ }
+ ruleNoOpAuthenticatorModifyUpstreamGlob := rule.Rule{
+ Match: &rule.Match{Methods: []string{"GET"}, URL: ts.URL + "/strip-path/authn-noop/<[0-9]*>"},
+ Authenticators: []rule.Handler{{Handler: "noop"}},
+ Authorizer: rule.Handler{Handler: "allow"},
+ Mutators: []rule.Handler{{Handler: "noop"}},
+ Upstream: rule.Upstream{URL: backend.URL, StripPath: "/strip-path/", PreserveHost: true},
+ }
// acceptRuleStripHost := rule.Rule{MatchesMethods: []string{"GET"}, MatchesURLCompiled: mustCompileRegex(t, proxy.URL+"/users/<[0-9]+>"), Mode: "pass_through_accept", Upstream: rule.Upstream{URLParsed: u, StripPath: "/users/", PreserveHost: true}}
// acceptRuleStripHostWithoutTrailing := rule.Rule{MatchesMethods: []string{"GET"}, MatchesURLCompiled: mustCompileRegex(t, proxy.URL+"/users/<[0-9]+>"), Mode: "pass_through_accept", Upstream: rule.Upstream{URLParsed: u, StripPath: "/users", PreserveHost: true}}
@@ -89,30 +104,34 @@ func TestProxy(t *testing.T) {
// denyRule := rule.Rule{MatchesMethods: []string{"GET"}, MatchesURLCompiled: mustCompileRegex(t, proxy.URL+"/users/<[0-9]+>"), Mode: "pass_through_deny", Upstream: rule.Upstream{URLParsed: u}}
for k, tc := range []struct {
- url string
- code int
- messages []string
- rules []rule.Rule
- transform func(r *http.Request)
- d string
+ url string
+ code int
+ messages []string
+ rulesRegexp []rule.Rule
+ rulesGlob []rule.Rule
+ transform func(r *http.Request)
+ d string
}{
{
- d: "should fail because url does not exist in rule set",
- url: ts.URL + "/invalid",
- rules: []rule.Rule{},
- code: http.StatusNotFound,
+ d: "should fail because url does not exist in rule set",
+ url: ts.URL + "/invalid",
+ rulesRegexp: []rule.Rule{},
+ rulesGlob: []rule.Rule{},
+ code: http.StatusNotFound,
},
{
- d: "should fail because url does exist but is matched by two rules",
- url: ts.URL + "/authn-noop/1234",
- rules: []rule.Rule{ruleNoOpAuthenticator, ruleNoOpAuthenticator},
- code: http.StatusInternalServerError,
+ d: "should fail because url does exist but is matched by two rules",
+ url: ts.URL + "/authn-noop/1234",
+ rulesRegexp: []rule.Rule{ruleNoOpAuthenticator, ruleNoOpAuthenticator},
+ rulesGlob: []rule.Rule{ruleNoOpAuthenticatorGlob, ruleNoOpAuthenticatorGlob},
+ code: http.StatusInternalServerError,
},
{
- d: "should pass",
- url: ts.URL + "/authn-noop/1234",
- rules: []rule.Rule{ruleNoOpAuthenticator},
- code: http.StatusOK,
+ d: "should pass",
+ url: ts.URL + "/authn-noop/1234",
+ rulesRegexp: []rule.Rule{ruleNoOpAuthenticator},
+ rulesGlob: []rule.Rule{ruleNoOpAuthenticatorGlob},
+ code: http.StatusOK,
transform: func(r *http.Request) {
r.Header.Add("Authorization", "bearer token")
},
@@ -123,10 +142,11 @@ func TestProxy(t *testing.T) {
},
},
{
- d: "should pass",
- url: ts.URL + "/strip-path/authn-noop/1234",
- rules: []rule.Rule{ruleNoOpAuthenticatorModifyUpstream},
- code: http.StatusOK,
+ d: "should pass",
+ url: ts.URL + "/strip-path/authn-noop/1234",
+ rulesRegexp: []rule.Rule{ruleNoOpAuthenticatorModifyUpstream},
+ rulesGlob: []rule.Rule{ruleNoOpAuthenticatorModifyUpstreamGlob},
+ code: http.StatusOK,
transform: func(r *http.Request) {
r.Header.Add("Authorization", "bearer token")
},
@@ -139,11 +159,16 @@ func TestProxy(t *testing.T) {
{
d: "should fail because no authorizer was configured",
url: ts.URL + "/authn-anon/authz-none/cred-none/1234",
- rules: []rule.Rule{{
+ rulesRegexp: []rule.Rule{{
Match: &rule.Match{Methods: []string{"GET"}, URL: ts.URL + "/authn-anon/authz-none/cred-none/<[0-9]+>"},
Authenticators: []rule.Handler{{Handler: "anonymous"}},
Upstream: rule.Upstream{URL: backend.URL},
}},
+ rulesGlob: []rule.Rule{{
+ Match: &rule.Match{Methods: []string{"GET"}, URL: ts.URL + "/authn-anon/authz-none/cred-none/<[0-9]*>"},
+ Authenticators: []rule.Handler{{Handler: "anonymous"}},
+ Upstream: rule.Upstream{URL: backend.URL},
+ }},
transform: func(r *http.Request) {
r.Header.Add("Authorization", "bearer token")
},
@@ -152,24 +177,37 @@ func TestProxy(t *testing.T) {
{
d: "should fail because no credentials issuer was configured",
url: ts.URL + "/authn-anon/authz-allow/cred-none/1234",
- rules: []rule.Rule{{
+ rulesRegexp: []rule.Rule{{
Match: &rule.Match{Methods: []string{"GET"}, URL: ts.URL + "/authn-anon/authz-allow/cred-none/<[0-9]+>"},
Authenticators: []rule.Handler{{Handler: "anonymous"}},
Authorizer: rule.Handler{Handler: "allow"},
Upstream: rule.Upstream{URL: backend.URL},
}},
+ rulesGlob: []rule.Rule{{
+ Match: &rule.Match{Methods: []string{"GET"}, URL: ts.URL + "/authn-anon/authz-allow/cred-none/<[0-9]*>"},
+ Authenticators: []rule.Handler{{Handler: "anonymous"}},
+ Authorizer: rule.Handler{Handler: "allow"},
+ Upstream: rule.Upstream{URL: backend.URL},
+ }},
code: http.StatusInternalServerError,
},
{
d: "should pass with anonymous and everything else set to noop",
url: ts.URL + "/authn-anon/authz-allow/cred-noop/1234",
- rules: []rule.Rule{{
+ rulesRegexp: []rule.Rule{{
Match: &rule.Match{Methods: []string{"GET"}, URL: ts.URL + "/authn-anon/authz-allow/cred-noop/<[0-9]+>"},
Authenticators: []rule.Handler{{Handler: "anonymous"}},
Authorizer: rule.Handler{Handler: "allow"},
Mutators: []rule.Handler{{Handler: "noop"}},
Upstream: rule.Upstream{URL: backend.URL},
}},
+ rulesGlob: []rule.Rule{{
+ Match: &rule.Match{Methods: []string{"GET"}, URL: ts.URL + "/authn-anon/authz-allow/cred-noop/<[0-9]*>"},
+ Authenticators: []rule.Handler{{Handler: "anonymous"}},
+ Authorizer: rule.Handler{Handler: "allow"},
+ Mutators: []rule.Handler{{Handler: "noop"}},
+ Upstream: rule.Upstream{URL: backend.URL},
+ }},
code: http.StatusOK,
messages: []string{
"authorization=",
@@ -180,19 +218,26 @@ func TestProxy(t *testing.T) {
{
d: "should fail when authorizer fails",
url: ts.URL + "/authn-anon/authz-deny/cred-noop/1234",
- rules: []rule.Rule{{
+ rulesRegexp: []rule.Rule{{
Match: &rule.Match{Methods: []string{"GET"}, URL: ts.URL + "/authn-anon/authz-deny/cred-noop/<[0-9]+>"},
Authenticators: []rule.Handler{{Handler: "anonymous"}},
Authorizer: rule.Handler{Handler: "deny"},
Mutators: []rule.Handler{{Handler: "noop"}},
Upstream: rule.Upstream{URL: backend.URL},
}},
+ rulesGlob: []rule.Rule{{
+ Match: &rule.Match{Methods: []string{"GET"}, URL: ts.URL + "/authn-anon/authz-deny/cred-noop/<[0-9]*>"},
+ Authenticators: []rule.Handler{{Handler: "anonymous"}},
+ Authorizer: rule.Handler{Handler: "deny"},
+ Mutators: []rule.Handler{{Handler: "noop"}},
+ Upstream: rule.Upstream{URL: backend.URL},
+ }},
code: http.StatusForbidden,
},
{
d: "should fail when authorizer fails and send www_authenticate as defined in the rule",
url: ts.URL + "/authn-anon/authz-deny/cred-noop/1234",
- rules: []rule.Rule{{
+ rulesRegexp: []rule.Rule{{
Match: &rule.Match{Methods: []string{"GET"}, URL: ts.URL + "/authn-anon/authz-deny/cred-noop/<[0-9]+>"},
Authenticators: []rule.Handler{{Handler: "anonymous"}},
Authorizer: rule.Handler{Handler: "deny"},
@@ -200,78 +245,123 @@ func TestProxy(t *testing.T) {
Upstream: rule.Upstream{URL: backend.URL},
Errors: []rule.ErrorHandler{{Handler: "www_authenticate"}},
}},
+ rulesGlob: []rule.Rule{{
+ Match: &rule.Match{Methods: []string{"GET"}, URL: ts.URL + "/authn-anon/authz-deny/cred-noop/<[0-9]*>"},
+ Authenticators: []rule.Handler{{Handler: "anonymous"}},
+ Authorizer: rule.Handler{Handler: "deny"},
+ Mutators: []rule.Handler{{Handler: "noop"}},
+ Upstream: rule.Upstream{URL: backend.URL},
+ Errors: []rule.ErrorHandler{{Handler: "www_authenticate"}},
+ }},
code: http.StatusUnauthorized,
},
{
d: "should fail when authenticator fails",
url: ts.URL + "/authn-broken/authz-none/cred-none/1234",
- rules: []rule.Rule{{
+ rulesRegexp: []rule.Rule{{
Match: &rule.Match{Methods: []string{"GET"}, URL: ts.URL + "/authn-broken/authz-none/cred-none/<[0-9]+>"},
Authenticators: []rule.Handler{{Handler: "unauthorized"}},
Upstream: rule.Upstream{URL: backend.URL},
}},
+ rulesGlob: []rule.Rule{{
+ Match: &rule.Match{Methods: []string{"GET"}, URL: ts.URL + "/authn-broken/authz-none/cred-none/<[0-9]*>"},
+ Authenticators: []rule.Handler{{Handler: "unauthorized"}},
+ Upstream: rule.Upstream{URL: backend.URL},
+ }},
code: http.StatusUnauthorized,
},
{
d: "should fail because no mutator was configured",
url: ts.URL + "/authn-anon/authz-deny/cred-noop/1234",
- rules: []rule.Rule{{
+ rulesRegexp: []rule.Rule{{
Match: &rule.Match{Methods: []string{"GET"}, URL: ts.URL + "/authn-anon/authz-deny/cred-noop/<[0-9]+>"},
Authenticators: []rule.Handler{{Handler: "anonymous"}},
Authorizer: rule.Handler{Handler: "allow"},
Upstream: rule.Upstream{URL: backend.URL},
}},
+ rulesGlob: []rule.Rule{{
+ Match: &rule.Match{Methods: []string{"GET"}, URL: ts.URL + "/authn-anon/authz-deny/cred-noop/<[0-9]*>"},
+ Authenticators: []rule.Handler{{Handler: "anonymous"}},
+ Authorizer: rule.Handler{Handler: "allow"},
+ Upstream: rule.Upstream{URL: backend.URL},
+ }},
code: http.StatusInternalServerError,
},
{
d: "should fail when one of the mutators fails",
url: ts.URL + "/authn-anon/authz-deny/cred-noop/1234",
- rules: []rule.Rule{{
+ rulesRegexp: []rule.Rule{{
Match: &rule.Match{Methods: []string{"GET"}, URL: ts.URL + "/authn-anon/authz-deny/cred-noop/<[0-9]+>"},
Authenticators: []rule.Handler{{Handler: "anonymous"}},
Authorizer: rule.Handler{Handler: "allow"},
Mutators: []rule.Handler{{Handler: "noop"}, {Handler: "broken"}},
Upstream: rule.Upstream{URL: backend.URL},
}},
+ rulesGlob: []rule.Rule{{
+ Match: &rule.Match{Methods: []string{"GET"}, URL: ts.URL + "/authn-anon/authz-deny/cred-noop/<[0-9]*>"},
+ Authenticators: []rule.Handler{{Handler: "anonymous"}},
+ Authorizer: rule.Handler{Handler: "allow"},
+ Mutators: []rule.Handler{{Handler: "noop"}, {Handler: "broken"}},
+ Upstream: rule.Upstream{URL: backend.URL},
+ }},
code: http.StatusInternalServerError,
},
{
d: "should fail when credentials issuer fails",
url: ts.URL + "/authn-anonymous/authz-allow/cred-broken/1234",
- rules: []rule.Rule{{
+ rulesRegexp: []rule.Rule{{
Match: &rule.Match{Methods: []string{"GET"}, URL: ts.URL + "/authn-anonymous/authz-allow/cred-broken/<[0-9]+>"},
Authenticators: []rule.Handler{{Handler: "anonymous"}},
Authorizer: rule.Handler{Handler: "allow"},
Mutators: []rule.Handler{{Handler: "broken"}},
Upstream: rule.Upstream{URL: backend.URL},
}},
+ rulesGlob: []rule.Rule{{
+ Match: &rule.Match{Methods: []string{"GET"}, URL: ts.URL + "/authn-anonymous/authz-allow/cred-broken/<[0-9]*>"},
+ Authenticators: []rule.Handler{{Handler: "anonymous"}},
+ Authorizer: rule.Handler{Handler: "allow"},
+ Mutators: []rule.Handler{{Handler: "broken"}},
+ Upstream: rule.Upstream{URL: backend.URL},
+ }},
code: http.StatusInternalServerError,
},
} {
t.Run(fmt.Sprintf("case=%d/description=%s", k, tc.d), func(t *testing.T) {
- reg.RuleRepository().(*rule.RepositoryMemory).WithRules(tc.rules)
+ testFunc := func(strategy configuration.MatchingStrategy, rules []rule.Rule) {
+ reg.RuleRepository().(*rule.RepositoryMemory).WithRules(rules)
+ require.NoError(t, reg.RuleRepository().SetMatchingStrategy(context.Background(), strategy))
- req, err := http.NewRequest("GET", tc.url, nil)
- require.NoError(t, err)
- if tc.transform != nil {
- tc.transform(req)
- }
+ req, err := http.NewRequest("GET", tc.url, nil)
+ require.NoError(t, err)
+ if tc.transform != nil {
+ tc.transform(req)
+ }
- res, err := http.DefaultClient.Do(req)
- require.NoError(t, err)
+ res, err := http.DefaultClient.Do(req)
+ require.NoError(t, err)
- greeting, err := ioutil.ReadAll(res.Body)
- require.NoError(t, res.Body.Close())
- require.NoError(t, err)
+ greeting, err := ioutil.ReadAll(res.Body)
+ require.NoError(t, res.Body.Close())
+ require.NoError(t, err)
- assert.Equal(t, tc.code, res.StatusCode, "%s", res.Body)
- for _, m := range tc.messages {
- assert.True(t, strings.Contains(string(greeting), m), `Value "%s" not found in message:
+ assert.Equal(t, tc.code, res.StatusCode, "%s", res.Body)
+ for _, m := range tc.messages {
+ assert.True(t, strings.Contains(string(greeting), m), `Value "%s" not found in message:
%s
proxy_url=%s
backend_url=%s
`, m, greeting, ts.URL, backend.URL)
+ }
+
}
+
+ t.Run("regexp", func(t *testing.T) {
+ testFunc(configuration.Regexp, tc.rulesRegexp)
+ })
+ t.Run("glob", func(t *testing.T) {
+ testFunc(configuration.Glob, tc.rulesGlob)
+ })
+
})
}
}
diff --git a/rule/engine_glob.go b/rule/engine_glob.go
new file mode 100644
index 0000000000..a0c4693a00
--- /dev/null
+++ b/rule/engine_glob.go
@@ -0,0 +1,99 @@
+package rule
+
+import (
+ "bytes"
+ "hash/crc64"
+
+ "github.com/gobwas/glob"
+)
+
+type globMatchingEngine struct {
+ compiled glob.Glob
+ checksum uint64
+ table *crc64.Table
+}
+
+// Checksum of a saved pattern.
+func (ge *globMatchingEngine) Checksum() uint64 {
+ return ge.checksum
+}
+
+// IsMatching determines whether the input matches the pattern.
+func (ge *globMatchingEngine) IsMatching(pattern, matchAgainst string) (bool, error) {
+ if err := ge.compile(pattern); err != nil {
+ return false, err
+ }
+ return ge.compiled.Match(matchAgainst), nil
+}
+
+// ReplaceAllString is noop for now and always returns an error.
+func (ge *globMatchingEngine) ReplaceAllString(_, _, _ string) (string, error) {
+ return "", ErrMethodNotImplemented
+}
+
+func (ge *globMatchingEngine) compile(pattern string) error {
+ if ge.table == nil {
+ ge.table = crc64.MakeTable(polynomial)
+ }
+ if checksum := crc64.Checksum([]byte(pattern), ge.table); checksum != ge.checksum {
+ compiled, err := compileGlob(pattern, '<', '>')
+ if err != nil {
+ return err
+ }
+ ge.checksum = checksum
+ ge.compiled = compiled
+ }
+ return nil
+}
+
+// delimiterIndices returns the first level delimiter indices from a string.
+// It returns an error in case of unbalanced delimiters.
+func delimiterIndices(s string, delimiterStart, delimiterEnd rune) ([]int, error) {
+ var level, idx int
+ idxs := make([]int, 0)
+ for ind := 0; ind < len(s); ind++ {
+ switch s[ind] {
+ case byte(delimiterStart):
+ if level++; level == 1 {
+ idx = ind
+ }
+ case byte(delimiterEnd):
+ if level--; level == 0 {
+ idxs = append(idxs, idx, ind+1)
+ } else if level < 0 {
+ return nil, ErrUnbalancedPattern
+ }
+ }
+ }
+
+ if level != 0 {
+ return nil, ErrUnbalancedPattern
+ }
+ return idxs, nil
+}
+
+func compileGlob(pattern string, delimiterStart, delimiterEnd rune) (glob.Glob, error) {
+ // Check if it is well-formed.
+ idxs, errBraces := delimiterIndices(pattern, delimiterStart, delimiterEnd)
+ if errBraces != nil {
+ return nil, errBraces
+ }
+ buffer := bytes.NewBufferString("")
+
+ var end int
+ for ind := 0; ind < len(idxs); ind += 2 {
+ // Set all values we are interested in.
+ raw := pattern[end:idxs[ind]]
+ end = idxs[ind+1]
+ patt := pattern[idxs[ind]+1 : end-1]
+ buffer.WriteString(glob.QuoteMeta(raw))
+ buffer.WriteString(patt)
+ }
+
+ // Add the remaining.
+ raw := pattern[end:]
+ buffer.WriteString(glob.QuoteMeta(raw))
+
+ // Compile full regexp.
+ return glob.Compile(buffer.String())
+}
diff --git a/rule/engine_glob_test.go b/rule/engine_glob_test.go
new file mode 100644
index 0000000000..da08639c5c
--- /dev/null
+++ b/rule/engine_glob_test.go
@@ -0,0 +1,259 @@
+package rule
+
+import (
+ "strconv"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestDelimiters(t *testing.T) {
+ var tests = []struct {
+ input string
+ out []int
+ err error
+ }{
+ {
+ input: "<",
+ err: ErrUnbalancedPattern,
+ },
+ {
+ input: ">",
+ err: ErrUnbalancedPattern,
+ },
+ {
+ input: ">>",
+ err: ErrUnbalancedPattern,
+ },
+ {
+ input: "><>",
+ err: ErrUnbalancedPattern,
+ },
+ {
+ input: "foo.barvar",
+ err: ErrUnbalancedPattern,
+ },
+ {
+ input: "foo.bar>var",
+ err: ErrUnbalancedPattern,
+ },
+ {
+ input: "foo.bar<<>>",
+ out: []int{
+ 7, 11,
+ },
+ },
+ {
+ input: "foo.bar<<>><>",
+ out: []int{
+ 7, 11,
+ 11, 13,
+ },
+ },
+ {
+ input: "foo.bar<<>><>tt<>",
+ out: []int{
+ 7, 11,
+ 11, 13,
+ 15, 17,
+ },
+ },
+ }
+
+ for tn, tc := range tests {
+ t.Run(strconv.Itoa(tn), func(t *testing.T) {
+ out, err := delimiterIndices(tc.input, '<', '>')
+ assert.Equal(t, tc.out, out)
+ assert.Equal(t, tc.err, err)
+
+ })
+ }
+}
+
+func TestIsMatch(t *testing.T) {
+ type args struct {
+ pattern string
+ matchAgainst string
+ }
+ tests := []struct {
+ name string
+ args args
+ want bool
+ wantErr bool
+ }{
+ {
+ name: "question mark1",
+ args: args{
+ pattern: `urn:foo:>`,
+ matchAgainst: "urn:foo:user",
+ },
+ want: false,
+ wantErr: false,
+ },
+ {
+ name: "question mark2",
+ args: args{
+ pattern: `urn:foo:>`,
+ matchAgainst: "urn:foo:u",
+ },
+ want: true,
+ wantErr: false,
+ },
+ {
+ name: "question mark3",
+ args: args{
+ pattern: `urn:foo:>`,
+ matchAgainst: "urn:foo:",
+ },
+ want: false,
+ wantErr: false,
+ },
+ {
+ name: "question mark4",
+ args: args{
+ pattern: `urn:foo:>&&>`,
+ matchAgainst: "urn:foo:w&&r",
+ },
+ want: true,
+ wantErr: false,
+ },
+ {
+ name: "question mark5 - both as a special char and a literal",
+ args: args{
+ pattern: `urn:foo:>?>`,
+ matchAgainst: "urn:foo:w&r",
+ },
+ want: false,
+ wantErr: false,
+ },
+ {
+ name: "question mark5 - both as a special char and a literal1",
+ args: args{
+ pattern: `urn:foo:>?>`,
+ matchAgainst: "urn:foo:w?r",
+ },
+ want: true,
+ wantErr: false,
+ },
+ {
+ name: "asterisk",
+ args: args{
+ pattern: `urn:foo:<*>`,
+ matchAgainst: "urn:foo:user",
+ },
+ want: true,
+ wantErr: false,
+ },
+ {
+ name: "asterisk1",
+ args: args{
+ pattern: `urn:foo:<*>`,
+ matchAgainst: "urn:foo:",
+ },
+ want: true,
+ wantErr: false,
+ },
+ {
+ name: "asterisk2",
+ args: args{
+ pattern: `urn:foo:<*>:<*>`,
+ matchAgainst: "urn:foo:usr:swen",
+ },
+ want: true,
+ wantErr: false,
+ },
+ {
+ name: "asterisk: both as a special char and a literal",
+ args: args{
+ pattern: `*:foo:<*>:<*>`,
+ matchAgainst: "urn:foo:usr:swen",
+ },
+ want: false,
+ wantErr: false,
+ },
+ {
+ name: "asterisk: both as a special char and a literal1",
+ args: args{
+ pattern: `*:foo:<*>:<*>`,
+ matchAgainst: "*:foo:usr:swen",
+ },
+ want: true,
+ wantErr: false,
+ },
+ {
+ name: "asterisk + question mark",
+ args: args{
+ pattern: `urn:foo:<*>:role:>`,
+ matchAgainst: "urn:foo:usr:role:a",
+ },
+ want: true,
+ wantErr: false,
+ },
+ {
+ name: "asterisk + question mark1",
+ args: args{
+ pattern: `urn:foo:<*>:role:>`,
+ matchAgainst: "urn:foo:usr:role:admin",
+ },
+ want: false,
+ wantErr: false,
+ },
+ {
+ name: "square brackets",
+ args: args{
+ pattern: `urn:foo:`,
+ matchAgainst: "urn:foo:moon",
+ },
+ want: false,
+ wantErr: false,
+ },
+ {
+ name: "square brackets1",
+ args: args{
+ pattern: `urn:foo:`,
+ matchAgainst: "urn:foo:man",
+ },
+ want: true,
+ wantErr: false,
+ },
+ {
+ name: "square brackets2",
+ args: args{
+ pattern: `urn:foo:`,
+ matchAgainst: "urn:foo:man",
+ },
+ want: false,
+ wantErr: false,
+ },
+ {
+ name: "square brackets3",
+ args: args{
+ pattern: `urn:foo:`,
+ matchAgainst: "urn:foo:min",
+ },
+ want: true,
+ wantErr: false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ globEngine := new(globMatchingEngine)
+ got, err := globEngine.IsMatching(tt.args.pattern, tt.args.matchAgainst)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("IsMatching() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ if got != tt.want {
+ t.Errorf("IsMatching() got = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
diff --git a/rule/engine_regexp.go b/rule/engine_regexp.go
new file mode 100644
index 0000000000..4ecc02d450
--- /dev/null
+++ b/rule/engine_regexp.go
@@ -0,0 +1,50 @@
+package rule
+
+import (
+ "hash/crc64"
+
+ "github.com/dlclark/regexp2"
+ "github.com/ory/ladon/compiler"
+)
+
+type regexpMatchingEngine struct {
+ compiled *regexp2.Regexp
+ checksum uint64
+ table *crc64.Table
+}
+
+func (re *regexpMatchingEngine) compile(pattern string) error {
+ if re.table == nil {
+ re.table = crc64.MakeTable(polynomial)
+ }
+ if checksum := crc64.Checksum([]byte(pattern), re.table); checksum != re.checksum {
+ compiled, err := compiler.CompileRegex(pattern, '<', '>')
+ if err != nil {
+ return err
+ }
+ re.compiled = compiled
+ re.checksum = checksum
+ }
+ return nil
+}
+
+// Checksum of a saved pattern.
+func (re *regexpMatchingEngine) Checksum() uint64 {
+ return re.checksum
+}
+
+// IsMatching determines whether the input matches the pattern.
+func (re *regexpMatchingEngine) IsMatching(pattern, matchAgainst string) (bool, error) {
+ if err := re.compile(pattern); err != nil {
+ return false, err
+ }
+ return re.compiled.MatchString(matchAgainst)
+}
+
+// ReplaceAllString replaces all matches in `input` with `replacement`.
+func (re *regexpMatchingEngine) ReplaceAllString(pattern, input, replacement string) (string, error) {
+ if err := re.compile(pattern); err != nil {
+ return "", err
+ }
+ return re.compiled.Replace(input, replacement, -1, -1)
+}
diff --git a/rule/fetcher_default.go b/rule/fetcher_default.go
index 25da8ec20f..0de8565260 100644
--- a/rule/fetcher_default.go
+++ b/rule/fetcher_default.go
@@ -40,8 +40,9 @@ type event struct {
type eventType int
const (
- eventRepositoryConfigChange eventType = iota
+ eventRepositoryConfigChanged eventType = iota
eventFileChanged
+ eventMatchingStrategyChanged
)
var _ Fetcher = new(FetcherDefault)
@@ -197,12 +198,24 @@ func (f *FetcherDefault) watch(ctx context.Context, watcher *fsnotify.Watcher, e
return nil
}
- f.enqueueEvent(events, event{et: eventRepositoryConfigChange, source: "viper_watcher"})
+ f.enqueueEvent(events, event{et: eventRepositoryConfigChanged, source: "viper_watcher"})
return nil
})
+ f.enqueueEvent(events, event{et: eventRepositoryConfigChanged, source: "entrypoint"})
- f.enqueueEvent(events, event{et: eventRepositoryConfigChange, source: "entrypoint"})
+ var strategy map[string]interface{}
+ viperx.AddWatcher(func(e fsnotify.Event) error {
+ if reflect.DeepEqual(strategy, viper.Get(configuration.ViperKeyAccessRuleMatchingStrategy)) {
+ f.r.Logger().
+ Debug("Not reloading access rule matching strategy because configuration value has not changed.")
+ return nil
+ }
+
+ f.enqueueEvent(events, event{et: eventMatchingStrategyChanged, source: "viper_watcher"})
+ return nil
+ })
+ f.enqueueEvent(events, event{et: eventMatchingStrategyChanged, source: "entrypoint"})
for {
select {
@@ -238,7 +251,7 @@ func (f *FetcherDefault) watch(ctx context.Context, watcher *fsnotify.Watcher, e
WithField("op", e.Op.String()).
Debugf("Detected file change in directory containing access rules. Triggering a reload.")
- f.enqueueEvent(events, event{et: eventRepositoryConfigChange, source: "fsnotify"})
+ f.enqueueEvent(events, event{et: eventRepositoryConfigChanged, source: "fsnotify"})
case e, ok := <-events:
if !ok {
// channel was closed
@@ -247,7 +260,7 @@ func (f *FetcherDefault) watch(ctx context.Context, watcher *fsnotify.Watcher, e
}
switch e.et {
- case eventRepositoryConfigChange:
+ case eventRepositoryConfigChanged:
f.r.Logger().
WithField("event", "config_change").
WithField("source", e.source).
@@ -255,6 +268,14 @@ func (f *FetcherDefault) watch(ctx context.Context, watcher *fsnotify.Watcher, e
if err := f.configUpdate(ctx, watcher, f.c.AccessRuleRepositories(), events); err != nil {
return err
}
+ case eventMatchingStrategyChanged:
+ f.r.Logger().
+ WithField("event", "matching_strategy_config_change").
+ WithField("source", e.source).
+ Debugf("Viper detected a configuration change, updating matching strategy")
+ if err := f.r.RuleRepository().SetMatchingStrategy(ctx, f.c.AccessRuleMatchingStrategy()); err != nil {
+ return errors.Wrapf(err, "unable to update matching strategy")
+ }
case eventFileChanged:
f.r.Logger().
WithField("event", "repository_change").
diff --git a/rule/fetcher_default_test.go b/rule/fetcher_default_test.go
index 3fc37bea8d..f606a9ea3b 100644
--- a/rule/fetcher_default_test.go
+++ b/rule/fetcher_default_test.go
@@ -28,6 +28,107 @@ import (
const testRule = `[{"id":"test-rule-5","upstream":{"preserve_host":true,"strip_path":"/api","url":"mybackend.com/api"},"match":{"url":"myproxy.com/api","methods":["GET","POST"]},"authenticators":[{"handler":"noop"},{"handler":"anonymous"}],"authorizer":{"handler":"allow"},"mutators":[{"handler":"noop"}]}]`
+func TestFetcherReload(t *testing.T) {
+ viper.Reset()
+ conf := internal.NewConfigurationWithDefaults() // this resets viper and must be at the top
+ r := internal.NewRegistry(conf)
+ testConfigPath := "../test/update"
+
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ _, _ = w.Write([]byte(testRule))
+ }))
+ defer ts.Close()
+
+ tempdir := os.TempDir()
+
+ id := uuid.New().String()
+ configFile := filepath.Join(tempdir, ".oathkeeper-"+id+".yml")
+ require.NoError(t, ioutil.WriteFile(configFile, []byte(""), 0666))
+
+ l := logrus.New()
+ l.Level = logrus.TraceLevel
+ viperx.InitializeConfig("oathkeeper-"+id, tempdir, nil)
+ viperx.WatchConfig(l, nil)
+
+ go func() {
+ require.NoError(t, r.RuleFetcher().Watch(context.TODO()))
+ }()
+
+ // initial config without a repo and without a matching strategy
+ config, err := ioutil.ReadFile(path.Join(testConfigPath, "config_no_repo.yaml"))
+ require.NoError(t, err)
+ require.NoError(t, ioutil.WriteFile(configFile, config, 0666))
+ time.Sleep(time.Millisecond * 500)
+
+ rules, err := r.RuleRepository().List(context.Background(), 500, 0)
+ require.NoError(t, err)
+ require.Empty(t, rules)
+
+ strategy, err := r.RuleRepository().MatchingStrategy(context.Background())
+ require.NoError(t, err)
+ require.Equal(t, configuration.MatchingStrategy(""), strategy)
+
+ // config with a repo and without a matching strategy
+ config, err = ioutil.ReadFile(path.Join(testConfigPath, "config_default.yaml"))
+ require.NoError(t, err)
+ require.NoError(t, ioutil.WriteFile(configFile, config, 0666))
+ time.Sleep(time.Millisecond * 500)
+
+ rules, err = r.RuleRepository().List(context.Background(), 500, 0)
+ require.NoError(t, err)
+ require.Equal(t, 1, len(rules))
+ require.Equal(t, "test-rule-1-glob", rules[0].ID)
+
+ strategy, err = r.RuleRepository().MatchingStrategy(context.Background())
+ require.NoError(t, err)
+ require.Equal(t, configuration.MatchingStrategy(""), strategy)
+
+ // config with a glob matching strategy
+ config, err = ioutil.ReadFile(path.Join(testConfigPath, "config_glob.yaml"))
+ require.NoError(t, err)
+ require.NoError(t, ioutil.WriteFile(configFile, config, 0666))
+ time.Sleep(time.Millisecond * 500)
+
+ rules, err = r.RuleRepository().List(context.Background(), 500, 0)
+ require.NoError(t, err)
+ require.Equal(t, 1, len(rules))
+ require.Equal(t, "test-rule-1-glob", rules[0].ID)
+
+ strategy, err = r.RuleRepository().MatchingStrategy(context.Background())
+ require.NoError(t, err)
+ require.Equal(t, configuration.Glob, strategy)
+
+ // config with unknown matching strategy
+ config, err = ioutil.ReadFile(path.Join(testConfigPath, "config_error.yaml"))
+ require.NoError(t, err)
+ require.NoError(t, ioutil.WriteFile(configFile, config, 0666))
+ time.Sleep(time.Millisecond * 500)
+
+ rules, err = r.RuleRepository().List(context.Background(), 500, 0)
+ require.NoError(t, err)
+ require.Equal(t, 1, len(rules))
+ require.Equal(t, "test-rule-1-glob", rules[0].ID)
+
+ strategy, err = r.RuleRepository().MatchingStrategy(context.Background())
+ require.NoError(t, err)
+ require.Equal(t, configuration.MatchingStrategy("UNKNOWN"), strategy)
+
+ // config with regexp matching strategy
+ config, err = ioutil.ReadFile(path.Join(testConfigPath, "config_regexp.yaml"))
+ require.NoError(t, err)
+ require.NoError(t, ioutil.WriteFile(configFile, config, 0666))
+ time.Sleep(time.Millisecond * 500)
+
+ rules, err = r.RuleRepository().List(context.Background(), 500, 0)
+ require.NoError(t, err)
+ require.Equal(t, 1, len(rules))
+ require.Equal(t, "test-rule-1-glob", rules[0].ID)
+
+ strategy, err = r.RuleRepository().MatchingStrategy(context.Background())
+ require.NoError(t, err)
+ require.Equal(t, configuration.Regexp, strategy)
+}
+
func TestFetcherWatchConfig(t *testing.T) {
viper.Reset()
conf := internal.NewConfigurationWithDefaults() // this resets viper and must be at the top
@@ -54,10 +155,11 @@ func TestFetcherWatchConfig(t *testing.T) {
}()
for k, tc := range []struct {
- config string
- tmpContent string
- expectIDs []string
- expectNone bool
+ config string
+ tmpContent string
+ expectIDs []string
+ expectNone bool
+ expectedStrategy configuration.MatchingStrategy
}{
{config: ""},
{
@@ -86,14 +188,18 @@ access_rules:
access_rules:
repositories:
- file://../test/stub/rules.yaml
+ matching_strategy: glob
`,
- expectIDs: []string{"test-rule-1-yaml"},
+ expectIDs: []string{"test-rule-1-yaml"},
+ expectedStrategy: configuration.Glob,
},
{
config: `
access_rules:
repositories:
+ matching_strategy: regexp
`,
+ expectedStrategy: configuration.Regexp,
},
} {
t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) {
@@ -104,6 +210,10 @@ access_rules:
require.NoError(t, err)
require.Len(t, rules, len(tc.expectIDs))
+ strategy, err := r.RuleRepository().MatchingStrategy(context.Background())
+ require.NoError(t, err)
+ require.Equal(t, tc.expectedStrategy, strategy)
+
ids := make([]string, len(rules))
for k, r := range rules {
ids[k] = r.ID
diff --git a/rule/matcher_test.go b/rule/matcher_test.go
index f82ee4a813..86d02c97a6 100644
--- a/rule/matcher_test.go
+++ b/rule/matcher_test.go
@@ -28,8 +28,16 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+
+ "github.com/ory/oathkeeper/driver/configuration"
)
+func mustParseURL(t *testing.T, u string) *url.URL {
+ p, err := url.Parse(u)
+ require.NoError(t, err)
+ return p
+}
+
var testRules = []Rule{
{
ID: "foo1",
@@ -60,10 +68,34 @@ var testRules = []Rule{
},
}
-func mustParseURL(t *testing.T, u string) *url.URL {
- p, err := url.Parse(u)
- require.NoError(t, err)
- return p
+var testRulesGlob = []Rule{
+ {
+ ID: "foo1",
+ Match: &Match{URL: "https://localhost:1234/<{foo*,bar*}>", Methods: []string{"POST"}},
+ Description: "Create users rule",
+ 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://localhost:1235/", StripPath: "/bar", PreserveHost: true},
+ },
+ {
+ ID: "foo2",
+ Match: &Match{URL: "https://localhost:34/<{baz*,bar*}>", Methods: []string{"GET"}},
+ Description: "Get users rule",
+ Authorizer: Handler{Handler: "deny", Config: []byte(`{"type":"any"}`)},
+ Authenticators: []Handler{{Handler: "oauth2_introspection", Config: []byte(`{"name":"anonymous1"}`)}},
+ Mutators: []Handler{{Handler: "id_token", Config: []byte(`{"issuer":"anything"}`)}},
+ Upstream: Upstream{URL: "http://localhost:333/", StripPath: "/foo", PreserveHost: false},
+ },
+ {
+ ID: "foo3",
+ Match: &Match{URL: "https://localhost:343/<{baz*,bar*}>", Methods: []string{"GET"}},
+ Description: "Get users rule",
+ Authorizer: Handler{Handler: "deny"},
+ Authenticators: []Handler{{Handler: "oauth2_introspection"}},
+ Mutators: []Handler{{Handler: "id_token"}},
+ Upstream: Upstream{URL: "http://localhost:3333/", StripPath: "/foo", PreserveHost: false},
+ },
}
func TestMatcher(t *testing.T) {
@@ -85,7 +117,7 @@ func TestMatcher(t *testing.T) {
for name, matcher := range map[string]m{
"memory": NewRepositoryMemory(new(mockRepositoryRegistry)),
} {
- t.Run(fmt.Sprintf("matcher=%s", name), func(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", true, nil)
testMatcher(t, matcher, "POST", "https://localhost:1234/foo", true, nil)
@@ -105,8 +137,7 @@ func TestMatcher(t *testing.T) {
require.NoError(t, err)
got, err := matcher.Get(context.Background(), r.ID)
require.NoError(t, err)
- assert.NotEmpty(t, got.Match.compiledURL)
- assert.NotEmpty(t, got.Match.compiledURLChecksum)
+ assert.NotEmpty(t, got.matchingEngine.Checksum())
})
require.NoError(t, matcher.Set(context.Background(), testRules[1:]))
@@ -117,5 +148,38 @@ func TestMatcher(t *testing.T) {
testMatcher(t, matcher, "DELETE", "https://localhost:1234/foo", true, nil)
})
})
+ 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", true, nil)
+ testMatcher(t, matcher, "POST", "https://localhost:1234/foo", true, nil)
+ testMatcher(t, matcher, "DELETE", "https://localhost:1234/foo", 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", false, &testRulesGlob[1])
+ testMatcher(t, matcher, "POST", "https://localhost:1234/foo", false, &testRulesGlob[0])
+ testMatcher(t, matcher, "DELETE", "https://localhost:1234/foo", true, nil)
+ })
+
+ t.Run("case=cache", func(t *testing.T) {
+ r, err := matcher.Match(context.Background(), "GET", mustParseURL(t, "https://localhost:34/baz"))
+ require.NoError(t, err)
+ got, err := matcher.Get(context.Background(), r.ID)
+ require.NoError(t, err)
+ assert.NotEmpty(t, got.matchingEngine.Checksum())
+ })
+
+ 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", false, &testRulesGlob[1])
+ testMatcher(t, matcher, "POST", "https://localhost:1234/foo", true, nil)
+ testMatcher(t, matcher, "DELETE", "https://localhost:1234/foo", true, nil)
+ })
+ })
}
}
diff --git a/rule/matching_engine.go b/rule/matching_engine.go
new file mode 100644
index 0000000000..1acc3344fa
--- /dev/null
+++ b/rule/matching_engine.go
@@ -0,0 +1,24 @@
+package rule
+
+import (
+ "hash/crc64"
+
+ "github.com/pkg/errors"
+)
+
+// polynomial for crc64 table which is used for checking crc64 checksum
+const polynomial = crc64.ECMA
+
+// common errors for MatchingEngine.
+var (
+ ErrUnbalancedPattern = errors.New("unbalanced pattern")
+ ErrMethodNotImplemented = errors.New("the method is not implemented")
+ ErrUnknownMatchingStrategy = errors.New("unknown matching strategy")
+)
+
+// MatchingEngine describes an interface of matching engine such as regexp or glob.
+type MatchingEngine interface {
+ IsMatching(pattern, matchAgainst string) (bool, error)
+ ReplaceAllString(pattern, input, replacement string) (string, error)
+ Checksum() uint64
+}
diff --git a/rule/repository.go b/rule/repository.go
index 9a63761acb..520b0b63b4 100644
--- a/rule/repository.go
+++ b/rule/repository.go
@@ -22,6 +22,8 @@ package rule
import (
"context"
+
+ "github.com/ory/oathkeeper/driver/configuration"
)
type Repository interface {
@@ -29,4 +31,6 @@ type Repository interface {
Set(context.Context, []Rule) error
Get(context.Context, string) (*Rule, error)
Count(context.Context) (int, error)
+ MatchingStrategy(context.Context) (configuration.MatchingStrategy, error)
+ SetMatchingStrategy(context.Context, configuration.MatchingStrategy) error
}
diff --git a/rule/repository_memory.go b/rule/repository_memory.go
index da4ac1bac0..9ee4b7d46f 100644
--- a/rule/repository_memory.go
+++ b/rule/repository_memory.go
@@ -29,6 +29,7 @@ import (
"github.com/ory/x/viperx"
+ "github.com/ory/oathkeeper/driver/configuration"
"github.com/ory/oathkeeper/helper"
"github.com/ory/oathkeeper/x"
@@ -44,8 +45,24 @@ type repositoryMemoryRegistry interface {
type RepositoryMemory struct {
sync.RWMutex
- rules []Rule
- r repositoryMemoryRegistry
+ rules []Rule
+ matchingStrategy configuration.MatchingStrategy
+ r repositoryMemoryRegistry
+}
+
+// MatchingStrategy returns current MatchingStrategy.
+func (m *RepositoryMemory) MatchingStrategy(_ context.Context) (configuration.MatchingStrategy, error) {
+ m.RLock()
+ defer m.RUnlock()
+ return m.matchingStrategy, nil
+}
+
+// SetMatchingStrategy updates MatchingStrategy.
+func (m *RepositoryMemory) SetMatchingStrategy(_ context.Context, ms configuration.MatchingStrategy) error {
+ m.Lock()
+ defer m.Unlock()
+ m.matchingStrategy = ms
+ return nil
}
func NewRepositoryMemory(r repositoryMemoryRegistry) *RepositoryMemory {
@@ -104,14 +121,16 @@ func (m *RepositoryMemory) Set(ctx context.Context, rules []Rule) error {
return nil
}
-func (m *RepositoryMemory) Match(ctx context.Context, method string, u *url.URL) (*Rule, error) {
+func (m *RepositoryMemory) Match(_ context.Context, method string, u *url.URL) (*Rule, error) {
m.Lock()
defer m.Unlock()
var rules []Rule
for k := range m.rules {
r := &m.rules[k]
- if err := r.IsMatching(method, u); err == nil {
+ if matched, err := r.IsMatching(m.matchingStrategy, method, u); err != nil {
+ return nil, errors.WithStack(err)
+ } else if matched {
rules = append(rules, *r)
}
m.rules[k] = *r
diff --git a/rule/repository_test.go b/rule/repository_test.go
index 6ba7f5b1a9..103485c966 100644
--- a/rule/repository_test.go
+++ b/rule/repository_test.go
@@ -35,6 +35,8 @@ import (
"github.com/stretchr/testify/require"
"github.com/ory/x/sqlcon/dockertest"
+
+ "github.com/ory/oathkeeper/driver/configuration"
)
func TestMain(m *testing.M) {
@@ -118,6 +120,18 @@ func TestRepository(t *testing.T) {
count, err = repo.Count(context.Background())
require.NoError(t, err)
assert.Equal(t, len(rules)-1, count)
+
+ strategy, err := repo.MatchingStrategy(context.Background())
+ require.NoError(t, err)
+ require.Equal(t, configuration.MatchingStrategy(""), strategy)
+
+ err = repo.SetMatchingStrategy(context.Background(), configuration.Glob)
+ require.NoError(t, err)
+
+ strategy, err = repo.MatchingStrategy(context.Background())
+ require.NoError(t, err)
+ require.Equal(t, configuration.Glob, strategy)
+
})
}
diff --git a/rule/rule.go b/rule/rule.go
index 80218c8c0f..5398ea475d 100644
--- a/rule/rule.go
+++ b/rule/rule.go
@@ -23,14 +23,12 @@ package rule
import (
"encoding/json"
"fmt"
- "hash/crc32"
"net/url"
- "regexp"
"strings"
"github.com/pkg/errors"
- "github.com/ory/ladon/compiler"
+ "github.com/ory/oathkeeper/driver/configuration"
)
type Match struct {
@@ -46,12 +44,12 @@ type Match struct {
// request with this field. If a match is found, the rule is considered a partial match.
// If the matchesMethods field is satisfied as well, the rule is considered a full match.
//
- // You can use regular expressions in this field to match more than one url. Regular expressions are encapsulated in
- // brackets < and >. The following example matches all paths of the domain `mydomain.com`: `https://mydomain.com/<.*>`.
+ // You can use regular expressions or glob patterns in this field to match more than one url.
+ // The matching strategy is determined by configuration parameter MatchingStrategy.
+ // Regular expressions and glob patterns are encapsulated in brackets < and >.
+ // 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"`
-
- compiledURL *regexp.Regexp
- compiledURLChecksum uint32
}
type Handler struct {
@@ -122,6 +120,8 @@ type Rule struct {
// Upstream is the location of the server where requests matching this rule should be forwarded to.
Upstream Upstream `json:"upstream"`
+
+ matchingEngine MatchingEngine
}
type Upstream struct {
@@ -138,14 +138,6 @@ type Upstream struct {
var _ json.Unmarshaler = new(Rule)
-func NewRule() *Rule {
- return &Rule{
- Match: &Match{},
- Authenticators: []Handler{},
- Mutators: []Handler{},
- }
-}
-
func (r *Rule) UnmarshalJSON(raw []byte) error {
var rr struct {
ID string `json:"id"`
@@ -157,6 +149,7 @@ func (r *Rule) UnmarshalJSON(raw []byte) error {
Mutators []Handler `json:"mutators"`
Errors []ErrorHandler `json:"errors"`
Upstream Upstream `json:"upstream"`
+ matchingEngine MatchingEngine
}
transformed, err := migrateRuleJSON(raw)
@@ -177,37 +170,27 @@ func (r *Rule) GetID() string {
return r.ID
}
-// IsMatching returns an error if the provided method and URL do not match the rule.
-func (r *Rule) IsMatching(method string, u *url.URL) error {
+// IsMatching checks whether the provided url and method 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) (bool, error) {
if !stringInSlice(method, r.Match.Methods) {
- return errors.Errorf("rule %s does not match URL %s", r.ID, u)
+ return false, nil
}
-
- c, err := r.CompileURL()
- if err != nil {
- return errors.WithStack(err)
- }
-
- if !c.MatchString(fmt.Sprintf("%s://%s%s", u.Scheme, u.Host, u.Path)) {
- return errors.Errorf("rule %s does not match URL %s", r.ID, u)
+ if err := ensureMatchingEngine(r, strategy); err != nil {
+ return false, err
}
-
- return nil
+ matchAgainst := fmt.Sprintf("%s://%s%s", u.Scheme, u.Host, u.Path)
+ return r.matchingEngine.IsMatching(r.Match.URL, matchAgainst)
}
-func (r *Rule) CompileURL() (*regexp.Regexp, error) {
- m := r.Match
- c := crc32.ChecksumIEEE([]byte(m.URL))
- if m.compiledURL == nil || c != m.compiledURLChecksum {
- regex, err := compiler.CompileRegex(m.URL, '<', '>')
- if err != nil {
- return nil, errors.Wrap(err, "Unable to compile URL matcher")
- }
- m.compiledURL = regex
- m.compiledURLChecksum = c
+// ReplaceAllString searches the input string and replaces each match (with the rule's pattern)
+// found with the replacement text.
+func (r *Rule) ReplaceAllString(strategy configuration.MatchingStrategy, input, replacement string) (string, error) {
+ if err := ensureMatchingEngine(r, strategy); err != nil {
+ return "", err
}
- return m.compiledURL, nil
+ return r.matchingEngine.ReplaceAllString(r.Match.URL, input, replacement)
}
func stringInSlice(a string, list []string) bool {
@@ -218,3 +201,19 @@ func stringInSlice(a string, list []string) bool {
}
return false
}
+
+func ensureMatchingEngine(rule *Rule, strategy configuration.MatchingStrategy) error {
+ if rule.matchingEngine != nil {
+ return nil
+ }
+ switch strategy {
+ case configuration.Glob:
+ rule.matchingEngine = new(globMatchingEngine)
+ return nil
+ case "", configuration.Regexp:
+ rule.matchingEngine = new(regexpMatchingEngine)
+ return nil
+ }
+
+ return errors.Wrap(ErrUnknownMatchingStrategy, string(strategy))
+}
diff --git a/rule/rule_test.go b/rule/rule_test.go
index e5144e3d2d..c9abb1b06f 100644
--- a/rule/rule_test.go
+++ b/rule/rule_test.go
@@ -26,6 +26,8 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+
+ "github.com/ory/oathkeeper/driver/configuration"
)
func mustParse(t *testing.T, u string) *url.URL {
@@ -35,14 +37,110 @@ func mustParse(t *testing.T, u string) *url.URL {
}
func TestRule(t *testing.T) {
+ rules := []Rule{
+ {
+ Match: &Match{
+ Methods: []string{"DELETE"},
+ URL: "https://localhost/users/<[0-9]+>",
+ },
+ },
+ {
+ Match: &Match{
+ Methods: []string{"DELETE"},
+ URL: "https://localhost/users/<[[:digit:]]*>",
+ },
+ },
+ {
+ Match: &Match{
+ Methods: []string{"DELETE"},
+ URL: "https://localhost/users/<[0-9]*>",
+ },
+ },
+ }
+
+ var tests = []struct {
+ method string
+ url string
+ expectedMatch bool
+ expectedErr error
+ }{
+ {
+ method: "DELETE",
+ url: "https://localhost/users/1234",
+ expectedMatch: true,
+ expectedErr: nil,
+ },
+ {
+ method: "DELETE",
+ url: "https://localhost/users/1234?key=value&key1=value1",
+ expectedMatch: true,
+ expectedErr: nil,
+ },
+ {
+ method: "DELETE",
+ url: "https://localhost/users/abcd",
+ expectedMatch: false,
+ expectedErr: nil,
+ },
+ }
+ for ind, tcase := range tests {
+ t.Run(string(ind), func(t *testing.T) {
+ testFunc := func(rule Rule, strategy configuration.MatchingStrategy) {
+ matched, err := rule.IsMatching(strategy, tcase.method, mustParse(t, tcase.url))
+ assert.Equal(t, tcase.expectedMatch, matched)
+ assert.Equal(t, tcase.expectedErr, err)
+ }
+ t.Run("rule0", func(t *testing.T) {
+ testFunc(rules[0], configuration.Regexp)
+ })
+ t.Run("rule1", func(t *testing.T) {
+ testFunc(rules[1], configuration.Regexp)
+ })
+ t.Run("rule2", func(t *testing.T) {
+ testFunc(rules[2], configuration.Glob)
+ })
+ })
+ }
+}
+
+func TestRule1(t *testing.T) {
r := &Rule{
Match: &Match{
Methods: []string{"DELETE"},
- URL: "https://localhost/users/<[0-9]+>",
+ URL: "https://localhost/users/<(?!admin).*>",
},
}
- assert.NoError(t, r.IsMatching("DELETE", mustParse(t, "https://localhost/users/1234")))
- assert.NoError(t, r.IsMatching("DELETE", mustParse(t, "https://localhost/users/1234?key=value&key1=value1")))
- assert.Error(t, r.IsMatching("DELETE", mustParse(t, "https://localhost/users/abcd")))
+ var tests = []struct {
+ method string
+ url string
+ expectedMatch bool
+ expectedErr error
+ }{
+ {
+ method: "DELETE",
+ url: "https://localhost/users/manager",
+ expectedMatch: true,
+ expectedErr: nil,
+ },
+ {
+ method: "DELETE",
+ url: "https://localhost/users/1234?key=value&key1=value1",
+ expectedMatch: true,
+ expectedErr: nil,
+ },
+ {
+ method: "DELETE",
+ url: "https://localhost/users/admin",
+ expectedMatch: false,
+ expectedErr: nil,
+ },
+ }
+ for ind, tcase := range tests {
+ t.Run(string(ind), func(t *testing.T) {
+ matched, err := r.IsMatching(configuration.Regexp, tcase.method, mustParse(t, tcase.url))
+ assert.Equal(t, tcase.expectedMatch, matched)
+ assert.Equal(t, tcase.expectedErr, err)
+ })
+ }
}
diff --git a/test/update/config_default.yaml b/test/update/config_default.yaml
new file mode 100644
index 0000000000..a11b1ef842
--- /dev/null
+++ b/test/update/config_default.yaml
@@ -0,0 +1,2 @@
+access_rules:
+ repositories: file://../test/update/rules_glob.yaml
\ No newline at end of file
diff --git a/test/update/config_error.yaml b/test/update/config_error.yaml
new file mode 100644
index 0000000000..a9c0376ca6
--- /dev/null
+++ b/test/update/config_error.yaml
@@ -0,0 +1,3 @@
+access_rules:
+ repositories: file://../test/update/rules_glob.yaml
+ matching_strategy: UNKNOWN
\ No newline at end of file
diff --git a/test/update/config_glob.yaml b/test/update/config_glob.yaml
new file mode 100644
index 0000000000..ec4aba7b3d
--- /dev/null
+++ b/test/update/config_glob.yaml
@@ -0,0 +1,3 @@
+access_rules:
+ repositories: file://../test/update/rules_glob.yaml
+ matching_strategy: glob
\ No newline at end of file
diff --git a/test/update/config_no_repo.yaml b/test/update/config_no_repo.yaml
new file mode 100644
index 0000000000..304048484a
--- /dev/null
+++ b/test/update/config_no_repo.yaml
@@ -0,0 +1,2 @@
+access_rules:
+ repositories:
\ No newline at end of file
diff --git a/test/update/config_regexp.yaml b/test/update/config_regexp.yaml
new file mode 100644
index 0000000000..369ac5c545
--- /dev/null
+++ b/test/update/config_regexp.yaml
@@ -0,0 +1,3 @@
+access_rules:
+ repositories: file://../test/update/rules_glob.yaml
+ matching_strategy: regexp
\ No newline at end of file
diff --git a/test/update/rules_glob.yaml b/test/update/rules_glob.yaml
new file mode 100644
index 0000000000..e898ab305a
--- /dev/null
+++ b/test/update/rules_glob.yaml
@@ -0,0 +1,6 @@
+- id: test-rule-1-glob
+ match:
+ url: myproxy.com/
+ methods:
+ - GET
+ - POST
\ No newline at end of file