Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stable master to v2 #25

Merged
merged 16 commits into from
Jan 9, 2019
Merged
14 changes: 14 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,17 @@ coverage: fmt
test: fmt
go vet ./...
go test ./...

pprof:
go test -c
./gorbac.test -test.cpuprofile cpu.prof -test.bench .
go tool pprof gorbac.test cpu.prof
rm cpu.prof gorbac.test

flamegraph:
go test -c
./gorbac.test -test.cpuprofile cpu.prof -test.bench .
go-torch ./gorbac.test cpu.prof
xdg-open torch.svg
sleep 5
rm cpu.prof gorbac.test torch.svg
19 changes: 12 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ Version

Currently, goRBAC has two versions:

[Version 1](https://github.com/mikespook/gorbac/tree/v1.dev) is the original design which will only mantain to fix bugs.
[Version 1](https://github.com/mikespook/gorbac/tree/v1.dev) is the original design which will only be mantained to fix bugs.

[Version 2](https://github.com/mikespook/gorbac/tree/v2.dev) is the newly design which will continually mantain with a stable API.
[Version 2](https://github.com/mikespook/gorbac/tree/v2.dev) is the new design which will be continually mantained with a stable API.

While [the master branch](https://github.com/mikespook/gorbac) will be under developing with new API and can be changed without notice.
[The master branch](https://github.com/mikespook/gorbac) will be under development with a new API and can be changed without notice.


Install
Expand All @@ -42,7 +42,7 @@ Install the package:
Usage
=====

Despite you can adjust the RBAC instance anytime and it's absolutely safe, the library is designed for using with two phases:
Although you can adjust the RBAC instance anytime and it's absolutely safe, the library is designed for use with two phases:

1. Preparing

Expand Down Expand Up @@ -85,7 +85,7 @@ Add the permissions to roles:

Also, you can implement `gorbac.Role` and `gorbac.Permission` for your own data structure.

After initailization, add the roles to the RBAC instance:
After initialization, add the roles to the RBAC instance:

rbac.Add(rA)
rbac.Add(rB)
Expand Down Expand Up @@ -117,15 +117,20 @@ And there are some built-in util-functions:
[AnyGranted](https://godoc.org/github.com/mikespook/gorbac#AnyGranted),
[AllGranted](https://godoc.org/github.com/mikespook/gorbac#AllGranted).
Please [open an issue](https://github.com/mikespook/gorbac/issues/new)
for the new built-in requriement.
for the new built-in requirement.

E.g.:

rbac.SetParent("role-c", "role-a")
if err := gorbac.InherCircle(rbac); err != nil {
fmt.Println("A circle inheratance ocurred.")
fmt.Println("A circle inheratance occurred.")
}

Persistence
-----------

The most asked question is how to persist the goRBAC instance. Please check the post [HOW TO PERSIST GORBAC INSTANCE](https://mikespook.com/2017/04/how-to-persist-gorbac-instance/) for the details.

Patches
=======

Expand Down
1 change: 1 addition & 0 deletions examples/persistence/inher.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"chief-editor":["editor","photographer"]}
122 changes: 122 additions & 0 deletions examples/persistence/persistence.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package main

import (
"encoding/json"
"log"
"os"

"github.com/mikespook/gorbac"
)

func init() {
log.SetFlags(log.LstdFlags | log.Lshortfile)
}

func LoadJson(filename string, v interface{}) error {
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close()
return json.NewDecoder(f).Decode(v)
}

func SaveJson(filename string, v interface{}) error {
f, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer f.Close()
return json.NewEncoder(f).Encode(v)
}

func main() {
// map[RoleId]PermissionIds
var jsonRoles map[string][]string
// map[RoleId]ParentIds
var jsonInher map[string][]string
// Load roles information
if err := LoadJson("roles.json", &jsonRoles); err != nil {
log.Fatal(err)
}
// Load inheritance information
if err := LoadJson("inher.json", &jsonInher); err != nil {
log.Fatal(err)
}
rbac := gorbac.New()
permissions := make(gorbac.Permissions)

// Build roles and add them to goRBAC instance
for rid, pids := range jsonRoles {
role := gorbac.NewStdRole(rid)
for _, pid := range pids {
_, ok := permissions[pid]
if !ok {
permissions[pid] = gorbac.NewStdPermission(pid)
}
role.Assign(permissions[pid])
}
rbac.Add(role)
}
// Assign the inheritance relationship
for rid, parents := range jsonInher {
if err := rbac.SetParents(rid, parents); err != nil {
log.Fatal(err)
}
}
// Check if `editor` can add text
if rbac.IsGranted("editor", permissions["add-text"], nil) {
log.Println("Editor can add text")
}
// Check if `chief-editor` can add text
if rbac.IsGranted("chief-editor", permissions["add-text"], nil) {
log.Println("Chief editor can add text")
}
// Check if `photographer` can add text
if !rbac.IsGranted("photographer", permissions["add-text"], nil) {
log.Println("Photographer can't add text")
}
// Check if `nobody` can add text
// `nobody` is not exist in goRBAC at the moment
if !rbac.IsGranted("nobody", permissions["read-text"], nil) {
log.Println("Nobody can't read text")
}
// Add `nobody` and assign `read-text` permission
nobody := gorbac.NewStdRole("nobody")
permissions["read-text"] = gorbac.NewStdPermission("read-text")
nobody.Assign(permissions["read-text"])
rbac.Add(nobody)
// Check if `nobody` can read text again
if rbac.IsGranted("nobody", permissions["read-text"], nil) {
log.Println("Nobody can read text")
}

// Persist the change
// map[RoleId]PermissionIds
jsonOutputRoles := make(map[string][]string)
// map[RoleId]ParentIds
jsonOutputInher := make(map[string][]string)
SaveJsonHandler := func(r gorbac.Role, parents []string) error {
// WARNING: Don't use gorbac.RBAC instance in the handler,
// otherwise it causes deadlock.
permissions := make([]string, 0)
for _, p := range r.(*gorbac.StdRole).Permissions() {
permissions = append(permissions, p.ID())
}
jsonOutputRoles[r.ID()] = permissions
jsonOutputInher[r.ID()] = parents
return nil
}
if err := gorbac.Walk(rbac, SaveJsonHandler); err != nil {
log.Fatalln(err)
}

// Save roles information
if err := SaveJson("new-roles.json", &jsonOutputRoles); err != nil {
log.Fatal(err)
}
// Save inheritance information
if err := SaveJson("new-inher.json", &jsonOutputInher); err != nil {
log.Fatal(err)
}
}
1 change: 1 addition & 0 deletions examples/persistence/roles.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"editor":["add-text","edit-text","insert-photo"],"photographer":["add-photo","edit-photo"],"chief-editor":["del-text","del-photo"]}
67 changes: 49 additions & 18 deletions helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,67 @@ package gorbac

import "fmt"

// InherCircle returns an error when detecting any circle inheritance.
func InherCircle(rbac *RBAC) error {
// WalkHandler is a function defined by user to handle role
type WalkHandler func(Role, []string) error

// Walk passes each Role to WalkHandler
func Walk(rbac *RBAC, h WalkHandler) (err error) {
if h == nil {
return
}
rbac.mutex.Lock()
defer rbac.mutex.Unlock()
for id := range rbac.roles {
var parents []string
r := rbac.roles[id]
for parent := range rbac.parents[id] {
parents = append(parents, parent)
}
if err := h(r, parents); err != nil {
return err
}
}
return
}

skipped := make(map[string]struct{})
// InherCircle returns an error when detecting any circle inheritance.
func InherCircle(rbac *RBAC) (err error) {
rbac.mutex.Lock()

skipped := make(map[string]struct{}, len(rbac.roles))
var stack []string

for id := range rbac.roles {
if err := dfs(rbac, id, skipped, stack); err != nil {
return err
if err = dfs(rbac, id, skipped, stack); err != nil {
break
}
}
return nil
rbac.mutex.Unlock()
return err
}

var (
ErrFoundCircle = fmt.Errorf("Found circle")
)

// https://en.wikipedia.org/wiki/Depth-first_search
func dfs(rbac *RBAC, id string, skipped map[string]struct{}, stack []string) error {
if _, ok := skipped[id]; ok {
return nil
}
for _, item := range stack {
if item == id {
return fmt.Errorf("Found circle: %s", stack)
return ErrFoundCircle
}
}
if len(rbac.parents[id]) == 0 {
stack = make([]string, 0)
parents := rbac.parents[id]
if len(parents) == 0 {
stack = nil
skipped[id] = empty
return nil
}
stack = append(stack, id)
for pid := range rbac.parents[id] {
for pid := range parents {
if err := dfs(rbac, pid, skipped, stack); err != nil {
return err
}
Expand All @@ -43,26 +72,28 @@ func dfs(rbac *RBAC, id string, skipped map[string]struct{}, stack []string) err

// AnyGranted checks if any role has the permission.
func AnyGranted(rbac *RBAC, roles []string, permission Permission,
assert AssertionFunc) bool {
assert AssertionFunc) (rslt bool) {
rbac.mutex.Lock()
defer rbac.mutex.Unlock()
for _, role := range roles {
if rbac.isGranted(role, permission, assert) {
return true
rslt = true
break
}
}
return false
rbac.mutex.Unlock()
return rslt
}

// AllGranted checks if all roles have the permission.
func AllGranted(rbac *RBAC, roles []string, permission Permission,
assert AssertionFunc) bool {
assert AssertionFunc) (rslt bool) {
rbac.mutex.Lock()
defer rbac.mutex.Unlock()
for _, role := range roles {
if !rbac.isGranted(role, permission, assert) {
return false
rslt = true
break
}
}
return true
rbac.mutex.Unlock()
return !rslt
}
26 changes: 26 additions & 0 deletions helper_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package gorbac

import (
"errors"
"testing"
)

Expand Down Expand Up @@ -65,6 +66,31 @@ func TestAnyGranted(t *testing.T) {

}

func TestWalk(t *testing.T) {
if err := Walk(rbac, nil); err != nil {
t.Errorf("Unexpected error: %s", err)
}
h := func(r Role, parents []string) error {
t.Logf("Role: %v", r.ID())
permissions := make([]string, 0)
for _, p := range r.(*StdRole).Permissions() {
permissions = append(permissions, p.ID())
}
t.Logf("Permission: %v", permissions)
t.Logf("Parents: %v", parents)
return nil
}
if err := Walk(rbac, h); err != nil {
t.Errorf("Unexpected error: %s", err)
}
he := func(r Role, parents []string) error {
return errors.New("Expected error")
}
if err := Walk(rbac, he); err == nil {
t.Errorf("Expected error, got nil")
}
}

func BenchmarkInherCircle(b *testing.B) {
rbac = New()
rbac.Add(rA)
Expand Down
Loading