Skip to content

Commit

Permalink
Merge pull request #25 from mikespook/master
Browse files Browse the repository at this point in the history
Stable master to v2
  • Loading branch information
mikespook authored Jan 9, 2019
2 parents 9acee98 + a989d0e commit 8842175
Show file tree
Hide file tree
Showing 10 changed files with 284 additions and 74 deletions.
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

0 comments on commit 8842175

Please sign in to comment.