├── examples ├── blog_simple │ ├── article.go │ ├── roles.go │ ├── main.go │ └── README.md ├── blog_complex │ ├── article.go │ ├── roles.go │ ├── main.go │ └── README.md └── iam │ ├── iam.go │ └── README.md ├── string.go ├── glob.go ├── regex.go ├── role.go ├── string_test.go ├── policy_test.go ├── LICENSE ├── matcher.go ├── permission.go ├── regex_test.go ├── glob_test.go ├── policy.go └── README.md /examples/blog_simple/article.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // The Article struct holds some information about a blog article 4 | type Article struct { 5 | ArticleID string 6 | AuthorID string 7 | Text string 8 | } 9 | -------------------------------------------------------------------------------- /string.go: -------------------------------------------------------------------------------- 1 | package rbac 2 | 3 | // StringMatch returns a Matcher that returns true 4 | // if the target string matches s. 5 | func StringMatch(s string) Matcher { 6 | return func(target string) (bool, error) { 7 | return target == s, nil 8 | } 9 | } 10 | 11 | // NewStringPermission returns a Permission that uses StringMatchers for the specified action and target. 12 | func NewStringPermission(action, target string) Permission { 13 | return NewPermission(StringMatch(action), StringMatch(target)) 14 | } 15 | -------------------------------------------------------------------------------- /glob.go: -------------------------------------------------------------------------------- 1 | package rbac 2 | 3 | import "github.com/ryanuber/go-glob" 4 | 5 | // GlobMatch returns a Matcher that returns true 6 | // if the target glob matches the specified pattern. 7 | func GlobMatch(pattern string) Matcher { 8 | return func(target string) (bool, error) { 9 | return glob.Glob(pattern, target), nil 10 | } 11 | } 12 | 13 | // NewGlobPermission returns a Permission that uses GlobMatchers for the specified action and target patterns. 14 | func NewGlobPermission(actionPattern, targetPattern string) Permission { 15 | return NewPermission(GlobMatch(actionPattern), GlobMatch(targetPattern)) 16 | } 17 | -------------------------------------------------------------------------------- /regex.go: -------------------------------------------------------------------------------- 1 | package rbac 2 | 3 | import "regexp" 4 | 5 | // RegexMatch returns a Matcher that returns true 6 | // if the target regular expression matches the specified pattern. 7 | func RegexMatch(pattern string) Matcher { 8 | return func(target string) (bool, error) { 9 | return regexp.MatchString(pattern, target) 10 | } 11 | } 12 | 13 | // NewRegexPermission returns a Permission that uses RegexMatchers for the specified action and target patterns. 14 | func NewRegexPermission(actionPattern, targetPattern string) Permission { 15 | return NewPermission(RegexMatch(actionPattern), RegexMatch(targetPattern)) 16 | } 17 | -------------------------------------------------------------------------------- /examples/blog_simple/roles.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/zpatrick/rbac" 4 | 5 | // NewAdminRole returns a role with admin-level permissions 6 | func NewAdminRole() rbac.Role { 7 | return rbac.Role{ 8 | RoleID: "Admin", 9 | Permissions: []rbac.Permission{ 10 | rbac.NewGlobPermission("*", "*"), 11 | }, 12 | } 13 | } 14 | 15 | // NewGuestRole returns a role with guest-level permissions 16 | func NewGuestRole() rbac.Role { 17 | return rbac.Role{ 18 | RoleID: "Guest", 19 | Permissions: []rbac.Permission{ 20 | rbac.NewGlobPermission("ReadArticle", "*"), 21 | rbac.NewGlobPermission("RateArticle", "*"), 22 | }, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/blog_complex/article.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // The Article struct holds some information about a blog article 4 | type Article struct { 5 | ArticleID string 6 | AuthorID string 7 | Text string 8 | } 9 | 10 | // Articles returns all the articles in the blog 11 | func Articles() []Article { 12 | return []Article{ 13 | { 14 | ArticleID: "a1", 15 | AuthorID: "u1", 16 | Text: "Five-star WR Blake Miller signs letter of intent...", 17 | }, 18 | { 19 | ArticleID: "a2", 20 | AuthorID: "u2", 21 | Text: "Late in the fourth quarter, senior quarterback Riley...", 22 | }, 23 | { 24 | ArticleID: "a3", 25 | AuthorID: "u3", 26 | Text: "If last week's scrimmage is any indicator, this season...", 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /role.go: -------------------------------------------------------------------------------- 1 | package rbac 2 | 3 | // A Role is a grouping of permissions. 4 | type Role struct { 5 | RoleID string 6 | Permissions Permissions 7 | } 8 | 9 | // Can returns true if the Role is allowed to perform the action on the target. 10 | func (r Role) Can(action, target string) (bool, error) { 11 | return r.Permissions.Can(action, target) 12 | } 13 | 14 | // The Roles type is an adapter to allow helper functions to execute on a slice of Roles 15 | type Roles []Role 16 | 17 | // Can returns true if at least one of the roles in r allows the action on the target 18 | func (r Roles) Can(action, target string) (bool, error) { 19 | for _, role := range r { 20 | can, err := role.Can(action, target) 21 | if err != nil { 22 | return false, err 23 | } 24 | 25 | if can { 26 | return true, nil 27 | } 28 | } 29 | 30 | return false, nil 31 | } 32 | -------------------------------------------------------------------------------- /string_test.go: -------------------------------------------------------------------------------- 1 | package rbac 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestStringMatch(t *testing.T) { 11 | cases := map[string]map[string]bool{ 12 | "": { 13 | "": true, 14 | "alpha": false, 15 | "beta": false, 16 | }, 17 | "alpha": { 18 | "": false, 19 | "alpha": true, 20 | "beta": false, 21 | }, 22 | "beta": { 23 | "": false, 24 | "alpha": false, 25 | "beta": true, 26 | }, 27 | "charlie": { 28 | "": false, 29 | "alpha": false, 30 | "beta": false, 31 | }, 32 | } 33 | 34 | for pattern, inputs := range cases { 35 | matcher := StringMatch(pattern) 36 | for input, expected := range inputs { 37 | name := fmt.Sprintf("%s/%s", pattern, input) 38 | t.Run(name, func(t *testing.T) { 39 | result, err := matcher(input) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | assert.Equal(t, expected, result) 45 | }) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /policy_test.go: -------------------------------------------------------------------------------- 1 | package rbac 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestPolicyTemplate(t *testing.T) { 13 | t.Skip("TODO: read/write to/from a buffer") 14 | 15 | p := NewPolicyTemplate("Admin") 16 | p.AddPermission("glob", "*", "grid:*:$userID:*") 17 | p.AddPermission("glob", "read:*", "*") 18 | 19 | bytes, err := json.MarshalIndent(p, "", " ") 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | 24 | if err := ioutil.WriteFile("admin.json", bytes, 0644); err != nil { 25 | t.Fatal(err) 26 | } 27 | 28 | bytes, err = ioutil.ReadFile("admin.json") 29 | if err != nil { 30 | t.Fatal(err) 31 | } 32 | 33 | var policy PolicyTemplate 34 | if err := json.Unmarshal(bytes, &policy); err != nil { 35 | t.Fatal(err) 36 | } 37 | 38 | role, err := policy.Role(strings.NewReplacer("$userID", "u123")) 39 | if err != nil { 40 | t.Fatal(err) 41 | } 42 | 43 | can, err := role.Can("read:comment", "c123") 44 | if err != nil { 45 | t.Fatal(err) 46 | } 47 | 48 | assert.True(t, can) 49 | } 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Zack Patrick 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /matcher.go: -------------------------------------------------------------------------------- 1 | package rbac 2 | 3 | // A Matcher is a function that returns a bool representing 4 | // whether or not the target matches some pre-defined pattern. 5 | type Matcher func(target string) (bool, error) 6 | 7 | // MatchAny will convert a slice of Matchers into a single Matcher 8 | // that returns true if and only if at least one of the specified matchers returns true. 9 | func MatchAny(matchers ...Matcher) Matcher { 10 | return func(target string) (bool, error) { 11 | for _, matcher := range matchers { 12 | match, err := matcher(target) 13 | if err != nil { 14 | return false, err 15 | } 16 | 17 | if match { 18 | return true, nil 19 | } 20 | } 21 | 22 | return false, nil 23 | } 24 | } 25 | 26 | // MatchAll will convert a slice of Matchers into a single Matcher 27 | // that returns true if and only if all of the specified matchers returns true. 28 | func MatchAll(matchers ...Matcher) Matcher { 29 | return func(target string) (bool, error) { 30 | for _, matcher := range matchers { 31 | match, err := matcher(target) 32 | if err != nil { 33 | return false, err 34 | } 35 | 36 | if !match { 37 | return false, nil 38 | } 39 | } 40 | 41 | return true, nil 42 | } 43 | } 44 | 45 | // Anything is a Matcher that always returns true 46 | func Anything(target string) (bool, error) { 47 | return true, nil 48 | } 49 | -------------------------------------------------------------------------------- /examples/iam/iam.go: -------------------------------------------------------------------------------- 1 | package iam 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/zpatrick/rbac" 7 | ) 8 | 9 | // NewAdminRole returns a rbac.Role that can do any action on any target. 10 | func NewAdminRole() rbac.Role { 11 | return rbac.Role{ 12 | RoleID: "Admin", 13 | Permissions: []rbac.Permission{ 14 | rbac.NewGlobPermission("*", "*"), 15 | }, 16 | } 17 | } 18 | 19 | // NewReadOnlyRole returns a rbac.Role that can do any "read" action on any target. 20 | func NewReadOnlyRole() rbac.Role { 21 | return rbac.Role{ 22 | RoleID: "ReadOnly", 23 | Permissions: []rbac.Permission{ 24 | rbac.NewGlobPermission("read:*", "*"), 25 | }, 26 | } 27 | } 28 | 29 | // NewEC2AdminRole returns a rbac.Role that can do any action 30 | // as long as the target belongs to the "ec2" service. 31 | func NewEC2AdminRole() rbac.Role { 32 | return rbac.Role{ 33 | RoleID: "EC2Admin", 34 | Permissions: []rbac.Permission{ 35 | rbac.NewGlobPermission("*", "arn:aws:ec2:*"), 36 | }, 37 | } 38 | } 39 | 40 | // NewS3BucketReadOnlyRole returns a rbac.Role that can do any "read" action 41 | // as long as the target belongs to the specified S3 bucket. 42 | func NewS3BucketReadOnlyRole(bucket string) rbac.Role { 43 | return rbac.Role{ 44 | RoleID: "S3BucketReadOnly", 45 | Permissions: []rbac.Permission{ 46 | rbac.NewGlobPermission("read:*", fmt.Sprintf("arn:aws:s3:::%s*", bucket)), 47 | }, 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /permission.go: -------------------------------------------------------------------------------- 1 | package rbac 2 | 3 | // A Permission is a function that returns true if the action is allowed on the target 4 | type Permission func(action string, target string) (bool, error) 5 | 6 | // The Permissions type is an adapter to allow helper functions to execute on a slice of Permissions 7 | type Permissions []Permission 8 | 9 | // Can returns true if at least one of the permissions in p allows the action on the target 10 | func (p Permissions) Can(action string, target string) (bool, error) { 11 | for _, permission := range p { 12 | can, err := permission(action, target) 13 | if err != nil { 14 | return false, err 15 | } 16 | 17 | if can { 18 | return true, nil 19 | } 20 | } 21 | 22 | return false, nil 23 | } 24 | 25 | // NewPermission returns a Permission that will return true 26 | // if the actionMatcher returns true for the given action, and 27 | // if the targetMatcher returns true the given target. 28 | func NewPermission(actionMatcher, targetMatcher Matcher) Permission { 29 | return func(action string, target string) (bool, error) { 30 | actionMatch, err := actionMatcher(action) 31 | if err != nil { 32 | return false, err 33 | } 34 | 35 | if !actionMatch { 36 | return false, nil 37 | } 38 | 39 | return targetMatcher(target) 40 | } 41 | } 42 | 43 | // AllowAll is a Permission that always returns true 44 | func AllowAll(action, target string) (bool, error) { 45 | return true, nil 46 | } 47 | -------------------------------------------------------------------------------- /regex_test.go: -------------------------------------------------------------------------------- 1 | package rbac 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestRegexMatch(t *testing.T) { 11 | cases := map[string]map[string]bool{ 12 | "": { 13 | "": true, 14 | "alpha": true, 15 | "beta": true, 16 | "charlie": true, 17 | }, 18 | ".*": { 19 | "": true, 20 | "alpha": true, 21 | "beta": true, 22 | "charlie": true, 23 | }, 24 | "alpha": { 25 | "": false, 26 | "alpha": true, 27 | "beta": false, 28 | "charlie": false, 29 | }, 30 | "^a.*$": { 31 | "": false, 32 | "alpha": true, 33 | "beta": false, 34 | "charlie": false, 35 | }, 36 | "^.*a$": { 37 | "": false, 38 | "alpha": true, 39 | "beta": true, 40 | "charlie": false, 41 | }, 42 | "a": { 43 | "": false, 44 | "alpha": true, 45 | "beta": true, 46 | "charlie": true, 47 | }, 48 | "delta": { 49 | "": false, 50 | "alpha": false, 51 | "beta": false, 52 | "charlie": false, 53 | }, 54 | } 55 | 56 | for pattern, inputs := range cases { 57 | matcher := RegexMatch(pattern) 58 | for input, expected := range inputs { 59 | name := fmt.Sprintf("%s/%s", pattern, input) 60 | t.Run(name, func(t *testing.T) { 61 | result, err := matcher(input) 62 | if err != nil { 63 | t.Fatal(err) 64 | } 65 | 66 | assert.Equal(t, expected, result) 67 | }) 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /examples/blog_complex/roles.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/zpatrick/rbac" 7 | ) 8 | 9 | // NewAdminRole returns a role with admin-level permissions 10 | func NewAdminRole() rbac.Role { 11 | return rbac.Role{ 12 | RoleID: "Admin", 13 | Permissions: []rbac.Permission{ 14 | rbac.NewGlobPermission("*", "*"), 15 | }, 16 | } 17 | } 18 | 19 | // NewGuestRole returns a role with guest-level permissions 20 | func NewGuestRole() rbac.Role { 21 | return rbac.Role{ 22 | RoleID: "Guest", 23 | Permissions: []rbac.Permission{ 24 | rbac.NewGlobPermission("ReadArticle", "*"), 25 | rbac.NewGlobPermission("RateArticle", "*"), 26 | }, 27 | } 28 | } 29 | 30 | // NewMemberRole returns a role with member-level permissions 31 | func NewMemberRole(userID string) rbac.Role { 32 | return rbac.Role{ 33 | RoleID: fmt.Sprintf("Member(%s)", userID), 34 | Permissions: []rbac.Permission{ 35 | rbac.NewGlobPermission("CreateArticle", "*"), 36 | rbac.NewGlobPermission("ReadArticle", "*"), 37 | rbac.NewGlobPermission("RateArticle", "*"), 38 | rbac.NewPermission(rbac.GlobMatch("EditArticle"), ifArticleAuthor(userID)), 39 | rbac.NewPermission(rbac.GlobMatch("DeleteArticle"), ifArticleAuthor(userID)), 40 | }, 41 | } 42 | } 43 | 44 | // ifArticleAuthor returns a matcher that will only return true if 45 | // the article's author matches userID. 46 | func ifArticleAuthor(userID string) rbac.Matcher { 47 | return func(target string) (bool, error) { 48 | for _, article := range Articles() { 49 | if article.ArticleID == target { 50 | return article.AuthorID == userID, nil 51 | } 52 | } 53 | 54 | return false, nil 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /examples/blog_simple/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | "strings" 9 | "text/tabwriter" 10 | 11 | "github.com/zpatrick/rbac" 12 | ) 13 | 14 | const target = "a1" 15 | 16 | func main() { 17 | roleName := flag.String("role", "guest", "the role to use") 18 | flag.Parse() 19 | 20 | // assign a role 21 | var role rbac.Role 22 | switch r := strings.ToLower(*roleName); r { 23 | case "guest": 24 | role = NewGuestRole() 25 | case "admin": 26 | role = NewAdminRole() 27 | default: 28 | log.Fatalf("Role '%s' not recognized. Only 'guest' or 'admin' may be used.", r) 29 | } 30 | 31 | // calculate role permissions 32 | canCreate, err := role.Can("CreateArticle", "") 33 | if err != nil { 34 | log.Fatal(err) 35 | } 36 | 37 | canRead, err := role.Can("ReadArticle", target) 38 | if err != nil { 39 | log.Fatal(err) 40 | } 41 | 42 | canEdit, err := role.Can("EditArticle", target) 43 | if err != nil { 44 | log.Fatal(err) 45 | } 46 | 47 | canDelete, err := role.Can("DeleteArticle", target) 48 | if err != nil { 49 | log.Fatal(err) 50 | } 51 | 52 | canRate, err := role.Can("RateArticle", target) 53 | if err != nil { 54 | log.Fatal(err) 55 | } 56 | 57 | // print role permissions 58 | w := tabwriter.NewWriter(os.Stdout, 20, 4, 0, ' ', 0) 59 | fmt.Fprintf(w, "Role: %s\n", role.RoleID) 60 | fmt.Fprintln(w, "Action\tArticleID\tAllowed") 61 | fmt.Fprintln(w, "-----------------------------------------------") 62 | fmt.Fprintf(w, "CreateArticle\t-\t%t\n", canCreate) 63 | fmt.Fprintf(w, "ReadArticle\t%s\t%t\n", target, canRead) 64 | fmt.Fprintf(w, "EditArticle\t%s\t%t\n", target, canEdit) 65 | fmt.Fprintf(w, "DeleteArticle\t%s\t%t\n", target, canDelete) 66 | fmt.Fprintf(w, "RateArticle\t%s\t%t\n", target, canRate) 67 | w.Flush() 68 | } 69 | -------------------------------------------------------------------------------- /examples/blog_complex/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | "strings" 9 | "text/tabwriter" 10 | 11 | "github.com/zpatrick/rbac" 12 | ) 13 | 14 | func main() { 15 | roleName := flag.String("role", "guest", "the role to use") 16 | flag.Parse() 17 | 18 | // assign a role 19 | var role rbac.Role 20 | switch r := strings.ToLower(*roleName); r { 21 | case "guest": 22 | role = NewGuestRole() 23 | case "member": 24 | role = NewMemberRole(Articles()[0].AuthorID) 25 | case "admin": 26 | role = NewAdminRole() 27 | default: 28 | log.Fatalf("Role '%s' not recognized. Only 'guest', 'member', or 'admin' may be used.", r) 29 | } 30 | 31 | // print role permissions 32 | w := tabwriter.NewWriter(os.Stdout, 20, 4, 0, ' ', 0) 33 | fmt.Fprintf(w, "Role: %s\n", role.RoleID) 34 | fmt.Fprintln(w, "Action\tArticleID\tAuthorID\tAllowed") 35 | fmt.Fprintln(w, "-------------------------------------------------------------------") 36 | 37 | canCreate, err := role.Can("CreateArticle", "") 38 | if err != nil { 39 | log.Fatal(err) 40 | } 41 | 42 | fmt.Fprintf(w, "CreateArticle\t-\t-\t%t\n", canCreate) 43 | 44 | for _, article := range Articles() { 45 | canRead, err := role.Can("ReadArticle", article.ArticleID) 46 | if err != nil { 47 | log.Fatal(err) 48 | } 49 | 50 | canEdit, err := role.Can("EditArticle", article.ArticleID) 51 | if err != nil { 52 | log.Fatal(err) 53 | } 54 | 55 | canDelete, err := role.Can("DeleteArticle", article.ArticleID) 56 | if err != nil { 57 | log.Fatal(err) 58 | } 59 | 60 | canRate, err := role.Can("RateArticle", article.ArticleID) 61 | if err != nil { 62 | log.Fatal(err) 63 | } 64 | 65 | fmt.Fprintf(w, "ReadArticle\t%s\t%s\t%t\n", article.ArticleID, article.AuthorID, canRead) 66 | fmt.Fprintf(w, "EditArticle\t%s\t%s\t%t\n", article.ArticleID, article.AuthorID, canEdit) 67 | fmt.Fprintf(w, "DeleteArticle\t%s\t%s\t%t\n", article.ArticleID, article.AuthorID, canDelete) 68 | fmt.Fprintf(w, "RateArticle\t%s\t%s\t%t\n", article.ArticleID, article.AuthorID, canRate) 69 | w.Flush() 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /glob_test.go: -------------------------------------------------------------------------------- 1 | package rbac 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | /* 11 | func ExampleNewGlobPermission() { 12 | role := Role{ 13 | Permissions: []Permission{ 14 | NewGlobPermission("delete:user", "*Doe"), 15 | NewGlobPermission("read:*", "*"), 16 | NewGlobPermission("*", "user_123"), 17 | }, 18 | } 19 | 20 | fmt.Println(role.Can("read", "comment")) 21 | fmt.Println(role.Can("write", "books")) 22 | // Output: 23 | // [action: "delete:user"] [target: "John Doe"] => true 24 | // [action: "delete:user"] [target: "Jane Doe"] => true 25 | // [action: "delete:user"] [target: "John Smith"] => false 26 | // [action: "read:comment"] [target: "comment_123"] => true 27 | // [action: "read:article"] [target: "article_123"] => true 28 | // [action: "edit:user"] [target: "user_123"] => true 29 | // [action: "edit:user"] [target: "user_456"] => false 30 | // [action: "delete:user"] [target: "user_123"] => true 31 | } 32 | */ 33 | 34 | func TestGlobMatch(t *testing.T) { 35 | cases := map[string]map[string]bool{ 36 | "": { 37 | "": true, 38 | "alpha": false, 39 | "beta": false, 40 | "charlie": false, 41 | }, 42 | "*": { 43 | "": true, 44 | "alpha": true, 45 | "beta": true, 46 | "charlie": true, 47 | }, 48 | "alpha": { 49 | "": false, 50 | "alpha": true, 51 | "beta": false, 52 | "charlie": false, 53 | }, 54 | "a*": { 55 | "": false, 56 | "alpha": true, 57 | "beta": false, 58 | "charlie": false, 59 | }, 60 | "*a": { 61 | "": false, 62 | "alpha": true, 63 | "beta": true, 64 | "charlie": false, 65 | }, 66 | "*a*": { 67 | "": false, 68 | "alpha": true, 69 | "beta": true, 70 | "charlie": true, 71 | }, 72 | "delta": { 73 | "": false, 74 | "alpha": false, 75 | "beta": false, 76 | "charlie": false, 77 | }, 78 | } 79 | 80 | for pattern, inputs := range cases { 81 | matcher := GlobMatch(pattern) 82 | for input, expected := range inputs { 83 | name := fmt.Sprintf("%s/%s", pattern, input) 84 | t.Run(name, func(t *testing.T) { 85 | result, err := matcher(input) 86 | if err != nil { 87 | t.Fatal(err) 88 | } 89 | 90 | assert.Equal(t, expected, result) 91 | }) 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /examples/iam/README.md: -------------------------------------------------------------------------------- 1 | # IAM Example 2 | This examples shows how to use `rbac` to implement a permissions model similar to [AWS IAM](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html#access_policies-json) policies. 3 | The permission model makes use of the following patterns: 4 | * Each **action** is a string with the following format: `"action_type:object_type"`, e.g. `"list:users"` or `"delete:comment"`. 5 | * Each **target** is a unique identifier for the specified `object_type` in the **action** string (where applicable). 6 | IAM uses their concept of [ARNs](https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html), which works well in this model as ARNs typically contain metadata about the object they represent. 7 | A typical ARN has the following pattern: 8 | ``` 9 | arn:aws::::: 10 | ``` 11 | 12 | ## Roles 13 | This section contains some example `rbac.Role` objects that can be created by using these patterns. 14 | 15 | #### Administrator 16 | This `rbac.Role` can do any action on any target. 17 | 18 | ```go 19 | func NewAdminRole() rbac.Role { 20 | return rbac.Role{ 21 | RoleID: "Admin", 22 | Permissions: []rbac.Permission{ 23 | rbac.NewGlobPermission("*", "*"), 24 | }, 25 | } 26 | } 27 | ``` 28 | 29 | #### ReadOnly 30 | This `rbac.Role` can do any `read` action on any target. 31 | 32 | ```go 33 | func NewReadOnlyRole() rbac.Role { 34 | return rbac.Role{ 35 | RoleID: "Admin", 36 | Permissions: []rbac.Permission{ 37 | rbac.NewGlobPermission("read:*", "*"), 38 | }, 39 | } 40 | } 41 | ``` 42 | 43 | #### EC2Admin 44 | This `rbac.Role` can do any action as long as the target belongs to the `ec2` service. 45 | 46 | ```go 47 | func NewEC2AdminRole() rbac.Role { 48 | return rbac.Role{ 49 | RoleID: "EC2Admin", 50 | Permissions: []rbac.Permission{ 51 | rbac.NewGlobPermission("*", "arn:aws:ec2:*"), 52 | }, 53 | } 54 | } 55 | ``` 56 | 57 | #### S3BucketReadOnly 58 | This `rbac.Role` can do any `read` action as long as the target belongs to the specified S3 bucket. 59 | 60 | ```go 61 | func NewS3BucketReadOnlyRole(bucket string) rbac.Role { 62 | return rbac.Role{ 63 | RoleID: "S3BucketReadOnly", 64 | Permissions: []rbac.Permission{ 65 | rbac.NewGlobPermission("read:*", fmt.Sprintf("arn:aws:s3:::%s*", bucket)), 66 | }, 67 | } 68 | } 69 | ``` 70 | -------------------------------------------------------------------------------- /policy.go: -------------------------------------------------------------------------------- 1 | package rbac 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | // A PermissionConstructor is a function that creates a new Permission 10 | // from the specified action and target strings. 11 | type PermissionConstructor func(action, target string) Permission 12 | 13 | // DefaultPermissionConstructors returns a mapping of constructor names to PermissionConstructor functions 14 | // for each of the builtin PermissionConstructors: 15 | // "glob": NewGlobPermission 16 | // "regex": NewRegexPermission 17 | // "string": NewStringPermission 18 | func DefaultPermissionConstructors() map[string]PermissionConstructor { 19 | return map[string]PermissionConstructor{ 20 | "glob": NewGlobPermission, 21 | "regex": NewRegexPermission, 22 | "string": NewStringPermission, 23 | } 24 | } 25 | 26 | // A PermissionTemplate holds information about a permission in templated format. 27 | type PermissionTemplate struct { 28 | Constructor string `json:"constructor"` 29 | Action string `json:"action"` 30 | Target string `json:"target"` 31 | } 32 | 33 | // A PolicyTemplate holds information about a Role in a templated format. 34 | // This format can be encoded to and from JSON. 35 | type PolicyTemplate struct { 36 | RoleID string `json:"role_id"` 37 | PermissionTemplates []PermissionTemplate `json:"permissions"` 38 | constructors map[string]PermissionConstructor 39 | } 40 | 41 | // NewPolicyTemplate generates a new PolicyTemplate with the specified roleID and default constructors. 42 | func NewPolicyTemplate(roleID string) *PolicyTemplate { 43 | return &PolicyTemplate{ 44 | RoleID: roleID, 45 | PermissionTemplates: []PermissionTemplate{}, 46 | constructors: DefaultPermissionConstructors(), 47 | } 48 | } 49 | 50 | // AddPermission adds a new PermissionTemplate to p.PermissionTemplates. 51 | func (p *PolicyTemplate) AddPermission(constructor, action, target string) { 52 | p.PermissionTemplates = append(p.PermissionTemplates, PermissionTemplate{constructor, action, target}) 53 | } 54 | 55 | // SetConstructor updates the mapping of a constructor name to a PermissionConstructor. 56 | // If a mapping for the specified same name already exists, it will be overwritten. 57 | func (p *PolicyTemplate) SetConstructor(name string, constructor PermissionConstructor) { 58 | p.constructors[name] = constructor 59 | } 60 | 61 | // DeleteConstructor will remove the constructor mapping at the specified name if it exists. 62 | func (p *PolicyTemplate) DeleteConstructor(name string) { 63 | if _, ok := p.constructors[name]; ok { 64 | delete(p.constructors, name) 65 | } 66 | } 67 | 68 | // Role converts the PolicyTemplate to a Role. 69 | // Replacer can be used to replace variables within the Action and Target fields in the PermissionTemplates. 70 | // An error will be returned if a PermissionTemplate.Constructor does not have a corresponding PermissionConstructor. 71 | func (p *PolicyTemplate) Role(replacer *strings.Replacer) (*Role, error) { 72 | role := &Role{ 73 | RoleID: p.RoleID, 74 | Permissions: make(Permissions, len(p.PermissionTemplates)), 75 | } 76 | 77 | for i, permissionTemplate := range p.PermissionTemplates { 78 | constructor, ok := p.constructors[permissionTemplate.Constructor] 79 | if !ok { 80 | return nil, fmt.Errorf("No constructor set for '%s'", permissionTemplate.Constructor) 81 | } 82 | 83 | action := replacer.Replace(permissionTemplate.Action) 84 | target := replacer.Replace(permissionTemplate.Target) 85 | role.Permissions[i] = constructor(action, target) 86 | } 87 | 88 | return role, nil 89 | } 90 | 91 | // UnmarshalJSON allows a *PolicyTemplate to implement the json.Unmarshaler interface. 92 | // We do this to set the default constructors on p after the unmarshalling. 93 | func (p *PolicyTemplate) UnmarshalJSON(data []byte) error { 94 | type Alias PolicyTemplate 95 | aux := &struct { 96 | *Alias 97 | }{ 98 | Alias: (*Alias)(p), 99 | } 100 | 101 | if err := json.Unmarshal(data, &aux); err != nil { 102 | return err 103 | } 104 | 105 | p.constructors = DefaultPermissionConstructors() 106 | return nil 107 | } 108 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RBAC 2 | 3 | [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/zpatrick/rbac/blob/master/LICENSE) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/zpatrick/rbac)](https://goreportcard.com/report/github.com/zpatrick/rbac) 5 | [![Go Doc](https://godoc.org/github.com/zpatrick/rbac?status.svg)](https://godoc.org/github.com/zpatrick/rbac) 6 | 7 | ## Overview 8 | RBAC is a package that makes it easy to implement Role Based Access Control (RBAC) models in Go applications. 9 | 10 | ## Download 11 | To download this package, run: 12 | ``` 13 | go get github.com/zpatrick/rbac 14 | ``` 15 | 16 | ## Getting Started 17 | This section will go over some of the basic concepts and an example of how to use `rbac` in an application. 18 | For more advanced usage, please see the [examples](/examples) directory. 19 | 20 | 21 | * **Action**: An action is a string that represents some desired operation. 22 | Actions are typically expressed as a verb or a verb-object combination, but it is ultimately up to the user how actions are expressed. 23 | Some examples are: `"Upvote"`, `"ReadArticle"`, or `"EditComment"`. 24 | * **Target**: A target is a string that represents what the action is trying to operate on. 25 | Targets are typically expressed as an object's unique identifier, but it is ultimately up to the user how targets are expressed. 26 | An example is passing an `articleID` as the target for a `"ReadArticle"` action. 27 | Not all actions require a target. 28 | * **Matcher**: A [matcher](https://godoc.org/github.com/zpatrick/rbac#Matcher) is a function that returns a bool representing whether or not the target matches some pre-defined pattern. 29 | This repo comes with some builtin matchers: 30 | [GlobMatch](https://godoc.org/github.com/zpatrick/rbac#GlobMatch), 31 | [RegexMatch](https://godoc.org/github.com/zpatrick/rbac#RegexMatch), 32 | and [StringMatch](https://godoc.org/github.com/zpatrick/rbac#StringMatch). 33 | Please see the [complex blog](/examples/blog_complex) example to see how one can implement custom matchers for their applications. 34 | * **Permission**: A [permission](https://godoc.org/github.com/zpatrick/rbac#Permission) is a function that takes an action and a target, and returns true if and only if the action is allowed on the target. 35 | A permission should always allow (as opposed to deny) action(s) to be made on target(s), since nothing is allowed by default. 36 | * **Role**: A [role](https://godoc.org/github.com/zpatrick/rbac#Role) is essentially a grouping of permissions. 37 | The [`role.Can`](https://godoc.org/github.com/zpatrick/rbac#Role.Can) function should be used to determine whether or not a role can do an action on a target. 38 | A role is only allowed to do something if it has at least one permission that allows it. 39 | 40 | ## Usage 41 | ```go 42 | package main 43 | 44 | import ( 45 | "fmt" 46 | 47 | "github.com/zpatrick/rbac" 48 | ) 49 | 50 | func main() { 51 | roles := []rbac.Role{ 52 | { 53 | RoleID: "Adult", 54 | Permissions: []rbac.Permission{ 55 | rbac.NewGlobPermission("watch", "*"), 56 | }, 57 | }, 58 | { 59 | RoleID: "Teenager", 60 | Permissions: []rbac.Permission{ 61 | rbac.NewGlobPermission("watch", "pg-13"), 62 | rbac.NewGlobPermission("watch", "g"), 63 | }, 64 | }, 65 | { 66 | RoleID: "Child", 67 | Permissions: []rbac.Permission{ 68 | rbac.NewGlobPermission("watch", "g"), 69 | }, 70 | }, 71 | } 72 | 73 | for _, role := range roles { 74 | fmt.Println("Role:", role.RoleID) 75 | for _, rating := range []string{"g", "pg-13", "r"} { 76 | canWatch, _ := role.Can("watch", rating) 77 | fmt.Printf("Can watch %s? %t\n", rating, canWatch) 78 | } 79 | } 80 | } 81 | 82 | ``` 83 | 84 | Output: 85 | ```console 86 | Role: Adult 87 | Can watch g? true 88 | Can watch pg-13? true 89 | Can watch r? true 90 | Role: Teenager 91 | Can watch g? true 92 | Can watch pg-13? true 93 | Can watch r? false 94 | Role: Child 95 | Can watch g? true 96 | Can watch pg-13? false 97 | Can watch r? false 98 | ``` 99 | 100 | ## License 101 | This work is published under the MIT license. 102 | 103 | Please see the `LICENSE` file for details. 104 | -------------------------------------------------------------------------------- /examples/blog_simple/README.md: -------------------------------------------------------------------------------- 1 | # Simple Blog Example 2 | This example shows how one can use `rbac` to manage permissions for a simple blog application. 3 | The application requires the following roles and permissions: 4 | * The **Guest** role can view and rate any article. 5 | * The **Admin** role can create, read, edit, delete, and rate any article. 6 | 7 | | Role | Create Article | Read Article | Edit Article | Delete Article | Rate Article | 8 | |-------|----------------|--------------|----------------|----------------|--------------| 9 | | Guest | - | Allow | - | - | Allow | 10 | | Admin | Allow | Allow | Allow | Allow | Allow | 11 | 12 | 13 | ## Creating the Roles 14 | The [roles.go](/examples/blog_simple/roles.go) file shows how one can implement this permission set. 15 | 16 | ### Admin Role 17 | Since the **Admin** role is allowed to do any action (`CreateArticle`, `ReadArticle`, `EditArticle`, `DeleteArticle`, and `RateArticle`), on any target (e.g. on any article), we can define that role's permissions in the following way: 18 | 19 | ```go 20 | func NewAdminRole() rbac.Role { 21 | return rbac.Role{ 22 | RoleID: "Admin", 23 | Permissions: []rbac.Permission{ 24 | rbac.NewGlobPermission("*", "*"), 25 | }, 26 | } 27 | } 28 | ``` 29 | The [rbac.NewGlobPermission](https://godoc.org/github.com/zpatrick/rbac#NewGlobPermission) function takes two arguments: `actionPattern` and `targetPattern`. 30 | Then, it creates a permission that will return true if the requested action matches `actionPattern`, and if the requested target matches `targetPattern`. 31 | Since `*`is a wildcard in glob matching, we've created a permission that will return true for _any_ action on _any_ target. 32 | To put it more simply: this permission allows the **Admin** role to do anything. 33 | 34 | ```go 35 | admin := NewAdminRole() 36 | 37 | // rbac.NewGlobPermission("*", "*") will cause this to return true since 38 | // the "ReadArticle" action glob matches the "*" actionPattern in the permission 39 | // and the "article_id" target glob matches the "*" targetPattern in the permission. 40 | admin.Can("ReadArticle", "article_id") 41 | 42 | // rbac.NewGlobPermission("*", "*") will cause this to return true since 43 | // the "DeleteArticle" action glob matches the "*" actionPattern in the permission 44 | // and the "article_id" target glob matches the "*" targetPattern in the permission. 45 | admin.Can("DeleteArticle", "article_id") 46 | ``` 47 | 48 | ### Guest Role 49 | Since the **Guest** role is only allowed to do the `ReadArticle` and `RateArticle` actions on any target (e.g. on any article), we can define that role's permissions in the following way: 50 | 51 | ```go 52 | ffunc NewGuestRole() rbac.Role { 53 | return rbac.Role{ 54 | RoleID: "Guest", 55 | Permissions: []rbac.Permission{ 56 | rbac.NewGlobPermission("ReadArticle", "*"), 57 | rbac.NewGlobPermission("RateArticle", "*"), 58 | }, 59 | } 60 | } 61 | ``` 62 | The first permission we define, `rbac.NewGlobPermission("ReadArticle", "*")`, allows the role to perform the `"ReadArticle"` action on `*` (any) target. 63 | To put it more simply: this permission allows the **Guest** role to read any article. 64 | 65 | The second permission we define, `rbac.NewGlobPermission("RateArticle", "*")`, allows the role to perform the `"RateArticle"` action on `*` (any) target. 66 | To put it more simply: this permission allows the **Guest** role to rate any article. 67 | 68 | ```go 69 | guest := NewGuestRole() 70 | 71 | // rbac.NewGlobPermission("ReadArticle", "*") will cause this to return true since 72 | // the "ReadArticle" action glob matches the "ReadArticle" actionPattern in the permission 73 | // and the "article_id" target glob matches the "*" targetPattern in the permission. 74 | guest.Can("ReadArticle", "article_id") 75 | 76 | // this will return false beacause the guest role has no permissions 77 | // that match the "DeleteArticle" action 78 | guest.Can("DeleteArticle", "article_id") 79 | ``` 80 | 81 | ## Try It Out 82 | You can run this program yourself to view the permission with the following commands: 83 | ```console 84 | $ go run *.go 85 | Role: Guest 86 | Action ArticleID Allowed 87 | ----------------------------------------------- 88 | CreateArticle - false 89 | ReadArticle a1 true 90 | EditArticle a1 false 91 | DeleteArticle a1 false 92 | RateArticle a1 true 93 | ``` 94 | 95 | ```console 96 | $ go run *.go -role=admin 97 | Role: Admin 98 | Action ArticleID Allowed 99 | ----------------------------------------------- 100 | CreateArticle - true 101 | ReadArticle a1 true 102 | EditArticle a1 true 103 | DeleteArticle a1 true 104 | RateArticle a1 true 105 | ``` 106 | -------------------------------------------------------------------------------- /examples/blog_complex/README.md: -------------------------------------------------------------------------------- 1 | # Complex Blog Example 2 | This example is a continuation of the [simple blog](https://github.com/zpatrick/rbac/tree/master/examples/blog_simple) example. 3 | Please view that example before continuing. 4 | 5 | 6 | In this example, we add a new role named **Member**. 7 | This role is allowed to create, read, and rate any article. 8 | It is also allowed to edit and delete articles that were authored by the user. 9 | 10 | | Role | Create Article | Read Article | Edit Article | Delete Article | Rate Article | 11 | |--------|----------------|--------------|----------------|----------------|--------------| 12 | | Guest | - | Allow | - | - | Allow | 13 | | Member | Allow | Allow | IfAuthor | IfAuthor | Allow | 14 | | Admin | Allow | Allow | Allow | Allow | Allow | 15 | 16 | ## Creating the Roles 17 | The [roles.go](/examples/blog_complex/roles.go) file shows how one can implement this permission set. 18 | Please see the [simple blog](https://github.com/zpatrick/rbac/tree/master/examples/blog_simple) example for information on how to create the **Guest** and **Admin** roles. 19 | 20 | ### Member Role 21 | Since the **Member** role is allowed to create, read, and rate any article, we can define those permissions in the following way: 22 | 23 | ```go 24 | func NewMemberRole() rbac.Role { 25 | return rbac.Role{ 26 | RoleID: "Member", 27 | Permissions: []rbac.Permission{ 28 | rbac.NewGlobPermission("CreateArticle", "*"), 29 | rbac.NewGlobPermission("ReadArticle", "*"), 30 | rbac.NewGlobPermission("RateArticle", "*"), 31 | }, 32 | } 33 | } 34 | ``` 35 | 36 | This role is also allowed to edit and delete articles as long as the article was authored by the the user who is assuming the **Member** role. 37 | In order to implement this sort logic, we need to create a custom [Matcher](https://godoc.org/github.com/zpatrick/rbac#Matcher). 38 | A matcher is a function that returns a bool representing whether or not the target matches some pre-defined pattern. 39 | In this context, we need a function that returns true if and only if the specified target (an article's ID) was authored by some specified user: 40 | ```go 41 | // ifArticleAuthor returns a rbac.Matcher that will only return true if 42 | // the article's author matches userID. 43 | func ifArticleAuthor(userID string) rbac.Matcher { 44 | return func(target string) (bool, error) { 45 | for _, article := range Articles() { 46 | if article.ArticleID == target { 47 | return article.AuthorID == userID, nil 48 | } 49 | } 50 | 51 | return false, nil 52 | } 53 | } 54 | ``` 55 | 56 | Now, we can create a new permission that utilizes this Matcher: 57 | ```go 58 | rbac.NewPermission(rbac.GlobMatch("EditArticle"), ifArticleAuthor(userID)) 59 | ``` 60 | This creates a new permission that will only return true in the following circumstance: 61 | * The specified action glob matches `"EditArticle"` 62 | * The specified target matches an article's `ArticleID`, and that article's author matches the specified `userID`. 63 | 64 | We can put this all together to generate the final `NewMemberRole` function: 65 | ```go 66 | func NewMemberRole(userID string) rbac.Role { 67 | return rbac.Role{ 68 | RoleID: fmt.Sprintf("Member(%s)", userID), 69 | Permissions: []rbac.Permission{ 70 | rbac.NewGlobPermission("CreateArticle", "*"), 71 | rbac.NewGlobPermission("ReadArticle", "*"), 72 | rbac.NewGlobPermission("RateArticle", "*"), 73 | rbac.NewPermission(rbac.GlobMatch("EditArticle"), ifArticleAuthor(userID)), 74 | rbac.NewPermission(rbac.GlobMatch("DeleteArticle"), ifArticleAuthor(userID)), 75 | }, 76 | } 77 | } 78 | ``` 79 | 80 | This is how the role could be used in an application: 81 | ```go 82 | member := NewMemberRole("u1") 83 | 84 | // rbac.NewGlobPermission("ReadArticle", "*") will cause this to always return true. 85 | member.Can("ReadArticle", "a1") 86 | 87 | // rbac.NewPermission(rbac.GlobMatch("DeleteArticle"), ifArticleAuthor(userID)) will cause 88 | // this to return true if and only if article "a1" exists, and that article's author is "u1". 89 | member.Can("DeleteArticle", "a1") 90 | ``` 91 | 92 | ## Try It Out 93 | You can run this program yourself to view the permission with the following commands: 94 | ```console 95 | $ go run *.go -role=member 96 | Role: Member(u1) 97 | Action ArticleID AuthorID Allowed 98 | ------------------------------------------------------------------- 99 | CreateArticle - - true 100 | ReadArticle a1 u1 true 101 | EditArticle a1 u1 true 102 | DeleteArticle a1 u1 true 103 | RateArticle a1 u1 true 104 | ReadArticle a2 u2 true 105 | EditArticle a2 u2 false 106 | DeleteArticle a2 u2 false 107 | RateArticle a2 u2 true 108 | ReadArticle a3 u3 true 109 | EditArticle a3 u3 false 110 | DeleteArticle a3 u3 false 111 | RateArticle a3 u3 true 112 | ``` 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | --------------------------------------------------------------------------------