diff --git a/Gopkg.lock b/Gopkg.lock index 3c3e73496f..0a7579105d 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -186,6 +186,12 @@ revision = "c74e4c2fa5c689d583d01521a7e6b2ecc1fddcde" version = "v0.9.14" +[[projects]] + name = "github.com/ory/ladon" + packages = ["compiler"] + revision = "4223d97b7a16808bc1213cc641d529e764e67eea" + version = "v0.8.3" + [[projects]] name = "github.com/pborman/uuid" packages = ["."] @@ -339,6 +345,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "c857c9359eea153d02809743535bf58228f15d216ead4ed5babd07a77ed7297d" + inputs-digest = "11498cdf99d7b585598cf1667dc5c24db3c7e0d26a315cba59902a4876f2b5e5" solver-name = "gps-cdcl" solver-version = 1 diff --git a/evaluator/evaluator_test.go b/evaluator/evaluator_test.go index 389fc4de73..819aa92106 100644 --- a/evaluator/evaluator_test.go +++ b/evaluator/evaluator_test.go @@ -10,6 +10,7 @@ import ( "github.com/golang/mock/gomock" "github.com/ory/hydra/sdk/go/hydra" "github.com/ory/hydra/sdk/go/hydra/swagger" + "github.com/ory/ladon/compiler" "github.com/ory/oathkeeper/rule" "github.com/pkg/errors" "github.com/stretchr/testify/assert" @@ -17,7 +18,7 @@ import ( ) func mustCompileRegex(t *testing.T, pattern string) *regexp.Regexp { - exp, err := regexp.Compile(pattern) + exp, err := compiler.CompileRegex(pattern, '<', '>') require.NoError(t, err) return exp } @@ -30,15 +31,29 @@ func mustGenerateURL(t *testing.T, u string) *url.URL { func TestEvaluator(t *testing.T) { we := NewWardenEvaluator(nil, nil, nil) - publicRule := rule.Rule{MatchesMethods: []string{"GET"}, MatchesPath: mustCompileRegex(t, "/users/[0-9]+"), AllowAnonymous: true} - bypassACPRule := rule.Rule{MatchesMethods: []string{"GET"}, MatchesPath: mustCompileRegex(t, "/users/[0-9]+"), BypassAccessControlPolicies: true} - privateRule := rule.Rule{ + publicRule := rule.Rule{MatchesMethods: []string{"GET"}, MatchesPath: mustCompileRegex(t, "/users/<[0-9]+>"), AllowAnonymous: true} + bypassACPRule := rule.Rule{MatchesMethods: []string{"GET"}, MatchesPath: mustCompileRegex(t, "/users/<[0-9]+>"), BypassAccessControlPolicies: true} + privateRuleWithSubstitution := rule.Rule{ MatchesMethods: []string{"POST"}, - MatchesPath: mustCompileRegex(t, "/users/([0-9]+)"), + MatchesPath: mustCompileRegex(t, "/users/<[0-9]+>"), RequiredResource: "users:$1", RequiredAction: "get:$1", RequiredScopes: []string{"users.create"}, } + privateRuleWithoutSubstitution := rule.Rule{ + MatchesMethods: []string{"POST"}, + MatchesPath: mustCompileRegex(t, "/users<$|/([0-9]+)>"), + RequiredResource: "users", + RequiredAction: "get", + RequiredScopes: []string{"users.create"}, + } + privateRuleWithPartialSubstitution := rule.Rule{ + MatchesMethods: []string{"POST"}, + MatchesPath: mustCompileRegex(t, "/users<$|/([0-9]+)>"), + RequiredResource: "users:$2", + RequiredAction: "get", + RequiredScopes: []string{"users.create"}, + } for k, tc := range []struct { d string @@ -215,7 +230,7 @@ func TestEvaluator(t *testing.T) { }, { d: "request is denied because token is missing and endpoint is not public", - rules: []rule.Rule{privateRule}, + rules: []rule.Rule{privateRuleWithSubstitution}, r: &http.Request{Method: "POST", Header: http.Header{"Authorization": []string{"bEaReR"}}, URL: mustGenerateURL(t, "https://localhost/users/1234")}, e: func(t *testing.T, s *Session, err error) { require.Error(t, err) @@ -226,7 +241,7 @@ func TestEvaluator(t *testing.T) { }, { d: "request is denied because warden request fails with a network error and endpoint is not public", - rules: []rule.Rule{privateRule}, + rules: []rule.Rule{privateRuleWithSubstitution}, r: &http.Request{Method: "POST", Header: http.Header{"Authorization": []string{"bEaReR token"}}, URL: mustGenerateURL(t, "https://localhost/users/1234")}, e: func(t *testing.T, s *Session, err error) { require.Error(t, err) @@ -239,7 +254,7 @@ func TestEvaluator(t *testing.T) { }, { d: "request is denied because warden request fails with a 400 status code and endpoint is not public", - rules: []rule.Rule{privateRule}, + rules: []rule.Rule{privateRuleWithSubstitution}, r: &http.Request{Method: "POST", Header: http.Header{"Authorization": []string{"bEaReR token"}}, URL: mustGenerateURL(t, "https://localhost/users/1234")}, e: func(t *testing.T, s *Session, err error) { require.Error(t, err) @@ -252,7 +267,7 @@ func TestEvaluator(t *testing.T) { }, { d: "request is denied because warden request fails with allowed=false", - rules: []rule.Rule{privateRule}, + rules: []rule.Rule{privateRuleWithSubstitution}, r: &http.Request{Method: "POST", Header: http.Header{"Authorization": []string{"bEaReR token"}}, URL: mustGenerateURL(t, "https://localhost/users/1234")}, e: func(t *testing.T, s *Session, err error) { require.Error(t, err) @@ -264,8 +279,8 @@ func TestEvaluator(t *testing.T) { }, }, { - d: "request is allowed because token is valid and allowed", - rules: []rule.Rule{privateRule}, + d: "request is allowed because token is valid and allowed (rule with substitution)", + rules: []rule.Rule{privateRuleWithSubstitution}, r: &http.Request{RemoteAddr: "127.0.0.1:1234", Method: "POST", Header: http.Header{"Authorization": []string{"bEaReR token"}}, URL: mustGenerateURL(t, "https://localhost/users/1234")}, e: func(t *testing.T, s *Session, err error) { require.NoError(t, err) @@ -282,6 +297,63 @@ func TestEvaluator(t *testing.T) { return s }, }, + { + d: "request is allowed because token is valid and allowed (rule with partial substitution)", + rules: []rule.Rule{privateRuleWithPartialSubstitution}, + r: &http.Request{RemoteAddr: "127.0.0.1:1234", Method: "POST", Header: http.Header{"Authorization": []string{"bEaReR token"}}, URL: mustGenerateURL(t, "https://localhost/users/1234")}, + e: func(t *testing.T, s *Session, err error) { + require.NoError(t, err) + }, + mock: func(c *gomock.Controller) hydra.SDK { + s := NewMockSDK(c) + s.EXPECT().DoesWardenAllowTokenAccessRequest(gomock.Eq(swagger.WardenTokenAccessRequest{ + Token: "token", + Resource: "users:1234", + Action: "get", + Scopes: []string{"users.create"}, + Context: map[string]interface{}{"remoteIpAddress": "127.0.0.1"}, + })).Return(&swagger.WardenTokenAccessRequestResponse{Allowed: true}, &swagger.APIResponse{Response: &http.Response{StatusCode: http.StatusOK}}, nil) + return s + }, + }, + { + d: "request is allowed because token is valid and allowed (rule with partial substitution and path parameter)", + rules: []rule.Rule{privateRuleWithoutSubstitution}, + r: &http.Request{RemoteAddr: "127.0.0.1:1234", Method: "POST", Header: http.Header{"Authorization": []string{"bEaReR token"}}, URL: mustGenerateURL(t, "https://localhost/users/1234")}, + e: func(t *testing.T, s *Session, err error) { + require.NoError(t, err) + }, + mock: func(c *gomock.Controller) hydra.SDK { + s := NewMockSDK(c) + s.EXPECT().DoesWardenAllowTokenAccessRequest(gomock.Eq(swagger.WardenTokenAccessRequest{ + Token: "token", + Resource: "users", + Action: "get", + Scopes: []string{"users.create"}, + Context: map[string]interface{}{"remoteIpAddress": "127.0.0.1"}, + })).Return(&swagger.WardenTokenAccessRequestResponse{Allowed: true}, &swagger.APIResponse{Response: &http.Response{StatusCode: http.StatusOK}}, nil) + return s + }, + }, + { + d: "request is allowed because token is valid and allowed (rule without substitution and path parameter)", + rules: []rule.Rule{privateRuleWithoutSubstitution}, + r: &http.Request{RemoteAddr: "127.0.0.1:1234", Method: "POST", Header: http.Header{"Authorization": []string{"bEaReR token"}}, URL: mustGenerateURL(t, "https://localhost/users")}, + e: func(t *testing.T, s *Session, err error) { + require.NoError(t, err) + }, + mock: func(c *gomock.Controller) hydra.SDK { + s := NewMockSDK(c) + s.EXPECT().DoesWardenAllowTokenAccessRequest(gomock.Eq(swagger.WardenTokenAccessRequest{ + Token: "token", + Resource: "users", + Action: "get", + Scopes: []string{"users.create"}, + Context: map[string]interface{}{"remoteIpAddress": "127.0.0.1"}, + })).Return(&swagger.WardenTokenAccessRequestResponse{Allowed: true}, &swagger.APIResponse{Response: &http.Response{StatusCode: http.StatusOK}}, nil) + return s + }, + }, } { t.Run(fmt.Sprintf("case=%d/description=%s", k, tc.d), func(t *testing.T) { ctrl := gomock.NewController(t) @@ -295,3 +367,14 @@ func TestEvaluator(t *testing.T) { }) } } + +func TestSubstitution(t *testing.T) { + reg, err := compiler.CompileRegex("/rules<$|/([^/]+)>", '<', '>') + fmt.Println(reg.String()) + fmt.Printf("Found: %s\n", reg.FindAllString("/rules", -1)) + fmt.Printf("Found: %s\n", reg.FindAllString("/rules/", -1)) + fmt.Printf("Found: %s\n", reg.FindAllString("/rules/2423", -1)) + fmt.Printf("Found: %s\n", reg.ReplaceAllString("/rules/2423", "read:$2")) + require.NoError(t, err) + +} diff --git a/rule/handler.go b/rule/handler.go index ff1410036b..85d51ebcc8 100644 --- a/rule/handler.go +++ b/rule/handler.go @@ -3,10 +3,10 @@ package rule import ( "encoding/json" "net/http" - "regexp" "github.com/julienschmidt/httprouter" "github.com/ory/herodot" + "github.com/ory/ladon/compiler" "github.com/ory/oathkeeper/helper" "github.com/pborman/uuid" "github.com/pkg/errors" @@ -198,7 +198,7 @@ func decodeRule(w http.ResponseWriter, r *http.Request) (*Rule, error) { } func toRule(rule *jsonRule) (*Rule, error) { - exp, err := regexp.Compile(rule.MatchesPath) + exp, err := compiler.CompileRegex(rule.MatchesPath, '<', '>') if err != nil { return nil, err } diff --git a/rule/manager_sql.go b/rule/manager_sql.go index 9570c554b4..abbdede008 100644 --- a/rule/manager_sql.go +++ b/rule/manager_sql.go @@ -3,11 +3,11 @@ package rule import ( "database/sql" "fmt" - "regexp" "strings" "github.com/jmoiron/sqlx" _ "github.com/lib/pq" + "github.com/ory/ladon/compiler" "github.com/ory/oathkeeper/helper" "github.com/pkg/errors" "github.com/rubenv/sql-migrate" @@ -27,7 +27,7 @@ type sqlRule struct { } func (r *sqlRule) toRule() (*Rule, error) { - exp, err := regexp.Compile(r.MatchesPath) + exp, err := compiler.CompileRegex(r.MatchesPath, '<', '>') if err != nil { return nil, errors.WithStack(err) } diff --git a/rule/manager_test.go b/rule/manager_test.go index 25a9d95a73..d5e77dc16e 100644 --- a/rule/manager_test.go +++ b/rule/manager_test.go @@ -9,6 +9,7 @@ import ( "github.com/jmoiron/sqlx" "github.com/ory/dockertest" + "github.com/ory/ladon/compiler" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -26,7 +27,7 @@ func kjillAll() { } func mustCompileRegex(t *testing.T, pattern string) *regexp.Regexp { - exp, err := regexp.Compile(pattern) + exp, err := compiler.CompileRegex(pattern, '<', '>') require.NoError(t, err) return exp } diff --git a/rule/matcher_test.go b/rule/matcher_test.go index df4c1e1165..5d0a1f548d 100644 --- a/rule/matcher_test.go +++ b/rule/matcher_test.go @@ -3,9 +3,10 @@ package rule import ( "fmt" "net/url" - "regexp" "strconv" "testing" + + "github.com/ory/ladon/compiler" ) var methods = []string{"POST", "PUT", "GET", "DELETE", "PATCH", "OPTIONS", "HEAD"} @@ -13,12 +14,12 @@ var methods = []string{"POST", "PUT", "GET", "DELETE", "PATCH", "OPTIONS", "HEAD func generateDummyRules(amount int) []Rule { rules := make([]Rule, amount) scopes := []string{"foo", "bar", "baz", "faz"} - expressions := []string{"/users/", "/users", "/blogs/", "/use(r)s/"} + expressions := []string{"/users/", "/users", "/blogs/", "/use<(r)>s/"} resources := []string{"users", "users:$1"} actions := []string{"get", "get:$1"} for i := 0; i < amount; i++ { - exp, _ := regexp.Compile(expressions[(i%(len(expressions)))] + "([0-" + strconv.Itoa(i) + "]+)") + exp, _ := compiler.CompileRegex(expressions[(i%(len(expressions)))]+"([0-"+strconv.Itoa(i)+"]+)", '<', '>') rules[i] = Rule{ ID: strconv.Itoa(i), MatchesMethods: methods[:i%(len(methods))],