Commit 5fa96e3d authored by khoipham's avatar khoipham

use go-pg v9 and casbin v2

parent 7fe62e47
Go-pg Adapter
====
# Go-pg Adapter
Go-pg Adapter is the [Go-pg](https://github.com/go-pg/pg) adapter for [Casbin](https://github.com/casbin/casbin). With this library, Casbin can load policy from PostgreSQL or save policy to it.
## Installation
go get github.com/MonedaCacao/casbin-pg-adapter
## Env Variables
Populate .env with necessary environment variable values:
$ nano .env
```
DATABASE_ADDRESSES=
DATABASE_USER_NAME=
DATABSE_USER_PASSORD=
```
go get github.com/pckhoi/casbin-pg-adapter
## Simple Postgres Example
......@@ -25,7 +12,9 @@ DATABSE_USER_PASSORD=
package main
import (
pgadapter "github.com/MonedaCacao/casbin-pg-adapter"
"os"
pgadapter "github.com/pckhoi/casbin-pg-adapter"
"github.com/casbin/casbin"
)
......@@ -33,7 +22,7 @@ func main() {
// Initialize a Go-pg adapter and use it in a Casbin enforcer:
// The adapter will use the Postgres database named "casbin".
// If it doesn't exist, the adapter will create it automatically.
a, _ := pgadapter.NewAdapter() // Your driver and data source.
a, _ := pgadapter.NewAdapter(os.Getenv("PG_CONN")) // Your driver and data source.
// Or you can use an existing DB "abc" like this:
// The adapter will use the table named "casbin_rule".
......@@ -58,7 +47,7 @@ func main() {
## Getting Help
- [Casbin](https://github.com/casbin/casbin)
- [Casbin](https://github.com/casbin/casbin)
## License
......
package pgadapter
import (
"github.com/MonedaCacao/casbin-pg-adapter/config"
"github.com/casbin/casbin/model"
"github.com/casbin/casbin/persist"
"github.com/go-pg/pg"
"github.com/go-pg/pg/orm"
"log"
"fmt"
"strings"
"github.com/casbin/casbin/v2/model"
"github.com/casbin/casbin/v2/persist"
"github.com/go-pg/pg/v9"
"github.com/go-pg/pg/v9/orm"
"golang.org/x/crypto/sha3"
)
const (
......@@ -16,7 +17,7 @@ const (
// CasbinRule represents a rule in Casbin.
type CasbinRule struct {
Id int
ID string
PType string
V0 string
V1 string
......@@ -35,74 +36,55 @@ type Adapter struct {
func finalizer(a *Adapter) {}
// NewAdapter is the constructor for Adapter.
// The adapter will automatically create a DB named "casbin"
func NewAdapter() (*Adapter, error) {
a := Adapter{}
// Open the DB, create it if not existed.
err := a.open()
// arg should be a PostgreS URL string or of type *pg.Options
// The adapter will create a DB named "casbin" if it doesn't exist
func NewAdapter(arg interface{}) (*Adapter, error) {
db, err := createCasbinDatabase(arg)
if err != nil {
return nil, err
return nil, fmt.Errorf("pgadapter.NewAdapter: %v", err)
}
return &a, nil
}
func (a *Adapter) createDatabase() error {
var err error
var db *pg.DB
env := config.GetEnvVariables()
cfg := config.GetConfig(*env)
a := &Adapter{db: db}
db = pg.Connect(&pg.Options{
Addr: cfg.DatabaseAddresses,
User: cfg.DatabaseUsername,
Password: cfg.DatabseUserPassord,
})
defer db.Close()
_, err = db.Exec("CREATE DATABASE casbin")
if err != nil {
log.Println("can't create database", err)
return err
if err := a.createTable(); err != nil {
return nil, fmt.Errorf("pgadapter.NewAdapter: %v", err)
}
return nil
return a, nil
}
func (a *Adapter) open() error {
func createCasbinDatabase(arg interface{}) (*pg.DB, error) {
var opts *pg.Options
var err error
var db *pg.DB
env := config.GetEnvVariables()
cfg := config.GetConfig(*env)
err = a.createDatabase()
if err != nil {
panic(err)
if connURL, ok := arg.(string); ok {
opts, err = pg.ParseURL(connURL)
if err != nil {
return nil, err
}
} else {
opts, ok = arg.(*pg.Options)
if !ok {
return nil, fmt.Errorf("must pass in a PostgreS URL string or an instance of *pg.Options, received %T instead", arg)
}
}
db = pg.Connect(&pg.Options{
Addr: cfg.DatabaseAddresses,
User: cfg.DatabaseUsername,
Password: cfg.DatabseUserPassord,
Database: "casbin",
})
db := pg.Connect(opts)
defer db.Close()
a.db = db
_, err = db.Exec("CREATE DATABASE casbin")
db.Close()
opts.Database = "casbin"
db = pg.Connect(opts)
return a.createTable()
return db, nil
}
func (a *Adapter) close() error {
err := a.db.Close()
if err != nil {
return err
// Close close database connection
func (a *Adapter) Close() error {
if a != nil && a.db != nil {
return a.db.Close()
}
a.db = nil
return nil
}
......@@ -119,15 +101,6 @@ func (a *Adapter) createTable() error {
return nil
}
func (a *Adapter) dropTable() error {
err := a.db.DropTable(&CasbinRule{}, &orm.DropTableOptions{})
if err != nil {
return err
}
return nil
}
func loadPolicyLine(line *CasbinRule, model model.Model) {
const prefixLine = ", "
var sb strings.Builder
......@@ -199,20 +172,16 @@ func savePolicyLine(ptype string, rule []string) *CasbinRule {
line.V5 = rule[5]
}
data := strings.Join(append([]string{ptype}, rule...), ",")
sum := make([]byte, 64)
sha3.ShakeSum128(sum, []byte(data))
line.ID = fmt.Sprintf("%x", sum)
return line
}
// SavePolicy saves policy to database.
func (a *Adapter) SavePolicy(model model.Model) error {
err := a.dropTable()
if err != nil {
return err
}
err = a.createTable()
if err != nil {
return err
}
var lines []*CasbinRule
for ptype, ast := range model["p"] {
......@@ -229,14 +198,18 @@ func (a *Adapter) SavePolicy(model model.Model) error {
}
}
err = a.db.Insert(&lines)
_, err := a.db.Model(&lines).
OnConflict("DO NOTHING").
Insert()
return err
}
// AddPolicy adds a policy rule to the storage.
func (a *Adapter) AddPolicy(sec string, ptype string, rule []string) error {
line := savePolicyLine(ptype, rule)
err := a.db.Insert(line)
_, err := a.db.Model(line).
OnConflict("DO NOTHING").
Insert()
return err
}
......
package pgadapter
import (
"github.com/casbin/casbin"
"github.com/casbin/casbin/util"
"log"
"os"
"testing"
"github.com/casbin/casbin/v2"
"github.com/casbin/casbin/v2/util"
"github.com/go-pg/pg/v9"
"github.com/stretchr/testify/suite"
)
func testGetPolicy(t *testing.T, e *casbin.Enforcer, res [][]string) {
t.Helper()
myRes := e.GetPolicy()
log.Print("Policy Got: ", myRes)
// AdapterTestSuite tests all functionalities of Adapter
type AdapterTestSuite struct {
suite.Suite
e *casbin.Enforcer
a *Adapter
}
func (s *AdapterTestSuite) testGetPolicy(res [][]string) {
myRes := s.e.GetPolicy()
s.Assert().True(util.Array2DEquals(res, myRes), "Policy Got: %v, supposed to be %v", myRes, res)
}
if !util.Array2DEquals(res, myRes) {
t.Error("Policy Got: ", myRes, ", supposed to be ", res)
}
func (s *AdapterTestSuite) dropCasbinDB() {
opts, err := pg.ParseURL(os.Getenv("PG_CONN"))
s.Require().NoError(err)
db := pg.Connect(opts)
defer db.Close()
_, err = db.Exec("DROP DATABASE casbin")
s.Require().NoError(err)
}
func initPolicy(t *testing.T) {
// Because the DB is empty at first,
// so we need to load the policy from the file adapter (.CSV) first.
e := casbin.NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv")
func (s *AdapterTestSuite) SetupTest() {
s.dropCasbinDB()
a, err := NewAdapter()
if err != nil {
panic(err)
}
var err error
s.a, err = NewAdapter(os.Getenv("PG_CONN"))
s.Require().NoError(err)
// This is a trick to save the current policy to the DB.
// We can't call e.SavePolicy() because the adapter in the enforcer is still the file adapter.
// The current policy means the policy in the Casbin enforcer (aka in memory).
err = a.SavePolicy(e.GetModel())
if err != nil {
panic(err)
}
// Clear the current policy.
e.ClearPolicy()
testGetPolicy(t, e, [][]string{})
// Load the policy from DB.
err = a.LoadPolicy(e.GetModel())
if err != nil {
panic(err)
}
testGetPolicy(t, e, [][]string{{"alice", "data1", "read"}, {"bob", "data2", "write"}, {"data2_admin", "data2", "read"}, {"data2_admin", "data2", "write"}})
}
s.e, err = casbin.NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv")
s.Require().NoError(err)
err = s.a.SavePolicy(s.e.GetModel())
s.Require().NoError(err)
func testSaveLoad(t *testing.T) {
// Initialize some policy in DB.
initPolicy(t)
// Note: you don't need to look at the above code
// if you already have a working DB with policy inside.
// Now the DB has policy, so we can provide a normal use case.
// Create an adapter and an enforcer.
// NewEnforcer() will load the policy automatically.
a, _ := NewAdapter()
e := casbin.NewEnforcer("examples/rbac_model.conf", a)
testGetPolicy(t, e, [][]string{{"alice", "data1", "read"}, {"bob", "data2", "write"}, {"data2_admin", "data2", "read"}, {"data2_admin", "data2", "write"}})
s.e, err = casbin.NewEnforcer("examples/rbac_model.conf", s.a)
s.Require().NoError(err)
}
func testAutoSave(t *testing.T) {
// Initialize some policy in DB.
initPolicy(t)
// Note: you don't need to look at the above code
// if you already have a working DB with policy inside.
func (s *AdapterTestSuite) TearDownTest() {
err := s.a.Close()
s.Require().NoError(err)
}
// Now the DB has policy, so we can provide a normal use case.
// Create an adapter and an enforcer.
// NewEnforcer() will load the policy automatically.
a, _ := NewAdapter()
e := casbin.NewEnforcer("examples/rbac_model.conf", a)
func (s *AdapterTestSuite) TestSaveLoad() {
s.testGetPolicy([][]string{{"alice", "data1", "read"}, {"bob", "data2", "write"}, {"data2_admin", "data2", "read"}, {"data2_admin", "data2", "write"}})
}
func (s *AdapterTestSuite) TestAutoSave() {
// AutoSave is enabled by default.
// Now we disable it.
e.EnableAutoSave(false)
s.e.EnableAutoSave(false)
// Because AutoSave is disabled, the policy change only affects the policy in Casbin enforcer,
// it doesn't affect the policy in the storage.
e.AddPolicy("alice", "data1", "write")
_, err := s.e.AddPolicy("alice", "data1", "write")
s.Require().NoError(err)
// Reload the policy from the storage to see the effect.
e.LoadPolicy()
err = s.e.LoadPolicy()
s.Require().NoError(err)
// This is still the original policy.
testGetPolicy(t, e, [][]string{{"alice", "data1", "read"}, {"bob", "data2", "write"}, {"data2_admin", "data2", "read"}, {"data2_admin", "data2", "write"}})
s.testGetPolicy([][]string{{"alice", "data1", "read"}, {"bob", "data2", "write"}, {"data2_admin", "data2", "read"}, {"data2_admin", "data2", "write"}})
// Now we enable the AutoSave.
e.EnableAutoSave(true)
s.e.EnableAutoSave(true)
// Because AutoSave is enabled, the policy change not only affects the policy in Casbin enforcer,
// but also affects the policy in the storage.
e.AddPolicy("alice", "data1", "write")
s.e.AddPolicy("alice", "data1", "write")
// Reload the policy from the storage to see the effect.
e.LoadPolicy()
s.e.LoadPolicy()
// The policy has a new rule: {"alice", "data1", "write"}.
testGetPolicy(t, e, [][]string{{"alice", "data1", "read"}, {"bob", "data2", "write"}, {"data2_admin", "data2", "read"}, {"data2_admin", "data2", "write"}, {"alice", "data1", "write"}})
testGetPolicy(t, e, [][]string{{"alice", "data1", "read"}, {"bob", "data2", "write"}, {"data2_admin", "data2", "read"}, {"data2_admin", "data2", "write"}, {"alice", "data1", "write"}})
s.testGetPolicy([][]string{{"alice", "data1", "read"}, {"bob", "data2", "write"}, {"data2_admin", "data2", "read"}, {"data2_admin", "data2", "write"}, {"alice", "data1", "write"}})
}
func TestAdapters(t *testing.T) {
// You can also use the following way to use an existing DB "abc":
testSaveLoad(t)
testAutoSave(t)
func TestAdapterTestSuite(t *testing.T) {
suite.Run(t, new(AdapterTestSuite))
}
package config
import (
"os"
"path/filepath"
log "github.com/inconshreveable/log15"
"github.com/spf13/viper"
)
// Config contains the config.yml settings
type Config struct {
DatabaseAddresses string
DatabaseUsername string
DatabseUserPassord string
Database string
}
// Env contains the environment variables
type Env struct {
Environment string
DatabasePassword string
ProjectRootPath string
}
// GetEnvVariables gets the environment variables and returns a new env struct
func GetEnvVariables() *Env {
viper.AutomaticEnv()
viper.SetDefault("go_env", "development")
viper.SetDefault("database_password", "")
// Get the current environment
environment := viper.GetString("go_env")
log.Info("Initializing", "ENVIROMENT", environment)
// Get the enviroment variables
log.Info("Obtaining env variables")
databasePassword := viper.GetString("database_password")
env := &Env{
Environment: environment,
DatabasePassword: databasePassword,
// Secret: secret,
}
return env
}
// GetConfig reads the configuration file and returns a new config
func GetConfig(env Env) *Config {
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath(".")
viper.AddConfigPath(env.ProjectRootPath)
log.Info("Reading config file")
err := viper.ReadInConfig()
if err != nil {
log.Error("Missing configuration file.", "Error:", err)
os.Exit(1)
}
var config Config
err = viper.UnmarshalKey(env.Environment, &config)
if err != nil {
panic("Unable to unmarshal config")
}
return &config
}
// GetTestingEnvVariables returns env variables for testing
func GetTestingEnvVariables() *Env {
wd, err := os.Getwd()
if err != nil {
panic(err)
}
// Since we are loading configuration files from the root dir, when running from main package
// this is fine but for testing we need to find the root dir
dir := filepath.Dir(wd)
for dir[len(dir)-24:] != "gopg-casbin-adapter" {
dir = filepath.Dir(dir)
}
return &Env{Environment: "test", ProjectRootPath: dir}
}
---
version: "3.4"
services:
postgres:
image: postgres:12
ports:
- 5432:5432
environment:
- POSTGRES_PASSWORD=password
go:
build:
context: ./go
depends_on:
- postgres
environment:
- PG_CONN=postgresql://postgres:password@postgres:5432/postgres?sslmode=disable
volumes:
- .:/app
- gopkg:/go/pkg
working_dir: /app
command:
- go
- test
- github.com/pckhoi/casbin-pg-adapter
volumes:
gopkg:
DATABASE_ADDRESSES=
DATABASE_USER_NAME=
DATABSE_USER_PASSORD=
\ No newline at end of file
module github.com/MonedaCacao/casbin-pg-adapter
module github.com/pckhoi/casbin-pg-adapter
go 1.12
require (
github.com/casbin/casbin v1.9.1
github.com/go-pg/pg v8.0.5+incompatible
github.com/inconshreveable/log15 v0.0.0-20180818164646-67afb5ed74ec
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/mattn/go-colorable v0.1.2 // indirect
github.com/onsi/ginkgo v1.8.0 // indirect
github.com/onsi/gomega v1.5.0 // indirect
github.com/spf13/viper v1.4.0
mellium.im/sasl v0.2.1 // indirect
github.com/casbin/casbin/v2 v2.1.2
github.com/go-pg/pg/v9 v9.1.0
github.com/stretchr/testify v1.4.0
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413
)
This diff is collapsed.
FROM golang:1.13-alpine
RUN apk add --no-cache make git curl build-base tar
RUN git clone https://github.com/go-delve/delve.git /go/src/github.com/go-delve/delve && \
cd /go/src/github.com/go-delve/delve && \
make install
RUN go get golang.org/x/tools/cmd/godoc
package main
import (
pgadapter "github.com/MonedaCacao/casbin-pg-adapter"
"os"
"github.com/casbin/casbin"
pgadapter "github.com/pckhoi/casbin-pg-adapter"
)
func main() {
// Initialize a Go-pg adapter and use it in a Casbin enforcer:
// The adapter will use the Postgres database named "casbin".
// If it doesn't exist, the adapter will create it automatically.
a, _ := pgadapter.NewAdapter() // Your driver and data source.
a, _ := pgadapter.NewAdapter(os.Getenv("PG_CONN")) // Your driver and data source.
// Or you can use an existing DB "abc" like this:
// The adapter will use the table named "casbin_rule".
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment