Skip to content

Commit

Permalink
all: address performance issues and refactor structure (#58)
Browse files Browse the repository at this point in the history
* core: addressing performance issues

This patch introduces a very simple cache (needs to be replaced with LRU and needs a lock) and also a check if regex is used, and if not a simple string match is done.

* LRU

* LRU

* more bench tests

* sql all

* finalize fetching everything from sql

* improve sql adapter performance

* fix tests

* all: improve performance of regexp matches

This patch introduces an LRU cache for compiled regular expressions. Manager implementations have been moved to their own packages. The SQL manager has been improved for better performance.

* #58 (comment)

* implement has_regex

* implement has_regex

* implement has_regex

* implement has_regex

* implement has_regex

* vendor: resolves glide issues

* readme: examples for new manager instantiation

* readme: examples for new manager instantiation

* vendor: updates ory-am/common to 0.2.2

* all: goimports and remove redis/rethinkdb

* all: get tests passing

* all: goimports

* sql: implement migration

* all: move to new org

* vendor: add glide files

* all: resolve broken import references, clean up

* all: goimports

* docs: update history file

* manager: implement and test GetAll function
  • Loading branch information
arekkas committed May 3, 2017
1 parent c7c7754 commit 214af7a
Show file tree
Hide file tree
Showing 29 changed files with 1,631 additions and 1,003 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
*.iml
vendor/
sqlite-test.db
tests/
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ env:

language: go

go_import_path: github.com/ory-am/ladon
go_import_path: github.com/ory/ladon

go:
- tip
Expand Down
69 changes: 69 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# History of breaking changes

<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->

- [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)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->


## 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`
93 changes: 50 additions & 43 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

---

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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() {
// ...
Expand All @@ -465,85 +465,64 @@ 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)

// ...
}
```

**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"

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.
`ladon.Ladon`, which is the default implementation for the `ladon.Warden` interface defines `ladon.Ladon.IsAllowed()` which
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() {
// ...
Expand All @@ -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 .`
Expand All @@ -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
```
91 changes: 91 additions & 0 deletions benchmark_warden_test.go
Original file line number Diff line number Diff line change
@@ -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
}
6 changes: 3 additions & 3 deletions condition_string_pairs_equal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}

Expand Down

0 comments on commit 214af7a

Please sign in to comment.