diff --git a/Makefile b/Makefile index 333ffcc..1bc24ce 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/README.md b/README.md index b4d722b..87599d5 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 @@ -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) @@ -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 ======= diff --git a/examples/persistence/inher.json b/examples/persistence/inher.json new file mode 100644 index 0000000..64df58c --- /dev/null +++ b/examples/persistence/inher.json @@ -0,0 +1 @@ +{"chief-editor":["editor","photographer"]} \ No newline at end of file diff --git a/examples/persistence/persistence.go b/examples/persistence/persistence.go new file mode 100644 index 0000000..c1751f6 --- /dev/null +++ b/examples/persistence/persistence.go @@ -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) + } +} diff --git a/examples/persistence/roles.json b/examples/persistence/roles.json new file mode 100644 index 0000000..3d2c8c4 --- /dev/null +++ b/examples/persistence/roles.json @@ -0,0 +1 @@ +{"editor":["add-text","edit-text","insert-photo"],"photographer":["add-photo","edit-photo"],"chief-editor":["del-text","del-photo"]} \ No newline at end of file diff --git a/helper.go b/helper.go index 905d971..29790c0 100644 --- a/helper.go +++ b/helper.go @@ -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 } @@ -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 } diff --git a/helper_test.go b/helper_test.go index 6fa957f..97d6e45 100644 --- a/helper_test.go +++ b/helper_test.go @@ -1,6 +1,7 @@ package gorbac import ( + "errors" "testing" ) @@ -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) diff --git a/rbac.go b/rbac.go index 793e28d..cd80cea 100644 --- a/rbac.go +++ b/rbac.go @@ -34,19 +34,17 @@ type AssertionFunc func(*RBAC, string, Permission) bool // RBAC object, in most cases it should be used as a singleton. type RBAC struct { - mutex sync.RWMutex - roles Roles - permissions Permissions - parents map[string]map[string]struct{} + mutex sync.RWMutex + roles Roles + parents map[string]map[string]struct{} } // New returns a RBAC structure. // The default role structure will be used. func New() *RBAC { return &RBAC{ - roles: make(Roles), - permissions: make(Permissions), - parents: make(map[string]map[string]struct{}), + roles: make(Roles), + parents: make(map[string]map[string]struct{}), } } @@ -131,59 +129,62 @@ func (rbac *RBAC) RemoveParent(id string, parent string) error { } // Add a role `r`. -func (rbac *RBAC) Add(r Role) error { +func (rbac *RBAC) Add(r Role) (err error) { rbac.mutex.Lock() - defer rbac.mutex.Unlock() - if _, ok := rbac.roles[r.ID()]; ok { - return ErrRoleExist + if _, ok := rbac.roles[r.ID()]; !ok { + rbac.roles[r.ID()] = r + } else { + err = ErrRoleExist } - rbac.roles[r.ID()] = r - return nil + rbac.mutex.Unlock() + return } // Remove the role by `id`. -func (rbac *RBAC) Remove(id string) error { +func (rbac *RBAC) Remove(id string) (err error) { rbac.mutex.Lock() - defer rbac.mutex.Unlock() - if _, ok := rbac.roles[id]; !ok { - return ErrRoleNotExist - } - delete(rbac.roles, id) - for rid, parents := range rbac.parents { - if rid == id { - delete(rbac.parents, rid) - continue - } - for parent := range parents { - if parent == id { - delete(rbac.parents[rid], id) - break + if _, ok := rbac.roles[id]; ok { + delete(rbac.roles, id) + for rid, parents := range rbac.parents { + if rid == id { + delete(rbac.parents, rid) + continue + } + for parent := range parents { + if parent == id { + delete(rbac.parents[rid], id) + break + } } } + } else { + err = ErrRoleNotExist } - return nil + rbac.mutex.Unlock() + return } // Get the role by `id` and a slice of its parents id. -func (rbac *RBAC) Get(id string) (Role, []string, error) { +func (rbac *RBAC) Get(id string) (r Role, parents []string, err error) { rbac.mutex.RLock() - defer rbac.mutex.RUnlock() - r, ok := rbac.roles[id] - if !ok { - return nil, nil, ErrRoleNotExist - } - var parents []string - for parent := range rbac.parents[id] { - parents = append(parents, parent) + var ok bool + if r, ok = rbac.roles[id]; ok { + for parent := range rbac.parents[id] { + parents = append(parents, parent) + } + } else { + err = ErrRoleNotExist } - return r, parents, nil + rbac.mutex.RUnlock() + return } // IsGranted tests if the role `id` has Permission `p` with the condition `assert`. -func (rbac *RBAC) IsGranted(id string, p Permission, assert AssertionFunc) bool { +func (rbac *RBAC) IsGranted(id string, p Permission, assert AssertionFunc) (rslt bool) { rbac.mutex.RLock() - defer rbac.mutex.RUnlock() - return rbac.isGranted(id, p, assert) + rslt = rbac.isGranted(id, p, assert) + rbac.mutex.RUnlock() + return } func (rbac *RBAC) isGranted(id string, p Permission, assert AssertionFunc) bool { diff --git a/rbac_test.go b/rbac_test.go index 9e89e37..69d507a 100644 --- a/rbac_test.go +++ b/rbac_test.go @@ -126,6 +126,10 @@ func TestRbacPermission(t *testing.T) { if rbac.IsGranted("role-c", pB, nil) { t.Fatalf("role-c should not have %s because of the unbinding with role-b", pB) } + + if rbac.IsGranted("role-a", nil, nil) { + t.Fatal("role-a should not have nil permission") + } } func BenchmarkRbacGranted(b *testing.B) { diff --git a/role.go b/role.go index 08aa099..1862a28 100644 --- a/role.go +++ b/role.go @@ -41,38 +41,43 @@ func (role *StdRole) ID() string { // Assign a permission to the role. func (role *StdRole) Assign(p Permission) error { role.Lock() - defer role.Unlock() role.permissions[p.ID()] = p + role.Unlock() return nil } // Permit returns true if the role has specific permission. -func (role *StdRole) Permit(p Permission) bool { +func (role *StdRole) Permit(p Permission) (rslt bool) { + if p == nil { + return false + } + role.RLock() - defer role.RUnlock() for _, rp := range role.permissions { if rp.Match(p) { - return true + rslt = true + break } } - return false + role.RUnlock() + return } // Revoke the specific permission. func (role *StdRole) Revoke(p Permission) error { role.Lock() - defer role.Unlock() delete(role.permissions, p.ID()) + role.Unlock() return nil } // Permissions returns all permissions into a slice. func (role *StdRole) Permissions() []Permission { role.RLock() - defer role.RUnlock() result := make([]Permission, 0, len(role.permissions)) for _, p := range role.permissions { result = append(result, p) } + role.RUnlock() return result }