Skip to content

trevex/zanzigo

Repository files navigation

Go Reference Go Report Card

zanzigo

The zanzigo-library provides building blocks for creating your own Zanzibar-esque authorization service. If you are unfamiliar with Google's Zanzibar, check out zanzibar.academy by Auth0.

This respository also includes a server-implementation using gRPC/ConnectRPC.

Install

go get -u github.com/trevex/zanzigo

Getting started

First you will need an authorization-model, which defines the ruleset of your relation-based access control. The structure of ObjectMap is inspired by warrant.

model, err := zanzigo.NewModel(zanzigo.ObjectMap{
    "user": zanzigo.RelationMap{},
	"group": zanzigo.RelationMap{
		"member": zanzigo.Rule{},
	},
	"folder": zanzigo.RelationMap{
		"owner": zanzigo.Rule{},
		"editor": zanzigo.Rule{
			InheritIf: "owner",
		},
		"viewer": zanzigo.Rule{
			InheritIf: "editor",
		},
	},
	"doc": zanzigo.RelationMap{
		"parent": zanzigo.Rule{},
		"owner": zanzigo.Rule{
			InheritIf:    "owner",
			OfType:       "folder",
			WithRelation: "parent",
		},
		"editor": zanzigo.AnyOf(
			zanzigo.Rule{InheritIf: "owner"},
			zanzigo.Rule{
				InheritIf:    "editor",
				OfType:       "folder",
				WithRelation: "parent",
			},
		),
		"viewer": zanzigo.AnyOf(
			zanzigo.Rule{InheritIf: "editor"},
			zanzigo.Rule{
				InheritIf:    "viewer",
				OfType:       "folder",
				WithRelation: "parent",
			},
		),
	},
})

Next, you will need a storage-implementation, check out the Storage-section of this document for details. For simplicity, let's use Postgres and assume databaseURL is defined:

if err := postgres.RunMigrations(databaseURL); err != nil {
    // ...
}

storage, err := postgres.NewPostgresStorage(databaseURL)
if err != nil {
    // ...
}

To traverse the authorization-model and check a permission, you need a resolver:

resolver, err := zanzigo.NewResolver(model, storage, 16)
if err != nil {
    // ...
}

// Alternatively construct zanzigo.Tuple directly instead of using the string-format from the paper.
result, err := resolver.Check(context.Background(), zanzigo.TupleString("doc:mydoc#viewer@user:myuser"))

That is it!

For more thorough examples, check out the examples/-folder in the repository. Details regarding the storage- and resolver-implementation can be found below or in the generated documentation.

Storage

Postgres

Make sure the database migrations ran before creating the storage-backend:

if err := postgres.RunMigrations(databaseURL); err != nil {
    log.Fatalf("Could not migrate db: %s", err)
}

The Postgres implementation comes in two flavors. One is using queries:

storage, err := postgres.NewPostgresStorage(databaseURL)

The queries are prepared and executed at the same time using UNION ALL, so no parallelism of the resolver is required. The database will traverse all checks of a certain depth at the same time for us.

The second flavor is using stored Postgres-functions:

storage, err := postgres.NewPostgresStorage(databaseURL, postgres.UseFunctions())

This storage-implementation prepares Postgres-functions, which will traverse the authorization-model. This means only a single query is issues calling a particular function and directly return the result of the check.

Both flavors have advantages and disadvantages, but are compatible, so swapping is possible at any time.

SQLite3

Alternatively SQLite3 can be used as follow:

dbfile := "./sqlite.db" # URL parameters from mattn/go-sqlite3 can be used
if err := sqlite3.RunMigrations(dbfile); err != nil {
    log.Fatalf("Could not migrate db: %s", err)
}
storage, err := sqlite3.NewSQLiteStorage(dbfile)

Which storage implementation to use?

This really depends on which underlying database will fulfill your needs, so familiarize yourself with their trade-offs using the upstream documentation.

You might also want to consider the following on day 2:

  1. You can use Litestream or LiteFS to scale beyond a single replica, e.g. multiple read-replicas of SQLite3.
  2. Both function and query-based flavors of the Postgres implementation should work with Neon, while only query-based approach is expected to be compatible with CockroachDB.

Benchmark

A benchmark was undertaken on Google Cloud. All the code and the raw results can be found in ./bench.

The code also includes some micro-benchmarks, that are run locally, which result in the following results on my laptop:

goos: linux
goarch: amd64
pkg: github.com/trevex/zanzigo/storage/postgres
cpu: 11th Gen Intel(R) Core(TM) i7-1185G7 @ 3.00GHz
BenchmarkPostgres
BenchmarkPostgres/queries
BenchmarkPostgres/queries/indirect_nested_4
BenchmarkPostgres/queries/indirect_nested_4-8         	    5533	    190032 ns/op	   20151 B/op	     112 allocs/op
BenchmarkPostgres/queries/direct
BenchmarkPostgres/queries/direct-8                    	   19274	     61679 ns/op	    5096 B/op	      40 allocs/op
BenchmarkPostgres/functions
BenchmarkPostgres/functions/indirect_nested_4
BenchmarkPostgres/functions/indirect_nested_4-8       	    9423	    127550 ns/op	     802 B/op	      13 allocs/op
BenchmarkPostgres/functions/direct
BenchmarkPostgres/functions/direct-8                  	   19890	     60806 ns/op	     801 B/op	      13 allocs/op
PASS
goos: linux
goarch: amd64
pkg: github.com/trevex/zanzigo/storage/sqlite3
cpu: 11th Gen Intel(R) Core(TM) i7-1185G7 @ 3.00GHz
BenchmarkSQLite3
BenchmarkSQLite3/queries
BenchmarkSQLite3/queries/indirect_nested_4
BenchmarkSQLite3/queries/indirect_nested_4-8         	   22962	     52081 ns/op	   13294 B/op	      70 allocs/op
BenchmarkSQLite3/queries/direct
BenchmarkSQLite3/queries/direct-8                    	   65703	     18410 ns/op	    3077 B/op	      26 allocs/op
PASS

Development

Persistent Postgres

During development it might make sense to persist data created by tests. You can specify a different database to use, by setting TEST_POSTGRES_DATABASE_URL environment variable.

For example start a postgres database in docker and run tests against it as follows:

docker run --name postgres -e POSTGRES_USER=zanzigo -e POSTGRES_PASSWORD=zanzigo -e POSTGRES_DB=zanzigo -e listen_addresses='*' --net=host -d postgres:15.4
TEST_POSTGRES_DATABASE_URL="postgres://zanzigo:zanzigo@127.0.0.1:5432/zanzigo?sslmode=disable" go test -v ./...

If you want to inspect the database it might be helpful to run pgAdmin4:

docker run -d --name pgadmin  -e PGADMIN_DEFAULT_EMAIL='test@test.local' -e PGADMIN_DEFAULT_PASSWORD=secret -e PGADMIN_CONFIG_SERVER_MODE='False' -e PGADMIN_LISTEN_PORT=8080 --net=host dpage/pgadmin4

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published