diff --git a/.gitignore b/.gitignore index d016b59..b33dcef 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ *.iml vendor/ sqlite-test.db +tests/ diff --git a/.travis.yml b/.travis.yml index caa9608..8f36f32 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,7 @@ env: language: go -go_import_path: github.com/ory-am/ladon +go_import_path: github.com/ory/ladon go: - tip diff --git a/HISTORY.md b/HISTORY.md new file mode 100644 index 0000000..b67f81c --- /dev/null +++ b/HISTORY.md @@ -0,0 +1,69 @@ +# History of breaking changes + + + + +- [0.6.0](#060) + - [New location](#new-location) + - [Deprecating Redis and RethinkDB](#deprecating-redis-and-rethinkdb) + - [New packages](#new-packages) + - [IMPORTANT: SQL Changes](#important-sql-changes) + - [Manager API Changes](#manager-api-changes) + + + + +## 0.6.0 + +Version 0.6.0 includes some larger BC breaks. This version focuses on various +performance boosts for both in-memory and SQL adapters, removes some technical debt +and restructures the repository. + +### New location + +The location of this library changed from `github.com/ory-am/ladon` to `github.com/ory/ladon`. + +### Deprecating Redis and RethinkDB + +Redis and RethinkDB are no longer maintained by ORY and were moved to +[ory/ladon-community](https://github.com/ory/ladon-community). The adapters had various +bugs and performance issues which is why they were removed from the official repository. + +### New packages + +The SQLManager and MemoryManager moved to their own packages in `ladon/manager/sql` and `ladon/manager/memory`. +This change was made to avoid pulling dependencies that are not required by the user. + +### IMPORTANT: SQL Changes + +The SQLManager was rewritten completely. Now, the database is 3NF (normalized) and includes +various improvements over the previous, naive adapter. The greatest challenge is matching +regular expressions within SQL databases, which causes significant overhead. + +While there is an auto-migration for the schema, the data **is not automatically transferred to +the new schema**. + +However, we provided a migration helper. For usage, check out +[xxx_manager_sql_migrator_test.go](xxx_manager_sql_migrator_test.go) or this short example: + +```go +var db = getSqlDatabaseFromSomewhere() +s := NewSQLManager(db, nil) + +if err := s.CreateSchemas(); err != nil { + log.Fatalf("Could not create mysql schema: %v", err) +} + +migrator := &SQLManagerMigrateFromMajor0Minor6ToMajor0Minor7{ + DB:db, + SQLManager:s, +} + +err := migrator.Migrate() +``` + +Please run this migrator **only once and make back ups before you run it**. + +### Manager API Changes + +`Manager.FindPoliciesForSubject` is now `Manager.FindRequestCandidates` diff --git a/README.md b/README.md index ec894e6..89b4892 100644 --- a/README.md +++ b/README.md @@ -7,18 +7,18 @@ [![Become a patron!](https://img.shields.io/badge/support%20us-on%20patreon-green.svg)](https://patreon.com/user?u=4298803) [![Build Status](https://travis-ci.org/ory/ladon.svg?branch=master)](https://travis-ci.org/ory/ladon) -[![Coverage Status](https://coveralls.io/repos/ory-am/ladon/badge.svg?branch=master&service=github)](https://coveralls.io/github/ory-am/ladon?branch=master) -[![Go Report Card](https://goreportcard.com/badge/github.com/ory-am/ladon)](https://goreportcard.com/report/github.com/ory-am/ladon) +[![Coverage Status](https://coveralls.io/repos/ory/ladon/badge.svg?branch=master&service=github)](https://coveralls.io/github/ory/ladon?branch=master) +[![Go Report Card](https://goreportcard.com/badge/github.com/ory/ladon)](https://goreportcard.com/report/github.com/ory/ladon) [Ladon](https://en.wikipedia.org/wiki/Ladon_%28mythology%29) is the serpent dragon protecting your resources. Ladon is a library written in [Go](https://golang.org) for access control policies, similar to [Role Based Access Control](https://en.wikipedia.org/wiki/Role-based_access_control) -or [Access Control Lists](https://en.wikipedia.org/wiki/Access_control_list). +or [Access Control Lists](https://en.wikipedia.org/wiki/Access_control_list). In contrast to [ACL](https://en.wikipedia.org/wiki/Access_control_list) and [RBAC](https://en.wikipedia.org/wiki/Role-based_access_control) you get fine-grained access control with the ability to answer questions in complex environments such as multi-tenant or distributed applications and large organizations. Ladon is inspired by [AWS IAM Policies](http://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html). -Ladon ships with storage adapters for SQL (officially supported: MySQL, PostgreSQL), Redis and RethinkDB (community supported). +Ladon ships with storage adapters for SQL (officially supported: MySQL, PostgreSQL) and in-memory. --- @@ -59,7 +59,7 @@ Please refer to [ory-am/dockertest](https://github.com/ory-am/dockertest) for mo ## Installation ``` -go get github.com/ory-am/ladon +go get github.com/ory/ladon ``` We recommend to use [Glide](https://github.com/Masterminds/glide) for dependency management. Ladon uses [semantic @@ -184,7 +184,7 @@ are abstracted as the `ladon.Policy` interface, and Ladon comes with a standard which is `ladon.DefaultPolicy`. Creating such a policy could look like: ```go -import "github.com/ory-am/ladon" +import "github.com/ory/ladon" var pol = &ladon.DefaultPolicy{ // A required unique identifier. Used primarily for database retrieval. @@ -449,7 +449,7 @@ var err = warden.IsAllowed(&ladon.Request{ You can add custom conditions by appending it to `ladon.ConditionFactories`: ```go -import "github.com/ory-am/ladon" +import "github.com/ory/ladon" func main() { // ... @@ -465,20 +465,23 @@ func main() { #### Persistence Obviously, creating such a policy is not enough. You want to persist it too. Ladon ships an interface `ladon.Manager` for -this purpose with default implementations for In-Memory, RethinkDB, SQL (PostgreSQL, MySQL) and Redis. Let's take a look how to -instantiate those. +this purpose with default implementations for In-Memory and SQL (PostgreSQL, MySQL). There are also adapters available +written by the community [for Redis and RethinkDB](https://github.com/ory/ladon-community) -**In-Memory** +Let's take a look how to instantiate those: + +**In-Memory** (officially supported) ```go import ( - "github.com/ory-am/ladon" + "github.com/ory/ladon" + manager "github.com/ory/ladon/manager/memory" ) func main() { warden := &ladon.Ladon{ - Manager: ladon.NewMemoryManager(), + Manager: manager.NewMemoryManager(), } err := warden.Manager.Create(pol) @@ -486,10 +489,11 @@ func main() { } ``` -**SQL** +**SQL** (officially supported) ```go -import "github.com/ory-am/ladon" +import "github.com/ory/ladon" +import manager "github.com/ory/ladon/manager/sql" import "database/sql" import _ "github.com/go-sql-driver/mysql" @@ -497,45 +501,20 @@ func main() { db, err = sql.Open("mysql", "user:pass@tcp(127.0.0.1:3306)"") // Or, if using postgres: // import _ "github.com/lib/pq" - // + // // db, err = sql.Open("postgres", "postgres://foo:bar@localhost/ladon") if err != nil { log.Fatalf("Could not connect to database: %s", err) } warden := ladon.Ladon{ - Manager: ladon.NewSQLManager(db, nil), + Manager: manager.NewSQLManager(db, nil), } // ... } ``` -**Redis** - -```go -import ( - "github.com/ory-am/ladon" - "gopkg.in/redis.v5" -) - -func main () { - db = redis.NewClient(&redis.Options{ - Addr: "localhost:6379", - }) - - if err := db.Ping().Err(); err != nil { - log.Fatalf("Could not connect to database: %s". err) - } - - warden := ladon.Ladon{ - Manager: ladon.NewRedisManager(db, "redis_key_prefix:") - } - - // ... -} -``` - ### Access Control (Warden) Now that we have defined our policies, we can use the warden to check if a request is valid. @@ -543,7 +522,7 @@ Now that we have defined our policies, we can use the warden to check if a reque will return `nil` if the access request can be granted and an error otherwise. ```go -import "github.com/ory-am/ladon" +import "github.com/ory/ladon" func main() { // ... @@ -563,6 +542,34 @@ func main() { } ``` +## Limitations + +Ladon's limitations are listed here. + +### Regular expressions + +Matching regular expressions has a complexity of `O(n)` and databases such as MySQL or Postgres can not +leverage indexes when parsing regular expressions. Thus, there is considerable overhead when using regular +expressions. + +We have implemented various strategies for reducing policy matching time: + +1. An LRU cache is used for caching frequently compiled regular expressions. This reduces cpu complexity +significantly for memory manager implementations. +2. The SQL schema is 3NF normalized. +3. Policies, subjects and actions are stored uniquely, reducing the total number of rows. +4. Only one query per look up is executed. +5. If no regular expression is used, a simple equal match is done in SQL back-ends. + +You will get the best performance with the in-memory manager. The SQL adapters perform about +1000:1 compared to the in-memory solution. Please note that these +tests where in laboratory environments with Docker, without an SSD, and single-threaded. You might get better +results on your system. We are thinking about introducing It would be possible a simple cache strategy such as +LRU with a maximum age to further reduce runtime complexity. + +We are also considering to offer different matching strategies (e.g. wildcard match) in the future, which will perform better +with SQL databases. If you have ideas or suggestions, leave us an issue. + ## Examples Check out [ladon_test.go](ladon_test.go) which includes a couple of policies and tests cases. You can run the code with `go test -run=TestLadon -v .` @@ -578,5 +585,5 @@ Ladon does not use reflection for matching conditions to their appropriate struc **Create mocks** ```sh -mockgen -package ladon_test -destination manager_mock_test.go github.com/ory-am/ladon Manager +mockgen -package ladon_test -destination manager_mock_test.go github.com/ory/ladon Manager ``` diff --git a/benchmark_warden_test.go b/benchmark_warden_test.go new file mode 100644 index 0000000..66e8a67 --- /dev/null +++ b/benchmark_warden_test.go @@ -0,0 +1,91 @@ +package ladon_test + +import ( + "fmt" + "strconv" + "testing" + + "github.com/ory/ladon" + "github.com/ory/ladon/manager/memory" + "github.com/pborman/uuid" + "github.com/pkg/errors" +) + +func benchmarkLadon(i int, b *testing.B, warden *ladon.Ladon) { + //var concurrency = 30 + //var sem = make(chan bool, concurrency) + // + //for _, pol := range generatePolicies(i) { + // sem <- true + // go func(pol ladon.Policy) { + // defer func() { <-sem }() + // if err := warden.Manager.Create(pol); err != nil { + // b.Logf("Got error from warden.Manager.Create: %s", err) + // } + // }(pol) + //} + // + //for i := 0; i < cap(sem); i++ { + // sem <- true + //} + + for _, pol := range generatePolicies(i) { + if err := warden.Manager.Create(pol); err != nil { + b.Logf("Got error from warden.Manager.Create: %s", err) + } + } + + b.ResetTimer() + var err error + for n := 0; n < b.N; n++ { + if err = warden.IsAllowed(&ladon.Request{ + Subject: "5", + Action: "bar", + Resource: "baz", + }); errors.Cause(err) == ladon.ErrRequestDenied || errors.Cause(err) == ladon.ErrRequestForcefullyDenied || err == nil { + } else { + b.Logf("Got error from warden: %s", err) + } + } +} + +func BenchmarkLadon(b *testing.B) { + for _, num := range []int{10, 100, 1000, 10000, 100000, 1000000} { + b.Run(fmt.Sprintf("store=memory/policies=%d", num), func(b *testing.B) { + matcher := ladon.NewRegexpMatcher(4096) + benchmarkLadon(num, b, &ladon.Ladon{ + Manager: memory.NewMemoryManager(), + Matcher: matcher, + }) + }) + + b.Run(fmt.Sprintf("store=mysql/policies=%d", num), func(b *testing.B) { + benchmarkLadon(num, b, &ladon.Ladon{ + Manager: managers["mysql"], + Matcher: ladon.NewRegexpMatcher(4096), + }) + }) + + b.Run(fmt.Sprintf("store=postgres/policies=%d", num), func(b *testing.B) { + benchmarkLadon(num, b, &ladon.Ladon{ + Manager: managers["postgres"], + Matcher: ladon.NewRegexpMatcher(4096), + }) + }) + } +} + +func generatePolicies(n int) map[string]ladon.Policy { + policies := map[string]ladon.Policy{} + for i := 0; i <= n; i++ { + id := uuid.New() + policies[id] = &ladon.DefaultPolicy{ + ID: id, + Subjects: []string{"foobar", "some-resource" + fmt.Sprintf("%d", i%100), strconv.Itoa(i)}, + Actions: []string{"foobar", "foobar", "foobar", "foobar", "foobar"}, + Resources: []string{"foobar", id}, + Effect: ladon.AllowAccess, + } + } + return policies +} diff --git a/condition_string_pairs_equal_test.go b/condition_string_pairs_equal_test.go index 1be3924..ffe118a 100644 --- a/condition_string_pairs_equal_test.go +++ b/condition_string_pairs_equal_test.go @@ -9,17 +9,17 @@ import ( func TestStringPairsEqualMatch(t *testing.T) { for _, c := range []struct { pairs interface{} - pass bool + pass bool }{ {pairs: "junk", pass: false}, {pairs: []interface{}{[]interface{}{}}, pass: false}, {pairs: []interface{}{[]interface{}{"1"}}, pass: false}, {pairs: []interface{}{[]interface{}{"1", "1", "2"}}, pass: false}, {pairs: []interface{}{[]interface{}{"1", "2"}}, pass: false}, - {pairs: []interface{}{[]interface{}{"1", "1"},[]interface{}{"2", "3"}}, pass: false}, + {pairs: []interface{}{[]interface{}{"1", "1"}, []interface{}{"2", "3"}}, pass: false}, {pairs: []interface{}{}, pass: true}, {pairs: []interface{}{[]interface{}{"1", "1"}}, pass: true}, - {pairs: []interface{}{[]interface{}{"1", "1"},[]interface{}{"2", "2"}}, pass: true}, + {pairs: []interface{}{[]interface{}{"1", "1"}, []interface{}{"2", "2"}}, pass: true}, } { condition := &StringPairsEqualCondition{} diff --git a/errors.go b/errors.go index 99e9689..d0cc02e 100644 --- a/errors.go +++ b/errors.go @@ -1,13 +1,70 @@ package ladon import ( + "net/http" + "github.com/pkg/errors" ) var ( // ErrRequestDenied is returned when an access request can not be satisfied by any policy. - ErrRequestDenied = errors.New("Request was denied by default") + ErrRequestDenied = errors.WithStack(&errorWithContext{ + error: errors.New("Request was denied by default"), + code: http.StatusForbidden, + status: http.StatusText(http.StatusForbidden), + reason: "The request was denied because no matching policy was found.", + }) // ErrRequestForcefullyDenied is returned when an access request is explicitly denied by a policy. - ErrRequestForcefullyDenied = errors.New("Request was forcefully denied") + ErrRequestForcefullyDenied = errors.WithStack(&errorWithContext{ + error: errors.New("Request was forcefully denied"), + code: http.StatusForbidden, + status: http.StatusText(http.StatusForbidden), + reason: "The request was denied because a policy denied request.", + }) ) + +func NewErrResourceNotFound(err error) error { + if err == nil { + err = errors.New("not found") + } + + return errors.WithStack(&errorWithContext{ + error: err, + code: http.StatusNotFound, + status: http.StatusText(http.StatusNotFound), + reason: "The requested resource could not be found.", + }) +} + +type errorWithContext struct { + code int + reason string + status string + error +} + +// StatusCode returns the status code of this error. +func (e *errorWithContext) StatusCode() int { + return e.code +} + +// RequestID returns the ID of the request that caused the error, if applicable. +func (e *errorWithContext) RequestID() string { + return "" +} + +// Reason returns the reason for the error, if applicable. +func (e *errorWithContext) Reason() string { + return e.reason +} + +// ID returns the error id, if applicable. +func (e *errorWithContext) Status() string { + return e.status +} + +// Details returns details on the error, if applicable. +func (e *errorWithContext) Details() []map[string]interface{} { + return []map[string]interface{}{} +} diff --git a/errors_test.go b/errors_test.go new file mode 100644 index 0000000..d8eefda --- /dev/null +++ b/errors_test.go @@ -0,0 +1,12 @@ +package ladon + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewErrResourceNotFound(t *testing.T) { + assert.EqualError(t, NewErrResourceNotFound(errors.New("not found")), "not found") +} diff --git a/glide.lock b/glide.lock index a857021..da9ce1a 100644 --- a/glide.lock +++ b/glide.lock @@ -1,5 +1,5 @@ -hash: 2899a52ce0b065044f32bab24fffbc031fe490b96de36527b5689c32261204bd -updated: 2017-04-14T11:24:58.2410603+02:00 +hash: 7c325176797486987b7692b14fd0bbfd24ae117b969157e943f44681ac9891d7 +updated: 2017-05-02T14:26:07.5353293+02:00 imports: - name: github.com/Azure/go-ansiterm version: fa152c58bc15761d0200cb75fe958b89a9d4888e @@ -14,7 +14,7 @@ imports: subpackages: - spew - name: github.com/docker/docker - version: 092cba3727bb9b4a2f0e922cd6c0f93ea270e363 + version: 4845c567eb35d68f35b0b1713a09b0c8d47fe67e subpackages: - api/types - api/types/blkiodev @@ -42,13 +42,13 @@ imports: - pkg/term - pkg/term/windows - name: github.com/docker/go-connections - version: 1b14b2d192e2f91cdc2bc6bf9aee0b0e116eed42 + version: e15c02316c12de00874640cd76311849de2aeed5 subpackages: - nat - name: github.com/docker/go-units version: 0dadbb0345b35ec7ef35e228dabb8de89a65bf52 - name: github.com/fsouza/go-dockerclient - version: 54fbd1ff920ca5fd5ec53068d513c14c3a25bba9 + version: 30d142bbfdec74e09ecb4bdb89a44440ae4662ac - name: github.com/go-errors/errors version: 8fa88b06e5974e97fbf9899a7f86a344bfd1f105 - name: github.com/go-redis/redis @@ -69,6 +69,10 @@ imports: version: e80d13ce29ede4452c43dea11e79b9bc8a15b478 - name: github.com/hashicorp/go-cleanhttp version: 3573b8b52aa7b37b9358d966a898feb387f62437 +- name: github.com/hashicorp/golang-lru + version: 0a025b7e63adc15a622f29b0b2c4c3848243bbf6 + subpackages: + - simplelru - name: github.com/jmoiron/sqlx version: f4076845477b10ac2453a16377a8383467aafe72 subpackages: @@ -78,22 +82,24 @@ imports: subpackages: - oid - name: github.com/Microsoft/go-winio - version: fff283ad5116362ca252298cfc9b95828956d85d + version: f3b1913901892ada21d52d0cffe1d4c7080d36b7 +- name: github.com/Nvveen/Gotty + version: cd527374f1e5bff4938207604a14f2e38a9cf512 - name: github.com/oleiade/reflections version: 2b6ec3da648e3e834dc41bad8d9ed7f2dc6a9496 - name: github.com/opencontainers/runc - version: 02141ce862d87ae7c0194880720e266111ac8b96 + version: efb2bc3fb0e36b2ec0d2c44b460bfc33a26a0bef subpackages: - libcontainer/system - libcontainer/user +- name: github.com/ory-am/dockertest + version: 9d0647ae761f96a6738c5afb49688d22979b21ff - name: github.com/ory-am/common version: b6357395e30805e2ad1f6d8fb759fa2b7146d8da subpackages: - compiler - integration - pkg -- name: github.com/ory-am/dockertest - version: 0eb2fe9391ca8b96bd6bb5ee4cc22aa63291e61a - name: github.com/pkg/errors version: 645ef00459ed84a119197bfb8d8205042c6df63d - name: github.com/pmezard/go-difflib @@ -128,7 +134,7 @@ imports: - name: gopkg.in/fatih/pool.v2 version: 6e328e67893eb46323ad06f0e92cb9536babbabc - name: gopkg.in/gorethink/gorethink.v3 - version: 610fcc04c971a9fa42f1b6625c7c52b5bb472c51 + version: 7ab832f7b65573104a555d84a27992ae9ea1f659 subpackages: - encoding - ql2 diff --git a/glide.yaml b/glide.yaml index 70c5771..c4e3fc4 100644 --- a/glide.yaml +++ b/glide.yaml @@ -1,7 +1,6 @@ -package: github.com/ory-am/ladon +package: github.com/ory/ladon import: -- package: github.com/Sirupsen/logrus - version: ~0.11.0 +- package: github.com/hashicorp/golang-lru - package: github.com/jmoiron/sqlx - package: github.com/ory-am/common version: ~0.4.0 @@ -11,13 +10,6 @@ import: - package: github.com/pkg/errors version: ~0.8.0 - package: github.com/rubenv/sql-migrate -- package: golang.org/x/net - subpackages: - - context -- package: gopkg.in/gorethink/gorethink.v3 - version: ~3.0.0 -- package: github.com/go-redis/redis - version: ~6.1.3 testImport: - package: github.com/go-sql-driver/mysql version: ~1.3.0 diff --git a/ladon.go b/ladon.go index 4cf11de..bf6805c 100644 --- a/ladon.go +++ b/ladon.go @@ -1,34 +1,44 @@ package ladon import ( - "regexp" - - "github.com/ory-am/common/compiler" "github.com/pkg/errors" ) // Ladon is an implementation of Warden. type Ladon struct { Manager Manager + Matcher matcher +} + +func (l *Ladon) matcher() matcher { + if l.Matcher == nil { + l.Matcher = DefaultMatcher + } + return l.Matcher } // IsAllowed returns nil if subject s has permission p on resource r with context c or an error otherwise. -func (g *Ladon) IsAllowed(r *Request) (err error) { - policies, err := g.Manager.FindPoliciesForSubject(r.Subject) +func (l *Ladon) IsAllowed(r *Request) (err error) { + policies, err := l.Manager.FindRequestCandidates(r) if err != nil { return err } - return g.doPoliciesAllow(r, policies) + // Although the manager is responsible of matching the policies, it might decide to just scan for + // subjects, it might return all policies, or it might have a different pattern matching than Golang. + // Thus, we need to make sure that we actually matched the right policies. + return l.doPoliciesAllow(r, policies) } -func (g *Ladon) doPoliciesAllow(r *Request, policies []Policy) (err error) { +func (l *Ladon) doPoliciesAllow(r *Request, policies []Policy) (err error) { var allowed = false // Iterate through all policies for _, p := range policies { // Does the action match with one of the policies? - if pm, err := Match(p, p.GetActions(), r.Action); err != nil { + // This is the first check because usually actions are a superset of get|update|delete|set + // and thus match faster. + if pm, err := l.matcher().Matches(p, p.GetActions(), r.Action); err != nil { return errors.WithStack(err) } else if !pm { // no, continue to next policy @@ -36,7 +46,9 @@ func (g *Ladon) doPoliciesAllow(r *Request, policies []Policy) (err error) { } // Does the subject match with one of the policies? - if sm, err := Match(p, p.GetSubjects(), r.Subject); err != nil { + // There are usually less subjects than resources which is why this is checked + // before checking for resources. + if sm, err := l.matcher().Matches(p, p.GetSubjects(), r.Subject); err != nil { return err } else if !sm { // no, continue to next policy @@ -44,7 +56,7 @@ func (g *Ladon) doPoliciesAllow(r *Request, policies []Policy) (err error) { } // Does the resource match with one of the policies? - if rm, err := Match(p, p.GetResources(), r.Resource); err != nil { + if rm, err := l.matcher().Matches(p, p.GetResources(), r.Resource); err != nil { return errors.WithStack(err) } else if !rm { // no, continue to next policy @@ -52,7 +64,8 @@ func (g *Ladon) doPoliciesAllow(r *Request, policies []Policy) (err error) { } // Are the policies conditions met? - if !g.passesConditions(p, r) { + // This is checked first because it usually has a small complexity. + if !l.passesConditions(p, r) { // no, continue to next policy continue } @@ -71,24 +84,7 @@ func (g *Ladon) doPoliciesAllow(r *Request, policies []Policy) (err error) { return nil } -// Match matches a needle with an array of regular expressions and returns true if a match was found. -func Match(p Policy, haystack []string, needle string) (bool, error) { - var reg *regexp.Regexp - var err error - for _, h := range haystack { - reg, err = compiler.CompileRegex(h, p.GetStartDelimiter(), p.GetEndDelimiter()) - if err != nil { - return false, errors.WithStack(err) - } - - if reg.MatchString(needle) { - return true, nil - } - } - return false, nil -} - -func (g *Ladon) passesConditions(p Policy, r *Request) bool { +func (l *Ladon) passesConditions(p Policy, r *Request) bool { for key, condition := range p.GetConditions() { if pass := condition.Fulfills(r.Context[key], r); !pass { return false diff --git a/ladon_test.go b/ladon_test.go index 895160c..cc61223 100644 --- a/ladon_test.go +++ b/ladon_test.go @@ -1,24 +1,25 @@ -package ladon +package ladon_test import ( + "fmt" "testing" + . "github.com/ory/ladon" + . "github.com/ory/ladon/manager/memory" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "fmt" ) - // A bunch of exemplary policies var pols = []Policy{ &DefaultPolicy{ - ID: "1", + ID: "1", Description: `This policy allows max, peter, zac and ken to create, delete and get the listed resources, but only if the client ip matches and the request states that they are the owner of those resources as well.`, - Subjects: []string{"max", "peter", ""}, - Resources: []string{"myrn:some.domain.com:resource:123", "myrn:some.domain.com:resource:345", "myrn:something:foo:<.+>"}, - Actions: []string{"", "get"}, - Effect: AllowAccess, + Subjects: []string{"max", "peter", ""}, + Resources: []string{"myrn:some.domain.com:resource:123", "myrn:some.domain.com:resource:345", "myrn:something:foo:<.+>"}, + Actions: []string{"", "get"}, + Effect: AllowAccess, Conditions: Conditions{ "owner": &EqualsSubjectCondition{}, "clientIP": &CIDRCondition{ @@ -27,20 +28,20 @@ var pols = []Policy{ }, }, &DefaultPolicy{ - ID: "2", + ID: "2", Description: "This policy allows max to update any resource", - Subjects: []string{"max"}, - Actions: []string{"update"}, - Resources: []string{"<.*>"}, - Effect: AllowAccess, + Subjects: []string{"max"}, + Actions: []string{"update"}, + Resources: []string{"<.*>"}, + Effect: AllowAccess, }, &DefaultPolicy{ - ID: "3", + ID: "3", Description: "This policy denies max to broadcast any of the resources", - Subjects: []string{"max"}, - Actions: []string{"broadcast"}, - Resources: []string{"<.*>"}, - Effect: DenyAccess, + Subjects: []string{"max"}, + Actions: []string{"broadcast"}, + Resources: []string{"<.*>"}, + Effect: DenyAccess, }, } diff --git a/manager.go b/manager.go index 3dc3609..3a6fda6 100644 --- a/manager.go +++ b/manager.go @@ -12,6 +12,11 @@ type Manager interface { // Delete removes a policy. Delete(id string) error - // Finds all policies associated with the subject. - FindPoliciesForSubject(subject string) (Policies, error) + // GetAll retrieves all policies. + GetAll(limit, offset int64) (Policies, error) + + // FindRequestCandidates returns candidates that could match the request object. It either returns + // a set that exactly matches the request, or a superset of it. If an error occurs, it returns nil and + // the error. + FindRequestCandidates(r *Request) (Policies, error) } diff --git a/manager_memory.go b/manager/memory/manager_memory.go similarity index 67% rename from manager_memory.go rename to manager/memory/manager_memory.go index e582bfb..c9439a5 100644 --- a/manager_memory.go +++ b/manager/memory/manager_memory.go @@ -1,8 +1,9 @@ -package ladon +package memory import ( "sync" + . "github.com/ory/ladon" "github.com/pkg/errors" ) @@ -19,10 +20,28 @@ func NewMemoryManager() *MemoryManager { } } +// GetAll returns all policies +func (m *MemoryManager) GetAll(limit, offset int64) (Policies, error) { + ps := make(Policies, len(m.Policies)) + i := 0 + for _, p := range m.Policies { + ps[i] = p + i++ + } + + if offset + limit > int64(len(m.Policies)) { + limit = int64(len(m.Policies)) + offset = 0 + } + + return ps[offset:limit], nil +} + // Create a new pollicy to MemoryManager func (m *MemoryManager) Create(policy Policy) error { m.Lock() defer m.Unlock() + if _, found := m.Policies[policy.GetID()]; found { return errors.New("Policy exists") } @@ -51,18 +70,14 @@ func (m *MemoryManager) Delete(id string) error { return nil } -// FindPoliciesForSubject finds all policies associated with the subject. -func (m *MemoryManager) FindPoliciesForSubject(subject string) (Policies, error) { +func (m *MemoryManager) FindRequestCandidates(r *Request) (Policies, error) { m.RLock() defer m.RUnlock() - ps := Policies{} + ps := make(Policies, len(m.Policies)) + var count int for _, p := range m.Policies { - if ok, err := Match(p, p.GetSubjects(), subject); err != nil { - return Policies{}, err - } else if !ok { - continue - } - ps = append(ps, p) + ps[count] = p + count++ } return ps, nil } diff --git a/manager/sql/manager_sql.go b/manager/sql/manager_sql.go new file mode 100644 index 0000000..b2788a8 --- /dev/null +++ b/manager/sql/manager_sql.go @@ -0,0 +1,459 @@ +package sql + +import ( + "crypto/sha256" + "database/sql" + "encoding/json" + "fmt" + "strings" + + "github.com/jmoiron/sqlx" + "github.com/ory-am/common/compiler" + . "github.com/ory/ladon" + "github.com/pkg/errors" + "github.com/rubenv/sql-migrate" +) + +var sqlDown = map[string][]string{ + "1": []string{ + "DROP TABLE ladon_policy", + "DROP TABLE ladon_policy_subject", + "DROP TABLE ladon_policy_permission", + "DROP TABLE ladon_policy_resource", + }, +} + +var sqlUp = map[string][]string{ + "1": []string{`CREATE TABLE IF NOT EXISTS ladon_policy ( + id varchar(255) NOT NULL PRIMARY KEY, + description text NOT NULL, + effect text NOT NULL CHECK (effect='allow' OR effect='deny'), + conditions text NOT NULL +)`, + `CREATE TABLE IF NOT EXISTS ladon_policy_subject ( +compiled text NOT NULL, +template varchar(1023) NOT NULL, +policy varchar(255) NOT NULL, +FOREIGN KEY (policy) REFERENCES ladon_policy(id) ON DELETE CASCADE +)`, + `CREATE TABLE IF NOT EXISTS ladon_policy_permission ( +compiled text NOT NULL, +template varchar(1023) NOT NULL, +policy varchar(255) NOT NULL, +FOREIGN KEY (policy) REFERENCES ladon_policy(id) ON DELETE CASCADE +)`, + `CREATE TABLE IF NOT EXISTS ladon_policy_resource ( +compiled text NOT NULL, +template varchar(1023) NOT NULL, +policy varchar(255) NOT NULL, +FOREIGN KEY (policy) REFERENCES ladon_policy(id) ON DELETE CASCADE +)`}, + "2": []string{`CREATE TABLE IF NOT EXISTS ladon_subject ( +id varchar(64) NOT NULL PRIMARY KEY, +has_regex bool NOT NULL, +compiled varchar(511) NOT NULL UNIQUE, +template varchar(511) NOT NULL UNIQUE +)`, + `CREATE TABLE IF NOT EXISTS ladon_action ( +id varchar(64) NOT NULL PRIMARY KEY, +has_regex bool NOT NULL, +compiled varchar(511) NOT NULL UNIQUE, +template varchar(511) NOT NULL UNIQUE +)`, + `CREATE TABLE IF NOT EXISTS ladon_resource ( +id varchar(64) NOT NULL PRIMARY KEY, +has_regex bool NOT NULL, +compiled varchar(511) NOT NULL UNIQUE, +template varchar(511) NOT NULL UNIQUE +)`, + `CREATE TABLE IF NOT EXISTS ladon_policy_subject_rel ( +policy varchar(255) NOT NULL, +subject varchar(64) NOT NULL, +PRIMARY KEY (policy, subject), +FOREIGN KEY (policy) REFERENCES ladon_policy(id) ON DELETE CASCADE, +FOREIGN KEY (subject) REFERENCES ladon_subject(id) ON DELETE CASCADE +)`, + `CREATE TABLE IF NOT EXISTS ladon_policy_action_rel ( +policy varchar(255) NOT NULL, +action varchar(64) NOT NULL, +PRIMARY KEY (policy, action), +FOREIGN KEY (policy) REFERENCES ladon_policy(id) ON DELETE CASCADE, +FOREIGN KEY (action) REFERENCES ladon_action(id) ON DELETE CASCADE +)`, + `CREATE TABLE IF NOT EXISTS ladon_policy_resource_rel ( +policy varchar(255) NOT NULL, +resource varchar(64) NOT NULL, +PRIMARY KEY (policy, resource), +FOREIGN KEY (policy) REFERENCES ladon_policy(id) ON DELETE CASCADE, +FOREIGN KEY (resource) REFERENCES ladon_resource(id) ON DELETE CASCADE +)`, + }, +} + +var migrations = map[string]*migrate.MemoryMigrationSource{ + "postgres": &migrate.MemoryMigrationSource{ + Migrations: []*migrate.Migration{ + {Id: "1", Up: sqlUp["1"], Down: sqlDown["1"]}, + { + Id: "2", + Up: sqlUp["2"], + }, + {Id: "3", + Up: []string{ + "CREATE INDEX ladon_subject_compiled_idx ON ladon_subject (compiled text_pattern_ops)", + "CREATE INDEX ladon_permission_compiled_idx ON ladon_action (compiled text_pattern_ops)", + "CREATE INDEX ladon_resource_compiled_idx ON ladon_resource (compiled text_pattern_ops)", + }, + Down: []string{ + "DROP INDEX ladon_subject_compiled_idx", + "DROP INDEX ladon_permission_compiled_idx", + "DROP INDEX ladon_resource_compiled_idx", + }, + }, + }, + }, + "mysql": &migrate.MemoryMigrationSource{ + Migrations: []*migrate.Migration{ + {Id: "1", Up: sqlUp["1"], Down: sqlDown["1"]}, + { + Id: "2", + Up: sqlUp["2"], + Down: []string{}, + }, + { + Id: "3", + Up: []string{ + "CREATE FULLTEXT INDEX ladon_subject_compiled_idx ON ladon_subject (compiled)", + "CREATE FULLTEXT INDEX ladon_action_compiled_idx ON ladon_action (compiled)", + "CREATE FULLTEXT INDEX ladon_resource_compiled_idx ON ladon_resource (compiled)", + }, + Down: []string{ + "DROP INDEX ladon_subject_compiled_idx", + "DROP INDEX ladon_permission_compiled_idx", + "DROP INDEX ladon_resource_compiled_idx", + }, + }, + }, + }, +} + +// SQLManager is a postgres implementation for Manager to store policies persistently. +type SQLManager struct { + db *sqlx.DB + schema []string +} + +// NewSQLManager initializes a new SQLManager for given db instance. +func NewSQLManager(db *sqlx.DB, schema []string) *SQLManager { + return &SQLManager{ + db: db, + schema: schema, + } +} + +// CreateSchemas creates ladon_policy tables +func (s *SQLManager) CreateSchemas() error { + var source *migrate.MemoryMigrationSource + switch s.db.DriverName() { + case "postgres", "pgx": + source = migrations["postgres"] + case "mysql": + source = migrations["mysql"] + default: + return errors.Errorf("Database driver %s is not supported", s.db.DriverName()) + } + + n, err := migrate.Exec(s.db.DB, s.db.DriverName(), source, migrate.Up) + if err != nil { + return errors.Wrapf(err, "Could not migrate sql schema, applied %d migrations", n) + } + return nil +} + +// Create inserts a new policy +func (s *SQLManager) Create(policy Policy) (err error) { + conditions := []byte("{}") + if policy.GetConditions() != nil { + cs := policy.GetConditions() + conditions, err = json.Marshal(&cs) + if err != nil { + return errors.WithStack(err) + } + } + + //fmt.Printf("INSERT INTO ladon_policy (id, description, effect, conditions) VALUES (\"%s\", \"%s\", \"%s\", '%s');\n", policy.GetID(), policy.GetDescription(), policy.GetEffect(), conditions) + tx, err := s.db.Begin() + if err != nil { + return errors.WithStack(err) + } + + switch s.db.DriverName() { + case "postgres", "pgx": + if _, err = tx.Exec(s.db.Rebind("INSERT INTO ladon_policy (id, description, effect, conditions) VALUES (?, ?, ?, ?) ON CONFLICT DO NOTHING"), policy.GetID(), policy.GetDescription(), policy.GetEffect(), conditions); err != nil { + if err := tx.Rollback(); err != nil { + return errors.WithStack(err) + } + return errors.WithStack(err) + } + case "mysql": + if _, err = tx.Exec(s.db.Rebind("INSERT IGNORE INTO ladon_policy (id, description, effect, conditions) VALUES (?, ?, ?, ?)"), policy.GetID(), policy.GetDescription(), policy.GetEffect(), conditions); err != nil { + if err := tx.Rollback(); err != nil { + return errors.WithStack(err) + } + return errors.WithStack(err) + } + default: + if err := tx.Rollback(); err != nil { + return errors.WithStack(err) + } + return errors.Errorf("Database driver %s is not supported", s.db.DriverName()) + } + + type relation struct { + p []string + t string + } + var relations = []relation{{p: policy.GetActions(), t: "action"}, {p: policy.GetResources(), t: "resource"}, {p: policy.GetSubjects(), t: "subject"}} + + for _, v := range relations { + for _, template := range v.p { + h := sha256.New() + h.Write([]byte(template)) + id := fmt.Sprintf("%x", h.Sum(nil)) + + compiled, err := compiler.CompileRegex(template, policy.GetStartDelimiter(), policy.GetEndDelimiter()) + if err != nil { + if err := tx.Rollback(); err != nil { + return errors.WithStack(err) + } + return errors.WithStack(err) + } + + switch s.db.DriverName() { + case "postgres", "pgx": + if _, err := tx.Exec(s.db.Rebind(fmt.Sprintf("INSERT INTO ladon_%s (id, template, compiled, has_regex) VALUES (?, ?, ?, ?) ON CONFLICT DO NOTHING", v.t)), id, template, compiled.String(), strings.Index(template, string(policy.GetStartDelimiter())) > -1); err != nil { + if err := tx.Rollback(); err != nil { + return errors.WithStack(err) + } + return errors.WithStack(err) + } + + if _, err := tx.Exec(s.db.Rebind(fmt.Sprintf("INSERT INTO ladon_policy_%s_rel (policy, %s) VALUES (?, ?) ON CONFLICT DO NOTHING", v.t, v.t)), policy.GetID(), id); err != nil { + if err := tx.Rollback(); err != nil { + return errors.WithStack(err) + } + return errors.WithStack(err) + } + break + case "mysql": + if _, err := tx.Exec(s.db.Rebind(fmt.Sprintf("INSERT IGNORE INTO ladon_%s (id, template, compiled, has_regex) VALUES (?, ?, ?, ?)", v.t)), id, template, compiled.String(), strings.Index(template, string(policy.GetStartDelimiter())) > -1); err != nil { + if err := tx.Rollback(); err != nil { + return errors.WithStack(err) + } + return errors.WithStack(err) + } + + if _, err := tx.Exec(s.db.Rebind(fmt.Sprintf("INSERT IGNORE INTO ladon_policy_%s_rel (policy, %s) VALUES (?, ?)", v.t, v.t)), policy.GetID(), id); err != nil { + if err := tx.Rollback(); err != nil { + return errors.WithStack(err) + } + return errors.WithStack(err) + } + break + default: + if err := tx.Rollback(); err != nil { + return errors.WithStack(err) + } + return errors.Errorf("Database driver %s is not supported", s.db.DriverName()) + } + } + } + + if err = tx.Commit(); err != nil { + if err := tx.Rollback(); err != nil { + return errors.WithStack(err) + } + return errors.WithStack(err) + } + + return nil +} + +func (s *SQLManager) FindRequestCandidates(r *Request) (Policies, error) { + var query string = `SELECT + p.id, p.effect, p.conditions, p.description, + subject.template as subject, resource.template as resource, action.template as action +FROM + ladon_policy as p + +INNER JOIN ladon_policy_subject_rel as rs ON rs.policy = p.id +LEFT JOIN ladon_policy_action_rel as ra ON ra.policy = p.id +LEFT JOIN ladon_policy_resource_rel as rr ON rr.policy = p.id + +INNER JOIN ladon_subject as subject ON rs.subject = subject.id +LEFT JOIN ladon_action as action ON ra.action = action.id +LEFT JOIN ladon_resource as resource ON rr.resource = resource.id + +WHERE` + switch s.db.DriverName() { + case "postgres", "pgx": + query = query + ` +( subject.has_regex IS NOT TRUE AND subject.template = $1 ) +OR +( subject.has_regex IS TRUE AND $2 ~ subject.compiled )` + break + case "mysql": + query = query + ` +( subject.has_regex = 0 AND subject.template = ? ) +OR +( subject.has_regex = 1 AND ? REGEXP BINARY subject.compiled )` + break + default: + return nil, errors.Errorf("Database driver %s is not supported", s.db.DriverName()) + } + + rows, err := s.db.Query(query, r.Subject, r.Subject) + if err == sql.ErrNoRows { + return nil, NewErrResourceNotFound(err) + } else if err != nil { + return nil, errors.WithStack(err) + } + defer rows.Close() + + return scanRows(rows) +} + +func scanRows(rows *sql.Rows) (Policies, error) { + var policies = map[string]*DefaultPolicy{} + + for rows.Next() { + var p DefaultPolicy + var conditions []byte + var resource, subject, action sql.NullString + p.Actions = []string{} + p.Subjects = []string{} + p.Resources = []string{} + + if err := rows.Scan(&p.ID, &p.Effect, &conditions, &p.Description, &subject, &resource, &action); err == sql.ErrNoRows { + return nil, NewErrResourceNotFound(err) + } else if err != nil { + return nil, errors.WithStack(err) + } + + p.Conditions = Conditions{} + if err := json.Unmarshal(conditions, &p.Conditions); err != nil { + return nil, errors.WithStack(err) + } + + if c, ok := policies[p.ID]; ok { + if action.Valid { + policies[p.ID].Actions = append(c.Actions, action.String) + } + + if subject.Valid { + policies[p.ID].Subjects = append(c.Subjects, subject.String) + } + + if resource.Valid { + policies[p.ID].Resources = append(c.Resources, resource.String) + } + } else { + if action.Valid { + p.Actions = []string{action.String} + } + + if subject.Valid { + p.Subjects = []string{subject.String} + } + + if resource.Valid { + p.Resources = []string{resource.String} + } + + policies[p.ID] = &p + } + } + + var result = make(Policies, len(policies)) + var count int + for _, v := range policies { + v.Actions = uniq(v.Actions) + v.Resources = uniq(v.Resources) + v.Subjects = uniq(v.Subjects) + result[count] = v + count++ + } + + return result, nil +} + +var getQuery = `SELECT + p.id, p.effect, p.conditions, p.description, + subject.template as subject, resource.template as resource, action.template as action +FROM + ladon_policy as p + +LEFT JOIN ladon_policy_subject_rel as rs ON rs.policy = p.id +LEFT JOIN ladon_policy_action_rel as ra ON ra.policy = p.id +LEFT JOIN ladon_policy_resource_rel as rr ON rr.policy = p.id + +LEFT JOIN ladon_subject as subject ON rs.subject = subject.id +LEFT JOIN ladon_action as action ON ra.action = action.id +LEFT JOIN ladon_resource as resource ON rr.resource = resource.id + +` + +// GetAll returns all policies +func (s *SQLManager) GetAll(limit, offset int64) (Policies, error) { + query := s.db.Rebind(getQuery + "LIMIT ? OFFSET ?") + + rows, err := s.db.Query(query, limit, offset) + if err != nil { + return nil, errors.WithStack(err) + } + defer rows.Close() + + return scanRows(rows) +} + +// Get retrieves a policy. +func (s *SQLManager) Get(id string) (Policy, error) { + query := s.db.Rebind(getQuery + "WHERE p.id=?") + + rows, err := s.db.Query(query, id) + if err == sql.ErrNoRows { + return nil, NewErrResourceNotFound(err) + } else if err != nil { + return nil, errors.WithStack(err) + } + defer rows.Close() + + policies, err := scanRows(rows) + if err != nil { + return nil, errors.WithStack(err) + } else if len(policies) == 0 { + return nil, NewErrResourceNotFound(sql.ErrNoRows) + } + + return policies[0], nil +} + +// Delete removes a policy. +func (s *SQLManager) Delete(id string) error { + _, err := s.db.Exec(s.db.Rebind("DELETE FROM ladon_policy WHERE id=?"), id) + return errors.WithStack(err) +} + +func uniq(input []string) []string { + u := make([]string, 0, len(input)) + m := make(map[string]bool) + + for _, val := range input { + if _, ok := m[val]; !ok { + m[val] = true + u = append(u, val) + } + } + + return u +} diff --git a/manager/sql/manager_sql_migration_0_5_to_0_6.go b/manager/sql/manager_sql_migration_0_5_to_0_6.go new file mode 100644 index 0000000..1e447c2 --- /dev/null +++ b/manager/sql/manager_sql_migration_0_5_to_0_6.go @@ -0,0 +1,153 @@ +package sql + +import ( + "database/sql" + "encoding/json" + "fmt" + + "log" + + "github.com/jmoiron/sqlx" + "github.com/ory-am/common/compiler" + "github.com/ory-am/common/pkg" + "github.com/ory/ladon" + . "github.com/ory/ladon" + "github.com/pkg/errors" +) + +type SQLManagerMigrateFromMajor0Minor6ToMajor0Minor7 struct { + DB *sqlx.DB + SQLManager *SQLManager +} + +func (s *SQLManagerMigrateFromMajor0Minor6ToMajor0Minor7) GetManager() ladon.Manager { + return s.SQLManager +} + +// Get retrieves a policy. +func (s *SQLManagerMigrateFromMajor0Minor6ToMajor0Minor7) Migrate() error { + rows, err := s.DB.Query(s.DB.Rebind("SELECT id, description, effect, conditions FROM ladon_policy")) + if err != nil { + return errors.WithStack(err) + } + defer rows.Close() + + var pols = Policies{} + for rows.Next() { + var p DefaultPolicy + var conditions []byte + + if err := rows.Scan(&p.ID, &p.Description, &p.Effect, &conditions); err != nil { + return errors.WithStack(err) + } + + p.Conditions = Conditions{} + if err := json.Unmarshal(conditions, &p.Conditions); err != nil { + return errors.WithStack(err) + } + + subjects, err := getLinkedSQL(s.DB, "ladon_policy_subject", p.GetID()) + if err != nil { + return errors.WithStack(err) + } + permissions, err := getLinkedSQL(s.DB, "ladon_policy_permission", p.GetID()) + if err != nil { + return errors.WithStack(err) + } + resources, err := getLinkedSQL(s.DB, "ladon_policy_resource", p.GetID()) + if err != nil { + return errors.WithStack(err) + } + + log.Printf("[DEBUG] Found policy %s", p.GetID()) + + p.Actions = permissions + p.Subjects = subjects + p.Resources = resources + pols = append(pols, &p) + } + + log.Printf("[DEBUG] Found %d policies, migrating", len(pols)) + + for _, p := range pols { + log.Printf("[DEBUG] Inserting policy %s", p.GetID()) + if err := s.SQLManager.Create(p); err != nil { + log.Printf("[DEBUG] Unable to insert policy %d: %s", p.GetID(), err) + return errors.WithStack(err) + } + } + + log.Printf("[DEBUG] Migrated %d policies successfully", len(pols)) + + return nil +} + +func getLinkedSQL(db *sqlx.DB, table, policy string) ([]string, error) { + urns := []string{} + rows, err := db.Query(db.Rebind(fmt.Sprintf("SELECT template FROM %s WHERE policy=?", table)), policy) + if err == sql.ErrNoRows { + return nil, errors.Wrap(pkg.ErrNotFound, "") + } else if err != nil { + return nil, errors.WithStack(err) + } + + defer rows.Close() + for rows.Next() { + var urn string + if err = rows.Scan(&urn); err != nil { + return []string{}, errors.WithStack(err) + } + urns = append(urns, urn) + } + return urns, nil +} + +// Create inserts a new policy +func (s *SQLManagerMigrateFromMajor0Minor6ToMajor0Minor7) Create(policy Policy) (err error) { + conditions := []byte("{}") + if policy.GetConditions() != nil { + cs := policy.GetConditions() + conditions, err = json.Marshal(&cs) + if err != nil { + return errors.WithStack(err) + } + } + + if tx, err := s.DB.Begin(); err != nil { + return errors.WithStack(err) + } else if _, err = tx.Exec(s.DB.Rebind("INSERT INTO ladon_policy (id, description, effect, conditions) VALUES (?, ?, ?, ?)"), policy.GetID(), policy.GetDescription(), policy.GetEffect(), conditions); err != nil { + if err := tx.Rollback(); err != nil { + return errors.WithStack(err) + } + return errors.WithStack(err) + } else if err = createLinkSQL(s.DB, tx, "ladon_policy_subject", policy, policy.GetSubjects()); err != nil { + return err + } else if err = createLinkSQL(s.DB, tx, "ladon_policy_permission", policy, policy.GetActions()); err != nil { + return err + } else if err = createLinkSQL(s.DB, tx, "ladon_policy_resource", policy, policy.GetResources()); err != nil { + return err + } else if err = tx.Commit(); err != nil { + if err := tx.Rollback(); err != nil { + return errors.WithStack(err) + } + return errors.WithStack(err) + } + + return nil +} + +func createLinkSQL(db *sqlx.DB, tx *sql.Tx, table string, p Policy, templates []string) error { + for _, template := range templates { + reg, err := compiler.CompileRegex(template, p.GetStartDelimiter(), p.GetEndDelimiter()) + + // Execute SQL statement + query := db.Rebind(fmt.Sprintf("INSERT INTO %s (policy, template, compiled) VALUES (?, ?, ?)", table)) + if _, err = tx.Exec(query, p.GetID(), template, reg.String()); err != nil { + if rb := tx.Rollback(); rb != nil { + return errors.WithStack(rb) + } + return errors.WithStack(err) + } + } + return nil +} diff --git a/manager_all_test.go b/manager_all_test.go new file mode 100644 index 0000000..cee5518 --- /dev/null +++ b/manager_all_test.go @@ -0,0 +1,254 @@ +package ladon_test + +import ( + "fmt" + "log" + "os" + "sync" + "testing" + + _ "github.com/go-sql-driver/mysql" + _ "github.com/lib/pq" + "github.com/ory-am/common/integration" + . "github.com/ory/ladon" + . "github.com/ory/ladon/manager/memory" + . "github.com/ory/ladon/manager/sql" + "github.com/pborman/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var managerPolicies = []*DefaultPolicy{ + { + ID: uuid.New(), + Description: "description", + Subjects: []string{"user", "anonymous"}, + Effect: AllowAccess, + Resources: []string{"article", "user"}, + Actions: []string{"create", "update"}, + Conditions: Conditions{}, + }, + { + ID: uuid.New(), + Description: "description", + Subjects: []string{}, + Effect: AllowAccess, + Resources: []string{""}, + Actions: []string{"view"}, + Conditions: Conditions{}, + }, + { + ID: uuid.New(), + Description: "description", + Subjects: []string{}, + Effect: AllowAccess, + Resources: []string{}, + Actions: []string{"view"}, + Conditions: Conditions{}, + }, + { + ID: uuid.New(), + Description: "description", + Subjects: []string{}, + Effect: AllowAccess, + Resources: []string{}, + Actions: []string{}, + Conditions: Conditions{}, + }, + { + ID: uuid.New(), + Description: "description", + Subjects: []string{}, + Effect: AllowAccess, + Resources: []string{"foo"}, + Actions: []string{}, + Conditions: Conditions{}, + }, + { + ID: uuid.New(), + Description: "description", + Subjects: []string{"foo"}, + Effect: AllowAccess, + Resources: []string{"foo"}, + Actions: []string{}, + Conditions: Conditions{}, + }, + { + ID: uuid.New(), + Description: "description", + Subjects: []string{"foo"}, + Effect: AllowAccess, + Resources: []string{}, + Actions: []string{}, + Conditions: Conditions{}, + }, + { + ID: uuid.New(), + Description: "description", + Effect: AllowAccess, + Conditions: Conditions{}, + }, + { + ID: uuid.New(), + Description: "description", + Subjects: []string{""}, + Effect: DenyAccess, + Resources: []string{"article", "user"}, + Actions: []string{"view"}, + Conditions: Conditions{ + "owner": &EqualsSubjectCondition{}, + }, + }, + { + ID: uuid.New(), + Description: "description", + Subjects: []string{"", "peter"}, + Effect: DenyAccess, + Resources: []string{".*"}, + Actions: []string{"disable"}, + Conditions: Conditions{ + "ip": &CIDRCondition{ + CIDR: "1234", + }, + "owner": &EqualsSubjectCondition{}, + }, + }, + { + ID: uuid.New(), + Description: "description", + Subjects: []string{"<.*>"}, + Effect: AllowAccess, + Resources: []string{""}, + Actions: []string{"view"}, + Conditions: Conditions{ + "ip": &CIDRCondition{ + CIDR: "1234", + }, + "owner": &EqualsSubjectCondition{}, + }, + }, + { + ID: uuid.New(), + Description: "description", + Subjects: []string{""}, + Effect: AllowAccess, + Resources: []string{""}, + Actions: []string{"view"}, + Conditions: Conditions{ + "ip": &CIDRCondition{ + CIDR: "1234", + }, + "owner": &EqualsSubjectCondition{}, + }, + }, +} + +var managers = map[string]Manager{} +var migrators = map[string]ManagerMigrator{} + +func TestMain(m *testing.M) { + var wg sync.WaitGroup + wg.Add(3) + connectMySQL(&wg) + connectPG(&wg) + connectMEM(&wg) + wg.Wait() + + s := m.Run() + integration.KillAll() + os.Exit(s) +} + +func connectMEM(wg *sync.WaitGroup) { + defer wg.Done() + managers["memory"] = NewMemoryManager() +} + +func connectPG(wg *sync.WaitGroup) { + defer wg.Done() + var db = integration.ConnectToPostgres("ladon") + s := NewSQLManager(db, nil) + if err := s.CreateSchemas(); err != nil { + log.Fatalf("Could not create postgres schema: %v", err) + } + + managers["postgres"] = s + migrators["postgres"] = &SQLManagerMigrateFromMajor0Minor6ToMajor0Minor7{ + DB: db, + SQLManager: s, + } +} + +func connectMySQL(wg *sync.WaitGroup) { + defer wg.Done() + var db = integration.ConnectToMySQL() + s := NewSQLManager(db, nil) + if err := s.CreateSchemas(); err != nil { + log.Fatalf("Could not create mysql schema: %v", err) + } + + managers["mysql"] = s + migrators["mysql"] = &SQLManagerMigrateFromMajor0Minor6ToMajor0Minor7{ + DB: db, + SQLManager: s, + } +} + +func TestGetErrors(t *testing.T) { + for _, s := range managers { + _, err := s.Get(uuid.New()) + assert.Error(t, err) + + _, err = s.Get("asdf") + assert.Error(t, err) + } +} + +func TestCreateGetDelete(t *testing.T) { + for k, s := range managers { + t.Run(fmt.Sprintf("manager=%s", k), func(t *testing.T) { + for i, c := range managerPolicies { + t.Run(fmt.Sprintf("case=%d/id=%s/type=create", i, c.GetID()), func(t *testing.T) { + _, err := s.Get(c.GetID()) + require.Error(t, err) + require.NoError(t, s.Create(c)) + }) + + t.Run(fmt.Sprintf("case=%d/id=%s/type=query", i, c.GetID()), func(t *testing.T) { + get, err := s.Get(c.GetID()) + require.NoError(t, err) + + assertPolicyEqual(t, c, get) + }) + } + + t.Run("type=query-all", func(t *testing.T) { + pols, err := s.GetAll(100, 0) + require.NoError(t, err) + assert.Len(t, pols, len(managerPolicies)) + + found := map[string]int{} + for _, got := range pols { + for _, expect := range managerPolicies { + if got.GetID() == expect.GetID() { + found[got.GetID()]++ + } + } + } + + for _, f := range found { + assert.Equal(t, 1, f) + } + }) + + for i, c := range managerPolicies { + t.Run(fmt.Sprintf("case=%d/id=%s/type=delete", i, c.GetID()), func(t *testing.T) { + assert.NoError(t, s.Delete(c.ID)) + + _, err := s.Get(c.GetID()) + assert.Error(t, err) + }) + } + }) + } +} diff --git a/manager_migrator.go b/manager_migrator.go new file mode 100644 index 0000000..61cf282 --- /dev/null +++ b/manager_migrator.go @@ -0,0 +1,7 @@ +package ladon + +type ManagerMigrator interface { + Create(policy Policy) (err error) + Migrate() (err error) + GetManager() Manager +} diff --git a/manager_mock_test.go b/manager_mock_test.go index c49dda3..cd57f13 100644 --- a/manager_mock_test.go +++ b/manager_mock_test.go @@ -1,35 +1,35 @@ // Automatically generated by MockGen. DO NOT EDIT! -// Source: github.com/ory-am/ladon (interfaces: Manager) +// Source: github.com/ory/ladon (interfaces: Manager) package ladon_test import ( gomock "github.com/golang/mock/gomock" - ladon "github.com/ory-am/ladon" + ladon "github.com/ory/ladon" ) // Mock of Manager interface -type mockManager struct { +type MockManager struct { ctrl *gomock.Controller recorder *_MockManagerRecorder } // Recorder for MockManager (not exported) type _MockManagerRecorder struct { - mock *mockManager + mock *MockManager } -func newMockManager(ctrl *gomock.Controller) *mockManager { - mock := &mockManager{ctrl: ctrl} +func NewMockManager(ctrl *gomock.Controller) *MockManager { + mock := &MockManager{ctrl: ctrl} mock.recorder = &_MockManagerRecorder{mock} return mock } -func (_m *mockManager) EXPECT() *_MockManagerRecorder { +func (_m *MockManager) EXPECT() *_MockManagerRecorder { return _m.recorder } -func (_m *mockManager) Create(_param0 ladon.Policy) error { +func (_m *MockManager) Create(_param0 ladon.Policy) error { ret := _m.ctrl.Call(_m, "Create", _param0) ret0, _ := ret[0].(error) return ret0 @@ -39,7 +39,7 @@ func (_mr *_MockManagerRecorder) Create(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Create", arg0) } -func (_m *mockManager) Delete(_param0 string) error { +func (_m *MockManager) Delete(_param0 string) error { ret := _m.ctrl.Call(_m, "Delete", _param0) ret0, _ := ret[0].(error) return ret0 @@ -49,18 +49,18 @@ func (_mr *_MockManagerRecorder) Delete(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Delete", arg0) } -func (_m *mockManager) FindPoliciesForSubject(_param0 string) (ladon.Policies, error) { - ret := _m.ctrl.Call(_m, "FindPoliciesForSubject", _param0) +func (_m *MockManager) FindRequestCandidates(_param0 *ladon.Request) (ladon.Policies, error) { + ret := _m.ctrl.Call(_m, "FindRequestCandidates", _param0) ret0, _ := ret[0].(ladon.Policies) ret1, _ := ret[1].(error) return ret0, ret1 } -func (_mr *_MockManagerRecorder) FindPoliciesForSubject(arg0 interface{}) *gomock.Call { - return _mr.mock.ctrl.RecordCall(_mr.mock, "FindPoliciesForSubject", arg0) +func (_mr *_MockManagerRecorder) FindRequestCandidates(arg0 interface{}) *gomock.Call { + return _mr.mock.ctrl.RecordCall(_mr.mock, "FindRequestCandidates", arg0) } -func (_m *mockManager) Get(_param0 string) (ladon.Policy, error) { +func (_m *MockManager) Get(_param0 string) (ladon.Policy, error) { ret := _m.ctrl.Call(_m, "Get", _param0) ret0, _ := ret[0].(ladon.Policy) ret1, _ := ret[1].(error) @@ -70,3 +70,14 @@ func (_m *mockManager) Get(_param0 string) (ladon.Policy, error) { func (_mr *_MockManagerRecorder) Get(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Get", arg0) } + +func (_m *MockManager) GetAll(_param0 int64, _param1 int64) (ladon.Policies, error) { + ret := _m.ctrl.Call(_m, "GetAll", _param0, _param1) + ret0, _ := ret[0].(ladon.Policies) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +func (_mr *_MockManagerRecorder) GetAll(arg0, arg1 interface{}) *gomock.Call { + return _mr.mock.ctrl.RecordCall(_mr.mock, "GetAll", arg0, arg1) +} diff --git a/manager_redis.go b/manager_redis.go deleted file mode 100644 index bbf4c21..0000000 --- a/manager_redis.go +++ /dev/null @@ -1,111 +0,0 @@ -package ladon - -import ( - "encoding/json" - - "github.com/pkg/errors" - "github.com/go-redis/redis" -) - -// RedisManager is a redis implementation of Manager to store policies persistently. -type RedisManager struct { - db *redis.Client - keyPrefix string -} - -// NewRedisManager initializes a new RedisManager with no policies -func NewRedisManager(db *redis.Client, keyPrefix string) *RedisManager { - return &RedisManager{ - db: db, - keyPrefix: keyPrefix, - } -} - -const redisPolicies = "ladon:policies" - -var ( - redisPolicyExists = errors.New("Policy exists") - redisNotFound = errors.New("Not found") -) - -func (m *RedisManager) redisPoliciesKey() string { - return m.keyPrefix + redisPolicies -} - -// Create a new policy to RedisManager -func (m *RedisManager) Create(policy Policy) error { - payload, err := json.Marshal(policy) - if err != nil { - return errors.Wrap(err, "policy marshal failed") - } - - wasKeySet, err := m.db.HSetNX(m.redisPoliciesKey(), policy.GetID(), string(payload)).Result() - if !wasKeySet { - return errors.WithStack(redisPolicyExists) - } else if err != nil { - return errors.Wrap(err, "policy creation failed") - } - - return nil -} - -// Get retrieves a policy. -func (m *RedisManager) Get(id string) (Policy, error) { - resp, err := m.db.HGet(m.redisPoliciesKey(), id).Bytes() - if err == redis.Nil { - return nil, redisNotFound - } else if err != nil { - return nil, errors.WithStack(err) - } - - return redisUnmarshalPolicy(resp) -} - -// Delete removes a policy. -func (m *RedisManager) Delete(id string) error { - if err := m.db.HDel(m.redisPoliciesKey(), id).Err(); err != nil { - return errors.Wrap(err, "policy deletion failed") - } - - return nil -} - -// FindPoliciesForSubject finds all policies associated with the subject. -func (m *RedisManager) FindPoliciesForSubject(subject string) (Policies, error) { - var ps Policies - - iter := m.db.HScan(m.redisPoliciesKey(), 0, "", 0).Iterator() - for iter.Next() { - if !iter.Next() { - break - } - resp := []byte(iter.Val()) - - p, err := redisUnmarshalPolicy(resp) - if err != nil { - return nil, err - } - - if ok, err := Match(p, p.GetSubjects(), subject); err != nil { - return nil, errors.Wrap(err, "policy subject match failed") - } else if !ok { - continue - } - - ps = append(ps, p) - } - if err := iter.Err(); err != nil { - return nil, errors.WithStack(err) - } - - return ps, nil -} - -func redisUnmarshalPolicy(policy []byte) (Policy, error) { - var p *DefaultPolicy - if err := json.Unmarshal(policy, &p); err != nil { - return nil, errors.Wrap(err, "policy unmarshal failed") - } - - return p, nil -} diff --git a/manager_rethink.go b/manager_rethink.go deleted file mode 100644 index 38e9b60..0000000 --- a/manager_rethink.go +++ /dev/null @@ -1,262 +0,0 @@ -package ladon - -import ( - "encoding/json" - "sync" - - "time" - - "github.com/Sirupsen/logrus" - "github.com/pkg/errors" - "golang.org/x/net/context" - r "gopkg.in/gorethink/gorethink.v3" -) - -// stupid hack -type rdbSchema struct { - ID string `json:"id" gorethink:"id"` - Description string `json:"description" gorethink:"description"` - Subjects []string `json:"subjects" gorethink:"subjects"` - Effect string `json:"effect" gorethink:"effect"` - Resources []string `json:"resources" gorethink:"resources"` - Actions []string `json:"actions" gorethink:"actions"` - Conditions json.RawMessage `json:"conditions" gorethink:"conditions"` -} - -func rdbToPolicy(s *rdbSchema) (*DefaultPolicy, error) { - if s == nil { - return nil, nil - } - - ret := &DefaultPolicy{ - ID: s.ID, - Description: s.Description, - Subjects: s.Subjects, - Effect: s.Effect, - Resources: s.Resources, - Actions: s.Actions, - Conditions: Conditions{}, - } - - if err := ret.Conditions.UnmarshalJSON(s.Conditions); err != nil { - return nil, errors.WithStack(err) - } - - return ret, nil - -} - -func rdbFromPolicy(p Policy) (*rdbSchema, error) { - cs, err := p.GetConditions().MarshalJSON() - if err != nil { - return nil, err - } - return &rdbSchema{ - ID: p.GetID(), - Description: p.GetDescription(), - Subjects: p.GetSubjects(), - Effect: p.GetEffect(), - Resources: p.GetResources(), - Actions: p.GetActions(), - Conditions: cs, - }, err -} - -// NewRethinkManager initializes a new RethinkManager for given session, table name defaults -// to "policies". -func NewRethinkManager(session *r.Session, table string) *RethinkManager { - if table == "" { - table = "policies" - } - - policies := make(map[string]Policy) - - return &RethinkManager{ - Session: session, - Table: r.Table(table), - Policies: policies, - } -} - -// RethinkManager is a rethinkdb implementation of Manager to store policies persistently. -type RethinkManager struct { - Session *r.Session - Table r.Term - sync.RWMutex - - Policies map[string]Policy -} - -// ColdStart loads all policies from rethinkdb into memory. -func (m *RethinkManager) ColdStart() error { - m.Policies = map[string]Policy{} - policies, err := m.Table.Run(m.Session) - if err != nil { - return errors.WithStack(err) - } - - m.Lock() - defer m.Unlock() - var tbl rdbSchema - for policies.Next(&tbl) { - policy, err := rdbToPolicy(&tbl) - if err != nil { - return err - } - m.Policies[tbl.ID] = policy - } - - return nil -} - -// Create inserts a new policy. -func (m *RethinkManager) Create(policy Policy) error { - if err := m.publishCreate(policy); err != nil { - return err - } - - return nil -} - -// Get retrieves a policy. -func (m *RethinkManager) Get(id string) (Policy, error) { - m.RLock() - defer m.RUnlock() - - p, ok := m.Policies[id] - if !ok { - return nil, errors.New("Not found") - } - - return p, nil -} - -// Delete removes a policy. -func (m *RethinkManager) Delete(id string) error { - if err := m.publishDelete(id); err != nil { - return err - } - - return nil -} - -// FindPoliciesForSubject returns Policies (an array of policy) for a given subject. -func (m *RethinkManager) FindPoliciesForSubject(subject string) (Policies, error) { - m.RLock() - defer m.RUnlock() - - ps := Policies{} - for _, p := range m.Policies { - if ok, err := Match(p, p.GetSubjects(), subject); err != nil { - return Policies{}, err - } else if !ok { - continue - } - ps = append(ps, p) - } - return ps, nil -} - -func (m *RethinkManager) fetch() error { - m.Policies = map[string]Policy{} - policies, err := m.Table.Run(m.Session) - if err != nil { - return errors.WithStack(err) - } - - var policy DefaultPolicy - m.Lock() - defer m.Unlock() - for policies.Next(&policy) { - m.Policies[policy.ID] = &policy - } - - return nil -} - -func (m *RethinkManager) publishCreate(policy Policy) error { - p, err := rdbFromPolicy(policy) - if err != nil { - return err - } - if _, err := m.Table.Insert(p).RunWrite(m.Session); err != nil { - return errors.WithStack(err) - } - return nil -} - -func (m *RethinkManager) publishDelete(id string) error { - if _, err := m.Table.Get(id).Delete().RunWrite(m.Session); err != nil { - return errors.WithStack(err) - } - return nil -} - -// Watch is used to watch for changes on rethinkdb (which happens -// asynchronous) and updates manager's policy accordingly. -func (m *RethinkManager) Watch(ctx context.Context) { - go retry(time.Second*15, time.Minute, func() error { - policies, err := m.Table.Changes().Run(m.Session) - if err != nil { - return errors.WithStack(err) - } - - defer policies.Close() - var update = make(map[string]*rdbSchema) - for policies.Next(&update) { - logrus.Debug("Received update from RethinkDB Cluster in policy manager.") - newVal, err := rdbToPolicy(update["new_val"]) - if err != nil { - logrus.Error(err) - continue - } - - oldVal, err := rdbToPolicy(update["old_val"]) - if err != nil { - logrus.Error(err) - continue - } - - m.Lock() - if newVal == nil && oldVal != nil { - delete(m.Policies, oldVal.GetID()) - } else if newVal != nil && oldVal != nil { - delete(m.Policies, oldVal.GetID()) - m.Policies[newVal.GetID()] = newVal - } else { - m.Policies[newVal.GetID()] = newVal - } - m.Unlock() - } - - if policies.Err() != nil { - logrus.Error(errors.Wrap(policies.Err(), "")) - } - return nil - }) -} - -func retry(maxWait time.Duration, failAfter time.Duration, f func() error) (err error) { - var lastStart time.Time - err = errors.New("Did not connect.") - loopWait := time.Millisecond * 500 - retryStart := time.Now() - for retryStart.Add(failAfter).After(time.Now()) { - lastStart = time.Now() - if err = f(); err == nil { - return nil - } - - if lastStart.Add(maxWait * 2).Before(time.Now()) { - retryStart = time.Now() - } - - logrus.Infof("Retrying in %f seconds...", loopWait.Seconds()) - time.Sleep(loopWait) - loopWait = loopWait * time.Duration(int64(2)) - if loopWait > maxWait { - loopWait = maxWait - } - } - return err -} diff --git a/manager_sql.go b/manager_sql.go deleted file mode 100644 index f32c4ff..0000000 --- a/manager_sql.go +++ /dev/null @@ -1,232 +0,0 @@ -package ladon - -import ( - "database/sql" - "encoding/json" - "fmt" - - "github.com/jmoiron/sqlx" - "github.com/ory-am/common/compiler" - "github.com/ory-am/common/pkg" - "github.com/pkg/errors" - "github.com/rubenv/sql-migrate" -) - -var migrations = &migrate.MemoryMigrationSource{ - Migrations: []*migrate.Migration{ - &migrate.Migration{ - Id: "1", - Up: []string{`CREATE TABLE IF NOT EXISTS ladon_policy ( - id varchar(255) NOT NULL PRIMARY KEY, - description text NOT NULL, - effect text NOT NULL CHECK (effect='allow' OR effect='deny'), - conditions text NOT NULL -)`, - `CREATE TABLE IF NOT EXISTS ladon_policy_subject ( - compiled text NOT NULL, - template varchar(1023) NOT NULL, - policy varchar(255) NOT NULL, - FOREIGN KEY (policy) REFERENCES ladon_policy(id) ON DELETE CASCADE -)`, - `CREATE TABLE IF NOT EXISTS ladon_policy_permission ( - compiled text NOT NULL, - template varchar(1023) NOT NULL, - policy varchar(255) NOT NULL, - FOREIGN KEY (policy) REFERENCES ladon_policy(id) ON DELETE CASCADE -)`, - `CREATE TABLE IF NOT EXISTS ladon_policy_resource ( - compiled text NOT NULL, - template varchar(1023) NOT NULL, - policy varchar(255) NOT NULL, - FOREIGN KEY (policy) REFERENCES ladon_policy(id) ON DELETE CASCADE -)`}, - Down: []string{ - "DROP TABLE ladon_policy", - "DROP TABLE ladon_policy_subject", - "DROP TABLE ladon_policy_permission", - "DROP TABLE ladon_policy_resource", - }, - }, - }, -} - -// SQLManager is a postgres implementation for Manager to store policies persistently. -type SQLManager struct { - db *sqlx.DB - schema []string -} - -// NewSQLManager initializes a new SQLManager for given db instance. -func NewSQLManager(db *sqlx.DB, schema []string) *SQLManager { - return &SQLManager{ - db: db, - schema: schema, - } -} - -// CreateSchemas creates ladon_policy tables -func (s *SQLManager) CreateSchemas() error { - n, err := migrate.Exec(s.db.DB, s.db.DriverName(), migrations, migrate.Up) - if err != nil { - return errors.Wrapf(err, "Could not migrate sql schema, applied %d migrations", n) - } - return nil -} - -// Create inserts a new policy -func (s *SQLManager) Create(policy Policy) (err error) { - conditions := []byte("{}") - if policy.GetConditions() != nil { - cs := policy.GetConditions() - conditions, err = json.Marshal(&cs) - if err != nil { - return errors.WithStack(err) - } - } - - if tx, err := s.db.Begin(); err != nil { - return errors.WithStack(err) - } else if _, err = tx.Exec(s.db.Rebind("INSERT INTO ladon_policy (id, description, effect, conditions) VALUES (?, ?, ?, ?)"), policy.GetID(), policy.GetDescription(), policy.GetEffect(), conditions); err != nil { - if err := tx.Rollback(); err != nil { - return errors.WithStack(err) - } - return errors.WithStack(err) - } else if err = createLinkSQL(s.db, tx, "ladon_policy_subject", policy, policy.GetSubjects()); err != nil { - return err - } else if err = createLinkSQL(s.db, tx, "ladon_policy_permission", policy, policy.GetActions()); err != nil { - return err - } else if err = createLinkSQL(s.db, tx, "ladon_policy_resource", policy, policy.GetResources()); err != nil { - return err - } else if err = tx.Commit(); err != nil { - if err := tx.Rollback(); err != nil { - return errors.WithStack(err) - } - return errors.WithStack(err) - } - - return nil -} - -// Get retrieves a policy. -func (s *SQLManager) Get(id string) (Policy, error) { - var p DefaultPolicy - var conditions []byte - - if err := s.db.QueryRow(s.db.Rebind("SELECT id, description, effect, conditions FROM ladon_policy WHERE id=?"), id).Scan(&p.ID, &p.Description, &p.Effect, &conditions); err == sql.ErrNoRows { - return nil, pkg.ErrNotFound - } else if err != nil { - return nil, errors.WithStack(err) - } - - p.Conditions = Conditions{} - if err := json.Unmarshal(conditions, &p.Conditions); err != nil { - return nil, errors.WithStack(err) - } - - subjects, err := getLinkedSQL(s.db, "ladon_policy_subject", id) - if err != nil { - return nil, err - } - permissions, err := getLinkedSQL(s.db, "ladon_policy_permission", id) - if err != nil { - return nil, err - } - resources, err := getLinkedSQL(s.db, "ladon_policy_resource", id) - if err != nil { - return nil, err - } - - p.Actions = permissions - p.Subjects = subjects - p.Resources = resources - return &p, nil -} - -// Delete removes a policy. -func (s *SQLManager) Delete(id string) error { - _, err := s.db.Exec(s.db.Rebind("DELETE FROM ladon_policy WHERE id=?"), id) - return errors.WithStack(err) -} - -// FindPoliciesForSubject returns Policies (an array of policy) for a given subject -func (s *SQLManager) FindPoliciesForSubject(subject string) (policies Policies, err error) { - find := func(query string, args ...interface{}) (ids []string, err error) { - rows, err := s.db.Query(query, args...) - if err == sql.ErrNoRows { - return nil, errors.Wrap(pkg.ErrNotFound, "") - } else if err != nil { - return nil, errors.WithStack(err) - } - defer rows.Close() - for rows.Next() { - var urn string - if err = rows.Scan(&urn); err != nil { - return nil, errors.WithStack(err) - } - ids = append(ids, urn) - } - return ids, nil - } - - var query string - switch s.db.DriverName() { - case "postgres", "pgx": - query = "SELECT policy FROM ladon_policy_subject WHERE $1 ~ ('^' || compiled || '$')" - case "mysql": - query = "SELECT policy FROM ladon_policy_subject WHERE ? REGEXP BINARY CONCAT('^', compiled, '$') GROUP BY policy" - } - - if query == "" { - return nil, errors.Errorf("driver %s not supported", s.db.DriverName()) - } - - subjects, err := find(query, subject) - if err != nil { - return policies, err - } - - for _, id := range subjects { - p, err := s.Get(id) - if err != nil { - return nil, err - } - policies = append(policies, p) - } - return policies, nil -} - -func getLinkedSQL(db *sqlx.DB, table, policy string) ([]string, error) { - urns := []string{} - rows, err := db.Query(db.Rebind(fmt.Sprintf("SELECT template FROM %s WHERE policy=?", table)), policy) - if err == sql.ErrNoRows { - return nil, errors.Wrap(pkg.ErrNotFound, "") - } else if err != nil { - return nil, errors.WithStack(err) - } - - defer rows.Close() - for rows.Next() { - var urn string - if err = rows.Scan(&urn); err != nil { - return []string{}, errors.WithStack(err) - } - urns = append(urns, urn) - } - return urns, nil -} - -func createLinkSQL(db *sqlx.DB, tx *sql.Tx, table string, p Policy, templates []string) error { - for _, template := range templates { - reg, err := compiler.CompileRegex(template, p.GetStartDelimiter(), p.GetEndDelimiter()) - - // Execute SQL statement - query := db.Rebind(fmt.Sprintf("INSERT INTO %s (policy, template, compiled) VALUES (?, ?, ?)", table)) - if _, err = tx.Exec(query, p.GetID(), template, reg.String()); err != nil { - if rb := tx.Rollback(); rb != nil { - return errors.WithStack(rb) - } - return errors.WithStack(err) - } - } - return nil -} diff --git a/manager_sql_test.go b/manager_sql_test.go new file mode 100644 index 0000000..7c521e4 --- /dev/null +++ b/manager_sql_test.go @@ -0,0 +1,161 @@ +package ladon_test + +import ( + "fmt" + "testing" + + _ "github.com/go-sql-driver/mysql" + _ "github.com/lib/pq" + "github.com/ory/ladon" + "github.com/pborman/uuid" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// This test is skipped because the method was deprecated +// +func TestFindPoliciesForSubject(t *testing.T) { + policies := []*ladon.DefaultPolicy{ + { + ID: uuid.New(), + Description: "description", + Subjects: []string{"sql<.*>match"}, + Effect: ladon.AllowAccess, + Resources: []string{"master", "user", "article"}, + Actions: []string{"create", "update", "delete"}, + Conditions: ladon.Conditions{ + "foo": &ladon.StringEqualCondition{ + Equals: "foo", + }, + }, + }, + { + ID: uuid.New(), + Description: "description", + Subjects: []string{"sqlmatch"}, + Effect: ladon.AllowAccess, + Resources: []string{"master", "user", "article"}, + Actions: []string{"create", "update", "delete"}, + Conditions: ladon.Conditions{ + "foo": &ladon.StringEqualCondition{ + Equals: "foo", + }, + }, + }, + { + ID: uuid.New(), + Description: "description", + Subjects: []string{}, + Effect: ladon.AllowAccess, + Resources: []string{"master", "user", "article"}, + Actions: []string{"create", "update", "delete"}, + Conditions: ladon.Conditions{ + "foo": &ladon.StringEqualCondition{ + Equals: "foo", + }, + }, + }, + { + ID: uuid.New(), + Description: "description", + Effect: ladon.AllowAccess, + Resources: []string{"master", "user", "article"}, + Actions: []string{"create", "update", "delete"}, + Conditions: ladon.Conditions{ + "foo": &ladon.StringEqualCondition{ + Equals: "foo", + }, + }, + }, + } + + for k, s := range map[string]ladon.Manager{ + "postgres": managers["postgres"], + "mysql": managers["mysql"], + } { + t.Run(fmt.Sprintf("manager=%s", k), func(t *testing.T) { + for _, c := range policies { + t.Run(fmt.Sprintf("create=%s", k), func(t *testing.T) { + require.NoError(t, s.Create(c)) + }) + } + + res, err := s.FindRequestCandidates(&ladon.Request{ + Subject: "sqlmatch", + Resource: "article", + Action: "create", + }) + require.NoError(t, err) + require.Len(t, res, 2) + + if policies[0].ID == res[0].GetID() { + assertPolicyEqual(t, policies[0], res[0]) + assertPolicyEqual(t, policies[1], res[1]) + } else { + assertPolicyEqual(t, policies[0], res[1]) + assertPolicyEqual(t, policies[1], res[0]) + } + + res, err = s.FindRequestCandidates(&ladon.Request{ + Subject: "sqlamatch", + Resource: "article", + Action: "create", + }) + + require.NoError(t, err) + require.Len(t, res, 1) + assertPolicyEqual(t, policies[0], res[0]) + }) + } +} + +func assertPolicyEqual(t *testing.T, expected, got ladon.Policy) { + assert.Equal(t, expected.GetID(), got.GetID()) + assert.Equal(t, expected.GetDescription(), got.GetDescription()) + assert.Equal(t, expected.GetEffect(), got.GetEffect()) + + // This won't work in the memory manager + //assert.NotNil(t, got.GetActions()) + //assert.NotNil(t, got.GetResources()) + //assert.NotNil(t, got.GetSubjects()) + + assert.NoError(t, testEq(expected.GetActions(), got.GetActions())) + assert.NoError(t, testEq(expected.GetResources(), got.GetResources())) + assert.NoError(t, testEq(expected.GetSubjects(), got.GetSubjects())) + assert.EqualValues(t, expected.GetConditions(), got.GetConditions()) +} + +func testEq(a, b []string) error { + + // We don't care about nil types + //if a == nil && b == nil { + // return true + //} + // + //if a == nil || b == nil { + // return false + //} + + if len(a) != len(b) { + return errors.Errorf("Length not equal: %v (%d) != %v (%d)", a, len(a), b, len(b)) + } + + var found bool + for i := range a { + found = false + + for y := range b { + if a[i] == b[y] { + found = true + break + } + } + + if !found { + return errors.Errorf("No match found: %s from %v in %v", i, a, b) + } + } + + return nil +} diff --git a/manager_test.go b/manager_test.go deleted file mode 100644 index 8957672..0000000 --- a/manager_test.go +++ /dev/null @@ -1,227 +0,0 @@ -package ladon - -import ( - "log" - "os" - "testing" - "time" - - _ "github.com/go-sql-driver/mysql" - _ "github.com/lib/pq" - "github.com/ory-am/common/pkg" - "github.com/pborman/uuid" - "github.com/stretchr/testify/assert" - - "github.com/ory-am/common/integration" - "github.com/stretchr/testify/require" - "golang.org/x/net/context" -) - -var managerPolicies = []*DefaultPolicy{ - { - ID: uuid.New(), - Description: "description", - Subjects: []string{"user", "anonymous"}, - Effect: AllowAccess, - Resources: []string{"article", "user"}, - Actions: []string{"create", "update"}, - Conditions: Conditions{}, - }, - { - ID: uuid.New(), - Description: "description", - Subjects: []string{}, - Effect: AllowAccess, - Resources: []string{""}, - Actions: []string{"view"}, - Conditions: Conditions{}, - }, - { - ID: uuid.New(), - Description: "description", - Subjects: []string{""}, - Effect: DenyAccess, - Resources: []string{"article", "user"}, - Actions: []string{"view"}, - Conditions: Conditions{ - "owner": &EqualsSubjectCondition{}, - }, - }, - { - ID: uuid.New(), - Description: "description", - Subjects: []string{"", "peter"}, - Effect: DenyAccess, - Resources: []string{".*"}, - Actions: []string{"disable"}, - Conditions: Conditions{ - "ip": &CIDRCondition{ - CIDR: "1234", - }, - "owner": &EqualsSubjectCondition{}, - }, - }, - { - ID: uuid.New(), - Description: "description", - Subjects: []string{"<.*>"}, - Effect: AllowAccess, - Resources: []string{""}, - Actions: []string{"view"}, - Conditions: Conditions{ - "ip": &CIDRCondition{ - CIDR: "1234", - }, - "owner": &EqualsSubjectCondition{}, - }, - }, - { - ID: uuid.New(), - Description: "description", - Subjects: []string{""}, - Effect: AllowAccess, - Resources: []string{""}, - Actions: []string{"view"}, - Conditions: Conditions{ - "ip": &CIDRCondition{ - CIDR: "1234", - }, - "owner": &EqualsSubjectCondition{}, - }, - }, -} - -var managers = map[string]Manager{} -var rethinkManager *RethinkManager - -func TestMain(m *testing.M) { - connectPG() - connectRDB() - connectMEM() - connectPG() - connectMySQL() - connectRedis() - - s := m.Run() - integration.KillAll() - os.Exit(s) -} - -func connectMEM() { - managers["memory"] = NewMemoryManager() -} - -func connectPG() { - var db = integration.ConnectToPostgres("ladon") - s := NewSQLManager(db, nil) - if err := s.CreateSchemas(); err != nil { - log.Fatalf("Could not create postgres schema: %v", err) - } - - managers["postgres"] = s -} - -func connectMySQL() { - var db = integration.ConnectToMySQL() - s := NewSQLManager(db, nil) - if err := s.CreateSchemas(); err != nil { - log.Fatalf("Could not create mysql schema: %v", err) - } - - managers["mysql"] = s -} - -func connectRDB() { - var session = integration.ConnectToRethinkDB("ladon", "policies") - rethinkManager = NewRethinkManager(session, "") - - rethinkManager.Watch(context.Background()) - time.Sleep(time.Second) - managers["rethink"] = rethinkManager -} - -func connectRedis() { - var db = integration.ConnectToRedis() - managers["redis"] = NewRedisManager(db, "") -} - -func TestColdStart(t *testing.T) { - assert.Nil(t, rethinkManager.Create(&DefaultPolicy{ID: "foo", Description: "description foo"})) - assert.Nil(t, rethinkManager.Create(&DefaultPolicy{ID: "bar", Description: "description bar"})) - - time.Sleep(time.Second / 2) - rethinkManager.Policies = make(map[string]Policy) - assert.Nil(t, rethinkManager.ColdStart()) - - c1, err := rethinkManager.Get("foo") - assert.Nil(t, err) - c2, err := rethinkManager.Get("bar") - assert.Nil(t, err) - - assert.NotEqual(t, c1, c2) - assert.Equal(t, "description foo", c1.GetDescription()) - assert.Equal(t, "description bar", c2.GetDescription()) - rethinkManager.Policies = make(map[string]Policy) -} - -func TestGetErrors(t *testing.T) { - for k, s := range managers { - _, err := s.Get(uuid.New()) - assert.EqualError(t, err, pkg.ErrNotFound.Error(), k) - - _, err = s.Get("asdf") - assert.NotNil(t, err) - } -} - -func TestCreateGetDelete(t *testing.T) { - for k, s := range managers { - for _, c := range managerPolicies { - err := s.Create(c) - assert.Nil(t, err, "%s: %s", k, err) - time.Sleep(time.Millisecond * 100) - - get, err := s.Get(c.GetID()) - assert.Nil(t, err, "%s: %s", k, err) - pkg.AssertObjectKeysEqual(t, c, get, "Description", "Subjects", "Resources", "Effect", "Actions") - assert.EqualValues(t, c.Conditions, get.GetConditions(), "%s", k) - } - - for _, c := range managerPolicies { - assert.Nil(t, s.Delete(c.GetID()), k) - _, err := s.Get(c.GetID()) - assert.NotNil(t, err, "%s: %s", k, err) - } - } -} - -func TestFindPoliciesForSubject(t *testing.T) { - for k, s := range managers { - for _, c := range managerPolicies { - require.Nil(t, s.Create(c), k) - } - - policies, err := s.FindPoliciesForSubject("user") - assert.Nil(t, err) - assert.Equal(t, 4, len(policies), k) - - policies, err = s.FindPoliciesForSubject("peter") - assert.Nil(t, err) - assert.Equal(t, 3, len(policies), k) - - // Test case-sensitive matching - policies, err = s.FindPoliciesForSubject("User") - assert.Nil(t, err) - assert.Equal(t, 1, len(policies), k) - - // Test case-sensitive matching - policies, err = s.FindPoliciesForSubject("taKwq") - assert.Nil(t, err) - assert.Equal(t, 1, len(policies), k) - - // Test user without policy - policies, err = s.FindPoliciesForSubject("foobar") - assert.Nil(t, err) - assert.Equal(t, 1, len(policies), k) - } -} diff --git a/matcher.go b/matcher.go new file mode 100644 index 0000000..fd85b1e --- /dev/null +++ b/matcher.go @@ -0,0 +1,7 @@ +package ladon + +type matcher interface { + Matches(p Policy, haystack []string, needle string) (matches bool, error error) +} + +var DefaultMatcher = NewRegexpMatcher(512) diff --git a/matcher_regexp.go b/matcher_regexp.go new file mode 100644 index 0000000..d3cdd93 --- /dev/null +++ b/matcher_regexp.go @@ -0,0 +1,79 @@ +package ladon + +import ( + "regexp" + "strings" + + "github.com/hashicorp/golang-lru" + "github.com/ory-am/common/compiler" + "github.com/pkg/errors" +) + +func NewRegexpMatcher(size int) *RegexpMatcher { + if size <= 0 { + size = 512 + } + + // golang-lru only returns an error if the cache's size is 0. This, we can safely ignore this error. + cache, _ := lru.New(size) + return &RegexpMatcher{ + Cache: cache, + } +} + +type RegexpMatcher struct { + *lru.Cache + + C map[string]*regexp.Regexp +} + +func (m *RegexpMatcher) get(pattern string) *regexp.Regexp { + if val, ok := m.Cache.Get(pattern); !ok { + return nil + } else if reg, ok := val.(*regexp.Regexp); !ok { + return nil + } else { + return reg + } +} + +func (m *RegexpMatcher) set(pattern string, reg *regexp.Regexp) { + m.Cache.Add(pattern, reg) +} + +// Matches a needle with an array of regular expressions and returns true if a match was found. +func (m *RegexpMatcher) Matches(p Policy, haystack []string, needle string) (bool, error) { + var reg *regexp.Regexp + var err error + for _, h := range haystack { + + // This means that the current haystack item does not contain a regular expression + if strings.Count(h, string(p.GetStartDelimiter())) == 0 { + // If we have a simple string match, we've got a match! + if h == needle { + return true, nil + } + + // Not string match, but also no regexp, continue with next haystack item + continue + } + + if reg = m.get(h); reg != nil { + if reg.MatchString(needle) { + return true, nil + } + continue + } + + reg, err = compiler.CompileRegex(h, p.GetStartDelimiter(), p.GetEndDelimiter()) + if err != nil { + return false, errors.WithStack(err) + } + + m.set(h, reg) + if reg.MatchString(needle) { + return true, nil + } + } + return false, nil +} diff --git a/policy_test.go b/policy_test.go index 5c3b8a1..442821b 100644 --- a/policy_test.go +++ b/policy_test.go @@ -2,9 +2,10 @@ package ladon_test import ( "encoding/json" + "fmt" "testing" - . "github.com/ory-am/ladon" + . "github.com/ory/ladon" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -35,17 +36,18 @@ func TestHasAccess(t *testing.T) { } func TestMarshalling(t *testing.T) { - for _, c := range policyCases { - var cc = DefaultPolicy{ - Conditions: make(Conditions), - } - data, err := json.Marshal(c) - RequireError(t, false, err) + for k, c := range policyCases { + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + var cc = DefaultPolicy{ + Conditions: make(Conditions), + } + data, err := json.Marshal(c) + RequireError(t, false, err) - t.Logf("Got data: %s\n", data) - json.Unmarshal(data, &cc) - RequireError(t, false, err) - assert.Equal(t, c, &cc) + json.Unmarshal(data, &cc) + RequireError(t, false, err) + assert.Equal(t, c, &cc) + }) } } diff --git a/warden_test.go b/warden_test.go index ac64fae..daadb4f 100644 --- a/warden_test.go +++ b/warden_test.go @@ -1,17 +1,18 @@ package ladon_test import ( + "fmt" "testing" - "github.com/pkg/errors" "github.com/golang/mock/gomock" - . "github.com/ory-am/ladon" + . "github.com/ory/ladon" + "github.com/pkg/errors" "github.com/stretchr/testify/assert" ) func TestWardenIsGranted(t *testing.T) { ctrl := gomock.NewController(t) - m := newMockManager(ctrl) + m := NewMockManager(ctrl) defer ctrl.Finish() w := &Ladon{ @@ -28,7 +29,7 @@ func TestWardenIsGranted(t *testing.T) { description: "should fail because no policies are found for peter", r: &Request{Subject: "peter"}, setup: func() { - m.EXPECT().FindPoliciesForSubject("peter").Return(Policies{}, nil) + m.EXPECT().FindRequestCandidates(gomock.Eq(&Request{Subject: "peter"})).Return(Policies{}, nil) }, expectErr: true, }, @@ -36,7 +37,7 @@ func TestWardenIsGranted(t *testing.T) { description: "should fail because lookup failure when accessing policies for peter", r: &Request{Subject: "peter"}, setup: func() { - m.EXPECT().FindPoliciesForSubject("peter").Return(Policies{}, errors.New("asdf")) + m.EXPECT().FindRequestCandidates(gomock.Eq(&Request{Subject: "peter"})).Return(Policies{}, errors.New("asdf")) }, expectErr: true, }, @@ -48,7 +49,11 @@ func TestWardenIsGranted(t *testing.T) { Action: "view", }, setup: func() { - m.EXPECT().FindPoliciesForSubject("peter").Return(Policies{ + m.EXPECT().FindRequestCandidates(gomock.Eq(&Request{ + Subject: "peter", + Resource: "articles:1234", + Action: "view", + })).Return(Policies{ &DefaultPolicy{ Subjects: []string{""}, Effect: AllowAccess, @@ -67,7 +72,11 @@ func TestWardenIsGranted(t *testing.T) { Action: "view", }, setup: func() { - m.EXPECT().FindPoliciesForSubject("ken").Return(Policies{ + m.EXPECT().FindRequestCandidates(gomock.Eq(&Request{ + Subject: "ken", + Resource: "articles:1234", + Action: "view", + })).Return(Policies{ &DefaultPolicy{ Subjects: []string{""}, Effect: AllowAccess, @@ -86,7 +95,11 @@ func TestWardenIsGranted(t *testing.T) { Action: "view", }, setup: func() { - m.EXPECT().FindPoliciesForSubject("ken").Return(Policies{ + m.EXPECT().FindRequestCandidates(gomock.Eq(&Request{ + Subject: "ken", + Resource: "printers:321", + Action: "view", + })).Return(Policies{ &DefaultPolicy{ Subjects: []string{"ken", "peter"}, Effect: AllowAccess, @@ -105,7 +118,11 @@ func TestWardenIsGranted(t *testing.T) { Action: "view", }, setup: func() { - m.EXPECT().FindPoliciesForSubject("ken").Return(Policies{ + m.EXPECT().FindRequestCandidates(gomock.Eq(&Request{ + Subject: "ken", + Resource: "articles:321", + Action: "view", + })).Return(Policies{ &DefaultPolicy{ Subjects: []string{"ken", "peter"}, Effect: AllowAccess, @@ -124,7 +141,11 @@ func TestWardenIsGranted(t *testing.T) { Action: "foo", }, setup: func() { - m.EXPECT().FindPoliciesForSubject("ken").Return(Policies{ + m.EXPECT().FindRequestCandidates(gomock.Eq(&Request{ + Subject: "ken", + Resource: "articles:321", + Action: "foo", + })).Return(Policies{ &DefaultPolicy{ Subjects: []string{"ken", "peter"}, Effect: AllowAccess, @@ -136,13 +157,14 @@ func TestWardenIsGranted(t *testing.T) { expectErr: false, }, } { - c.setup() - err := w.IsAllowed(c.r) - if c.expectErr { - assert.NotNil(t, err, "(%d) %s", k, c.description) - } else { - assert.Nil(t, err, "(%d) %s", k, c.description) - } - t.Logf("Passed test case (%d) %s", k, c.description) + t.Run(fmt.Sprintf("case=%d/description=%s", k, c.description), func(t *testing.T) { + c.setup() + err := w.IsAllowed(c.r) + if c.expectErr { + assert.NotNil(t, err) + } else { + assert.Nil(t, err) + } + }) } } diff --git a/xxx_manager_sql_migrator_test.go b/xxx_manager_sql_migrator_test.go new file mode 100644 index 0000000..e2098bb --- /dev/null +++ b/xxx_manager_sql_migrator_test.go @@ -0,0 +1,53 @@ +package ladon_test + +import ( + "fmt" + "testing" + + "github.com/ory/ladon" + "github.com/stretchr/testify/require" +) + +func TestSQLManagerMigrateFromMajor0Minor6ToMajor0Minor7(t *testing.T) { + // Setting up the Migration is easy: + // var db = getSqlDatabaseFromSomewhere() + // s := NewSQLManager(db, nil) + // + // if err := s.CreateSchemas(); err != nil { + // log.Fatalf("Could not create mysql schema: %v", err) + // } + // + // migrator = &SQLManagerMigrateFromMajor0Minor6ToMajor0Minor7{ + // DB:db, + // SQLManager:s, + // } + + for k, s := range map[string]ladon.ManagerMigrator{ + "postgres": migrators["postgres"], + "mysql": migrators["mysql"], + } { + t.Run(fmt.Sprintf("manager=%s", k), func(t *testing.T) { + + // This create part is only necessary to populate the data store with some values. If you + // migrate you won't need this + for _, c := range managerPolicies { + t.Run(fmt.Sprintf("create=%s", k), func(t *testing.T) { + require.NoError(t, s.Create(c)) + }) + } + + require.NoError(t, s.Migrate()) + + for _, c := range managerPolicies { + t.Run(fmt.Sprintf("fetch=%s", k), func(t *testing.T) { + get, err := s.GetManager().Get(c.GetID()) + require.NoError(t, err) + + assertPolicyEqual(t, c, get) + + require.NoError(t, s.GetManager().Delete(c.GetID())) + }) + } + }) + } +}