From 67face611b9f19ed9b6606931c9b7a82df769154 Mon Sep 17 00:00:00 2001 From: Igor Zibarev Date: Mon, 1 Jul 2019 13:11:52 +0300 Subject: [PATCH] Add support for rules in YAML format (#213) This commit adds support for defining access rules in YAML format, in addition to existing JSON format. --- docs/config.yaml | 4 +-- go.mod | 1 + go.sum | 1 + rule/fetcher_default.go | 27 +++++++++++++---- rule/fetcher_default_test.go | 56 ++++++++++++++++++++++++++++++++++++ rule/testdata/rules.json | 31 ++++++++++++++++++++ rule/testdata/rules.yaml | 17 +++++++++++ 7 files changed, 129 insertions(+), 8 deletions(-) create mode 100644 rule/testdata/rules.json create mode 100644 rule/testdata/rules.yaml diff --git a/docs/config.yaml b/docs/config.yaml index 06f646777..325bff978 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -233,13 +233,13 @@ serve: # Configures Access Rules access_rules: # Locations (list of URLs) where access rules should be fetched from on boot. - # It is expected that the documents at those locations return a JSON Array containing ORY Oathkeeper Access Rules. + # It is expected that the documents at those locations return a JSON or YAML Array containing ORY Oathkeeper Access Rules. repositories: # If the URL Scheme is `file://`, the access rules (an array of access rules is expected) will be # fetched from the local file system. - file://path/to/rules.json # If the URL Scheme is `inline://`, the access rules (an array of access rules is expected) - # are expected to be a base64 encoded (with padding!) JSON string (base64_encode(`[{"id":"foo-rule","authenticators":[....]}]`)): + # are expected to be a base64 encoded (with padding!) JSON/YAML string (base64_encode(`[{"id":"foo-rule","authenticators":[....]}]`)): - inline://W3siaWQiOiJmb28tcnVsZSIsImF1dGhlbnRpY2F0b3JzIjpbXX1d # 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. diff --git a/go.mod b/go.mod index 5d016e46f..541fa82af 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/bxcodec/faker v2.0.1+incompatible github.com/codegangsta/negroni v1.0.0 // indirect github.com/dgrijalva/jwt-go v3.2.0+incompatible + github.com/ghodss/yaml v1.0.0 github.com/go-errors/errors v1.0.1 github.com/go-openapi/analysis v0.19.0 // indirect github.com/go-openapi/errors v0.19.0 diff --git a/go.sum b/go.sum index 4e4399f8c..7445f4a9f 100644 --- a/go.sum +++ b/go.sum @@ -79,6 +79,7 @@ github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= diff --git a/rule/fetcher_default.go b/rule/fetcher_default.go index 9c01fb5ce..a6044c057 100644 --- a/rule/fetcher_default.go +++ b/rule/fetcher_default.go @@ -5,6 +5,7 @@ import ( "encoding/base64" "encoding/json" "io" + "io/ioutil" "net/http" "net/url" "os" @@ -12,11 +13,11 @@ import ( "path/filepath" "strings" - "github.com/ory/x/httpx" - "github.com/ory/oathkeeper/driver/configuration" "github.com/ory/oathkeeper/x" + "github.com/ory/x/httpx" + "github.com/ghodss/yaml" "github.com/pkg/errors" ) @@ -62,7 +63,7 @@ func (f *FetcherDefault) fetch(source url.URL) ([]Rule, error) { return f.fetchRemote(source.String()) case "file": p := strings.Replace(source.String(), "file://", "", 1) - if path.Ext(p) == ".json" { + if path.Ext(p) == ".json" || path.Ext(p) == ".yaml" || path.Ext(p) == ".yml" { return f.fetchFile(p) } return f.fetchDir(p) @@ -124,11 +125,25 @@ func (f *FetcherDefault) fetchFile(source string) ([]Rule, error) { } func (f *FetcherDefault) decode(r io.Reader) ([]Rule, error) { + b, err := ioutil.ReadAll(r) + if err != nil { + return nil, errors.WithStack(err) + } + var ks []Rule - d := json.NewDecoder(r) - d.DisallowUnknownFields() - if err := d.Decode(&ks); err != nil { + + if json.Valid(b) { + d := json.NewDecoder(bytes.NewReader(b)) + d.DisallowUnknownFields() + if err := d.Decode(&ks); err != nil { + return nil, errors.WithStack(err) + } + return ks, nil + } + + if err := yaml.Unmarshal(b, &ks); err != nil { return nil, errors.WithStack(err) } + return ks, nil } diff --git a/rule/fetcher_default_test.go b/rule/fetcher_default_test.go index 66eba6f2f..a435546a4 100644 --- a/rule/fetcher_default_test.go +++ b/rule/fetcher_default_test.go @@ -12,6 +12,7 @@ import ( "github.com/ory/oathkeeper/driver/configuration" "github.com/ory/oathkeeper/internal" + "github.com/ory/oathkeeper/rule" ) 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"},"mutator":{"handler":"noop"}}]` @@ -67,3 +68,58 @@ func TestFetcher(t *testing.T) { }) } } + +func TestFetcherDefaultFetchFormats(t *testing.T) { + expected := []rule.Rule{ + { + ID: "test-rule-1", + Match: rule.RuleMatch{ + Methods: []string{"GET", "POST"}, + URL: "myproxy.com/api", + }, + Authenticators: []rule.RuleHandler{ + { + Handler: "noop", + }, + { + Handler: "anonymous", + }, + }, + Authorizer: rule.RuleHandler{ + Handler: "allow", + }, + Mutator: rule.RuleHandler{ + Handler: "noop", + }, + Upstream: rule.Upstream{ + PreserveHost: true, + StripPath: "/api", + URL: "mybackend.com/api", + }, + }, + } + + testCases := map[string]struct { + fpath string + }{ + "json file": { + fpath: "testdata/rules.json", + }, + "yaml file": { + fpath: "testdata/rules.yaml", + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + conf := internal.NewConfigurationWithDefaults() + viper.Set(configuration.ViperKeyAccessRuleRepositories, []string{"file://" + tc.fpath}) + + r := internal.NewRegistry(conf) + rules, err := r.RuleFetcher().Fetch() + require.NoError(t, err) + + assert.Equal(t, expected, rules) + }) + } +} diff --git a/rule/testdata/rules.json b/rule/testdata/rules.json new file mode 100644 index 000000000..6859fe6db --- /dev/null +++ b/rule/testdata/rules.json @@ -0,0 +1,31 @@ +[ + { + "id": "test-rule-1", + "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" + }, + "mutator": { + "handler": "noop" + } + } +] diff --git a/rule/testdata/rules.yaml b/rule/testdata/rules.yaml new file mode 100644 index 000000000..368498741 --- /dev/null +++ b/rule/testdata/rules.yaml @@ -0,0 +1,17 @@ +- id: test-rule-1 + 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 + mutator: + handler: noop