├── .gitattributes ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── definition.go ├── definition_test.go ├── errors.go ├── example_test.go ├── permission.go ├── permission_test.go ├── scope.go ├── scope_test.go └── utils.go /.gitattributes: -------------------------------------------------------------------------------- 1 | # Automatically normalize line endings for all text-based files 2 | # http://git-scm.com/docs/gitattributes#_end_of_line_conversion 3 | * text=auto 4 | 5 | # For the following file types, normalize line endings to LF on checking and 6 | # prevent conversion to CRLF when they are checked out (this is required in 7 | # order to prevent newline related issues) 8 | .* text eol=lf 9 | *.go text eol=lf 10 | *.yml text eol=lf 11 | *.html text eol=lf 12 | *.css text eol=lf 13 | *.js text eol=lf 14 | *.json text eol=lf 15 | LICENSE text eol=lf 16 | 17 | # Exclude `website` and `recipes` from Github's language statistics 18 | # https://github.com/github/linguist#using-gitattributes 19 | recipes/* linguist-documentation 20 | website/* linguist-documentation 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IntelliJ 2 | .idea 3 | *.iml 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | before_install: 4 | - go get github.com/stretchr/testify 5 | 6 | go: 7 | - 1.2 8 | - 1.3 9 | - 1.4 10 | - 1.5 11 | - 1.6 12 | - tip 13 | 14 | script: 15 | - go test -v ./... 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) [2015] [Asdine El Hrychy] 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Permission 2 | 3 | [![Build Status](https://travis-ci.org/asdine/permission.svg)](https://travis-ci.org/asdine/permission) 4 | [![GoDoc](https://godoc.org/github.com/asdine/permission?status.svg)](https://godoc.org/github.com/asdine/permission) 5 | ![License MIT](https://img.shields.io/badge/license-MIT-blue.svg?style=flat) 6 | 7 | Permission is a low-level Go package that allows to easily manage permissions. 8 | 9 | ## Install 10 | 11 | ``` 12 | $ go get -u github.com/asdine/permission 13 | ``` 14 | 15 | ## Usage 16 | 17 | ```go 18 | // Define the permissions 19 | def := permission.Definitions{ 20 | { 21 | Name: "user", 22 | Subset: []string{"edit", "profile", "email", "friends", "about"}, 23 | DefaultSubset: []string{"profile", "about"}, 24 | }, 25 | { 26 | Name: "playlist", 27 | Subset: []string{"edit", "share", "read"}, 28 | DefaultSubset: []string{"read", "share"}, 29 | } 30 | } 31 | 32 | // Require specific permissions and test it against a scope 33 | def.Require("user.edit", "user.profile,user.about,user.email") 34 | // -> false 35 | 36 | 37 | // User permissions 38 | p, _ := permission.Parse("user.edit") 39 | 40 | q, _ := permission.Parse("user") 41 | // q has user:profile and user:about 42 | 43 | // Required permission 44 | required := permission.Permission{Name: "user", Sub: "edit"} 45 | 46 | // Compare 47 | allowed := def[0].Allowed(required, p) 48 | fmt.Println(allowed) 49 | // -> true 50 | 51 | allowed = def[0].Allowed(required, q) 52 | fmt.Println(allowed) 53 | // -> false 54 | ``` 55 | 56 | ## Permission 57 | 58 | Permission is the primitive that defines a single permission. 59 | 60 | ```go 61 | p, _ := permission.Parse("read") 62 | ``` 63 | 64 | You can specify a sub Permission by adding a `.` 65 | 66 | ```go 67 | p, _ := permission.Parse("user.edit") 68 | ``` 69 | 70 | The `.` delimiter can be changed by setting the global package delimiter 71 | 72 | ```go 73 | permission.Delimiter(":") 74 | 75 | p, _ := permission.Parse("user:edit") 76 | ``` 77 | 78 | The variable returned by `permission.Parse` is a Permission primitive than can be easily manipulated and marshalled. 79 | 80 | ```go 81 | p, _ := permission.Parse("user.edit") 82 | q, _ := permission.Parse("user.edit") 83 | 84 | fmt.Println(p.Name) 85 | // user 86 | 87 | fmt.Println(p.Sub) 88 | // edit 89 | 90 | fmt.Println(p.Equal(q)) 91 | // true 92 | 93 | fmt.Println(p) 94 | // user.edit 95 | ``` 96 | 97 | The primitive can be Unmarshalled from JSON ... 98 | 99 | ```go 100 | type Access struct { 101 | Name string 102 | Permission permission.Permission 103 | } 104 | 105 | a := Access{} 106 | 107 | input := []byte(`{"Name":"Edit User","Permission":"user.edit"}`) 108 | json.Unmarshal(input, &a) 109 | 110 | fmt.Println(a.Permission.Name) 111 | // user 112 | fmt.Println(a.Permission.Sub) 113 | // edit 114 | ``` 115 | 116 | ... and marshalled to JSON 117 | 118 | ```go 119 | output, _ := json.Marshal(a) 120 | fmt.Println(output) 121 | // {"Name":"Edit User","Permission":"user.edit"} 122 | ``` 123 | 124 | ## Scope 125 | 126 | A Scope is a set of permissions. It can be used to describe multiple permissions. 127 | 128 | ```go 129 | s, _ := permission.ParseScope("read,write,edit,user.email") 130 | ``` 131 | 132 | The `,` separator can be changed by setting the global package separator 133 | 134 | ```go 135 | permission.Separator(" ") 136 | 137 | s, _ := permission.ParseScope("read write edit user.email") 138 | ``` 139 | 140 | The variable returned by `permission.ParseScope` is a Scope primitive helper to manipulate sets of Permissions. 141 | 142 | ```go 143 | s, _ := permission.ParseScope("read,write,user.email") 144 | 145 | fmt.Println(len(s)) 146 | // 3 147 | 148 | fmt.Println(s[0].Name) 149 | // read 150 | 151 | fmt.Println(s[2].Sub) 152 | // email 153 | 154 | fmt.Println(s) 155 | // read,write,edit,user.email 156 | ``` 157 | 158 | JSON example 159 | ```go 160 | type Role struct { 161 | Name string 162 | Permissions permission.Scope 163 | } 164 | 165 | r := Role{} 166 | 167 | input := []byte(`{"Name":"Admin","Permission":"read,write,user.email"}`) 168 | json.Unmarshal(input, &r) 169 | 170 | fmt.Println(len(r.Permissions)) 171 | // 3 172 | 173 | fmt.Println(r.Permissions[0].Name) 174 | // read 175 | 176 | fmt.Println(a.Permissions[2].Sub) 177 | // edit 178 | 179 | output, _ := json.Marshal(r) 180 | fmt.Println(output) 181 | // {"Name":"Admin","Permission":"read,write,user.email"} 182 | ``` 183 | 184 | ## Definition 185 | 186 | Definition is a way of defining permission attributes and rules. 187 | 188 | ```go 189 | def := permission.Definition{ 190 | Name: "playlist", 191 | Subset: []string{"edit", "share", "read"}, 192 | DefaultSubset: []string{"read", "share"}, 193 | } 194 | ``` 195 | 196 | Once a definition is created you can test permissions against it to see if they match 197 | 198 | ```go 199 | p, _ := permission.Parse("playlist.edit") 200 | 201 | fmt.Println(def.Match(p)) 202 | // true 203 | ``` 204 | 205 | You can also compare two permissions and test them against the definition. 206 | It is useful when you have a permission that has default sub permissions. 207 | 208 | ```go 209 | required, _ := permission.Parse("playlist") 210 | // required gets granted the DefaultSubset list of permissions. 211 | // It is equivalent to playlist.read and playlist.share 212 | 213 | p, _ := permission.Parse("playlist.share") 214 | 215 | fmt.Println(def.Allowed(required, p)) 216 | // true 217 | ``` 218 | 219 | The given permission can also benefit of the DefaultSubset 220 | 221 | ```go 222 | required, _ := permission.Parse("playlist.read") 223 | 224 | p, _ := permission.Parse("playlist") 225 | // required gets granted the DefaultSubset list of permissions. 226 | // It is equivalent to playlist.read and playlist.share 227 | 228 | fmt.Println(def.Allowed(required, p)) 229 | // true 230 | ``` 231 | 232 | ## License 233 | 234 | MIT 235 | 236 | ## Author 237 | 238 | **Asdine El Hrychy** 239 | 240 | - [Twitter](https://twitter.com/asdine_) 241 | - [Github](https://github.com/asdine) 242 | -------------------------------------------------------------------------------- /definition.go: -------------------------------------------------------------------------------- 1 | package permission 2 | 3 | // Definition defines a Permission and its subset. 4 | // It allows to explicitly define the rules of a permission and to test permissions against the definition. 5 | type Definition struct { 6 | // Name is the name of the Permission 7 | Name string 8 | 9 | // Subset is a list of all allowed sub permissions 10 | Subset []string 11 | 12 | // DefaultSubset is a list of sub permissions allowed when only the name of the permission is specified 13 | DefaultSubset []string 14 | } 15 | 16 | // Match detects if the given permission matches the Definition 17 | func (def *Definition) Match(perm Permission) bool { 18 | if perm.Name != def.Name { 19 | return false 20 | } 21 | 22 | if perm.Sub != "" { 23 | return InStringSlice(def.Subset, perm.Sub) 24 | } 25 | 26 | return true 27 | } 28 | 29 | // Allowed checks wether given respects required and the definition 30 | func (def *Definition) Allowed(required, given Permission) bool { 31 | if required.Name != def.Name || required.Name != given.Name { 32 | return false 33 | } 34 | 35 | if required.Sub == "" && given.Sub == "" { 36 | return true 37 | } 38 | 39 | if given.Sub != "" { 40 | if required.Sub == "" { 41 | return InStringSlice(def.DefaultSubset, given.Sub) 42 | } 43 | return required.Sub == given.Sub && InStringSlice(def.Subset, given.Sub) 44 | } 45 | 46 | return InStringSlice(def.DefaultSubset, required.Sub) 47 | } 48 | 49 | // Definitions are a group of Definition 50 | type Definitions []Definition 51 | 52 | // Require checks wether the given scope matches the required permission and is listed in the definitions. 53 | // Returns false if the parsing fails 54 | func (d Definitions) Require(required, scope string) bool { 55 | req, err := ParseScope(required) 56 | if err != nil { 57 | return false 58 | } 59 | 60 | s, err := ParseScope(scope) 61 | if err != nil { 62 | return false 63 | } 64 | 65 | for _, perm := range req { 66 | def := d.Definition(perm) 67 | if def != nil { 68 | for _, p := range s { 69 | if def.Allowed(perm, p) { 70 | return true 71 | } 72 | } 73 | } 74 | } 75 | 76 | return false 77 | } 78 | 79 | // Definition returns the Definition that matches the Permission 80 | func (d Definitions) Definition(p Permission) *Definition { 81 | for i := range d { 82 | if d[i].Match(p) { 83 | return &d[i] 84 | } 85 | } 86 | return nil 87 | } 88 | -------------------------------------------------------------------------------- /definition_test.go: -------------------------------------------------------------------------------- 1 | package permission 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestMatchSimpleDef(t *testing.T) { 10 | d := Definition{Name: "a"} 11 | 12 | p := Permission{Name: "b"} 13 | assert.False(t, d.Match(p)) 14 | 15 | p = Permission{Name: ""} 16 | assert.False(t, d.Match(p)) 17 | 18 | p = Permission{Name: "a"} 19 | assert.True(t, d.Match(p)) 20 | } 21 | 22 | func TestMatchDef(t *testing.T) { 23 | d := Definition{ 24 | Name: "a", 25 | Subset: []string{"i", "j", "k"}, 26 | DefaultSubset: []string{"i", "j"}, 27 | } 28 | 29 | p := Permission{Name: "a"} 30 | assert.True(t, d.Match(p)) 31 | 32 | p = Permission{Name: "b"} 33 | assert.False(t, d.Match(p)) 34 | 35 | p = Permission{Name: ""} 36 | assert.False(t, d.Match(p)) 37 | 38 | p = Permission{Name: "a", Sub: "l"} 39 | assert.False(t, d.Match(p)) 40 | 41 | p = Permission{Name: "b", Sub: "i"} 42 | assert.False(t, d.Match(p)) 43 | 44 | p = Permission{Name: "a", Sub: "i"} 45 | assert.True(t, d.Match(p)) 46 | 47 | p = Permission{Name: "a", Sub: "j"} 48 | assert.True(t, d.Match(p)) 49 | 50 | p = Permission{Name: "a", Sub: "k"} 51 | assert.True(t, d.Match(p)) 52 | } 53 | 54 | func TestAllowedSimple(t *testing.T) { 55 | d := Definition{ 56 | Name: "a", 57 | Subset: []string{"i", "j", "k"}, 58 | DefaultSubset: []string{"i", "j"}, 59 | } 60 | 61 | required := Permission{Name: "a"} 62 | 63 | p := Permission{Name: "b"} 64 | assert.False(t, d.Allowed(required, p)) 65 | 66 | p = Permission{Name: ""} 67 | assert.False(t, d.Allowed(required, p)) 68 | 69 | p = Permission{Name: "a"} 70 | assert.True(t, d.Allowed(required, p)) 71 | 72 | p = Permission{Name: "a", Sub: "i"} 73 | assert.True(t, d.Allowed(required, p)) 74 | 75 | required = Permission{Name: ""} 76 | p = Permission{Name: "a"} 77 | assert.False(t, d.Allowed(required, p)) 78 | } 79 | 80 | func TestAllowed(t *testing.T) { 81 | d := Definition{ 82 | Name: "a", 83 | Subset: []string{"i", "j", "k"}, 84 | DefaultSubset: []string{"i", "j"}, 85 | } 86 | 87 | required := Permission{Name: "a", Sub: "i"} 88 | 89 | p := Permission{Name: "a", Sub: "j"} 90 | assert.False(t, d.Allowed(required, p)) 91 | 92 | p = Permission{Name: "a", Sub: "i"} 93 | assert.True(t, d.Allowed(required, p)) 94 | 95 | p = Permission{Name: "a"} // a = a.i,a.j 96 | assert.True(t, d.Allowed(required, p)) 97 | 98 | required = Permission{Name: "a", Sub: "k"} 99 | 100 | p = Permission{Name: "a"} 101 | assert.False(t, d.Allowed(required, p)) 102 | } 103 | 104 | func TestDefinitions_Definition(t *testing.T) { 105 | d := Definitions{ 106 | Definition{ 107 | Name: "a", 108 | Subset: []string{"i", "j", "k"}, 109 | DefaultSubset: []string{"i", "j"}, 110 | }, 111 | Definition{ 112 | Name: "b", 113 | Subset: []string{"i", "j", "k"}, 114 | DefaultSubset: []string{"i", "j"}, 115 | }, 116 | } 117 | 118 | assert.NotNil(t, d.Definition(Permission{Name: "a"})) 119 | assert.NotNil(t, d.Definition(Permission{Name: "b"})) 120 | assert.NotNil(t, d.Definition(Permission{Name: "a", Sub: "i"})) 121 | assert.Nil(t, d.Definition(Permission{Name: "a", Sub: "z"})) 122 | assert.Nil(t, d.Definition(Permission{Name: "c"})) 123 | assert.Nil(t, d.Definition(Permission{Name: ""})) 124 | } 125 | 126 | func TestDefinitions_Require(t *testing.T) { 127 | d := Definitions{ 128 | { 129 | Name: "a", 130 | Subset: []string{"i", "j", "k"}, 131 | DefaultSubset: []string{"i", "j"}, 132 | }, 133 | { 134 | Name: "b", 135 | Subset: []string{"i", "j", "k"}, 136 | DefaultSubset: []string{"i", "j"}, 137 | }, 138 | } 139 | 140 | assert.False(t, d.Require("a", "b")) 141 | assert.True(t, d.Require("a", "a")) 142 | assert.True(t, d.Require("b", "b")) 143 | assert.True(t, d.Require("a,b", "b.i")) 144 | assert.True(t, d.Require("a,b", "a.i")) 145 | assert.False(t, d.Require("a,b", "a.k")) 146 | assert.True(t, d.Require("a,b", "a.k,b.i")) 147 | assert.False(t, d.Require("a.i", "a.k,b.i")) 148 | assert.True(t, d.Require("a.i", "a,b.i")) 149 | assert.False(t, d.Require("a.", "a,b.i")) 150 | assert.False(t, d.Require("a", "a,")) 151 | } 152 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package permission 2 | 3 | import "errors" 4 | 5 | // Errors 6 | var ( 7 | ErrEmptyName = errors.New("The permission name is empty") 8 | ErrEmptyInput = errors.New("The given input is an empty string") 9 | ErrBadFormat = errors.New("The given input is not in the correct format") 10 | ) 11 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package permission_test 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/asdine/permission" 8 | ) 9 | 10 | func Example() { 11 | p := permission.Permission{ 12 | Name: "user", 13 | Sub: "edit", 14 | } 15 | 16 | data, _ := json.Marshal(p) 17 | fmt.Printf("%s\n", data) 18 | 19 | p = permission.Permission{} 20 | json.Unmarshal(data, &p) 21 | fmt.Println(p.Name) 22 | fmt.Println(p.Sub) 23 | 24 | def := permission.Definition{ 25 | Name: "user", 26 | Subset: []string{"edit", "profile", "email", "friends", "about"}, 27 | DefaultSubset: []string{"profile", "about"}, 28 | } 29 | 30 | required, _ := permission.Parse("user.edit") 31 | allowed := def.Allowed(required, p) 32 | fmt.Println(allowed) 33 | // Output: 34 | // "user.edit" 35 | // user 36 | // edit 37 | // true 38 | } 39 | 40 | func ExamplePermission() { 41 | // Simple Permission 42 | perm := permission.Permission{Name: "read"} 43 | fmt.Println(perm) 44 | // Output: read 45 | } 46 | 47 | func ExamplePermission_subPermission() { 48 | // Sub Permission 49 | perm := permission.Permission{Name: "user", Sub: "edit"} 50 | fmt.Println(perm) 51 | // Output: user.edit 52 | } 53 | 54 | func ExampleParse() { 55 | perm, _ := permission.Parse("user.edit") 56 | 57 | fmt.Println(perm.Name) 58 | fmt.Println(perm.Sub) 59 | fmt.Println(perm) 60 | // Output: 61 | // user 62 | // edit 63 | // user.edit 64 | } 65 | 66 | func ExampleDelimiter() { 67 | permission.Delimiter(":") 68 | defer permission.Delimiter(".") 69 | perm := permission.Permission{Name: "user", Sub: "edit"} 70 | fmt.Println(perm) 71 | // Output: user:edit 72 | } 73 | 74 | func ExamplePermission_Equal() { 75 | p, _ := permission.Parse("user.edit") 76 | q, _ := permission.Parse("user.edit") 77 | 78 | fmt.Println(p.Equal(q)) 79 | // Output: true 80 | } 81 | 82 | func ExampleScope() { 83 | perms := permission.Scope{ 84 | permission.Permission{Name: "user", Sub: "edit"}, 85 | permission.Permission{Name: "profile"}, 86 | permission.Permission{Name: "friends"}, 87 | } 88 | 89 | text, _ := perms.MarshalText() 90 | fmt.Println(string(text)) 91 | // Output: user.edit,profile,friends 92 | } 93 | 94 | func ExampleParseScope() { 95 | perms, _ := permission.ParseScope("user.edit,profile,friends") 96 | 97 | fmt.Println(len(perms)) 98 | fmt.Println(perms[0].Name) 99 | fmt.Println(perms[0].Sub) 100 | fmt.Println(perms[1].Name) 101 | fmt.Println(perms[2].Name) 102 | // Output: 103 | // 3 104 | // user 105 | // edit 106 | // profile 107 | // friends 108 | } 109 | 110 | func ExampleDefinition() { 111 | def := permission.Definition{ 112 | Name: "file", 113 | Subset: []string{"read", "write", "execute"}, 114 | DefaultSubset: []string{"read", "execute"}, 115 | } 116 | 117 | required, _ := permission.Parse("file.read") 118 | 119 | p, _ := permission.Parse("file.read") 120 | fmt.Println(def.Allowed(required, p)) 121 | 122 | p, _ = permission.Parse("file.execute") 123 | fmt.Println(def.Allowed(required, p)) 124 | 125 | p, _ = permission.Parse("file") // file = file.read,file.execute 126 | fmt.Println(def.Allowed(required, p)) 127 | 128 | required, _ = permission.Parse("file.write") 129 | 130 | p, _ = permission.Parse("file") // file = file.read,file.execute 131 | fmt.Println(def.Allowed(required, p)) 132 | // Output: 133 | // true 134 | // false 135 | // true 136 | // false 137 | } 138 | 139 | func ExampleDefinition_Match() { 140 | defs := []permission.Definition{ 141 | permission.Definition{ 142 | Name: "repo", 143 | Subset: []string{"read", "write"}, 144 | DefaultSubset: []string{"read"}, 145 | }, 146 | permission.Definition{ 147 | Name: "user", 148 | Subset: []string{"profile", "edit", "friends", "email"}, 149 | DefaultSubset: []string{"profile", "friends"}, 150 | }, 151 | permission.Definition{ 152 | Name: "playlist", 153 | Subset: []string{"edit", "share", "read"}, 154 | DefaultSubset: []string{"read", "share"}, 155 | }, 156 | } 157 | 158 | p, _ := permission.Parse("user.edit") 159 | 160 | for _, def := range defs { 161 | if def.Match(p) { 162 | fmt.Println(def.Name) 163 | } 164 | } 165 | // Output: 166 | // user 167 | } 168 | -------------------------------------------------------------------------------- /permission.go: -------------------------------------------------------------------------------- 1 | // Package permission is a low-level Go package that allows to easily manage permissions 2 | package permission 3 | 4 | import ( 5 | "fmt" 6 | "strings" 7 | "sync" 8 | ) 9 | 10 | var delimiter = "." 11 | 12 | var delimLock sync.Mutex 13 | 14 | // Delimiter is a thread-safe function that sets a global delimiter for Permissions. 15 | // Defaults to "." 16 | func Delimiter(delim string) { 17 | delimLock.Lock() 18 | defer delimLock.Unlock() 19 | delimiter = delim 20 | } 21 | 22 | // Parse takes a string representation and returns the corresponding Permission 23 | func Parse(repr string) (Permission, error) { 24 | p := Permission{} 25 | err := p.UnmarshalText([]byte(repr)) 26 | if err != nil { 27 | return Permission{}, err 28 | } 29 | return p, nil 30 | } 31 | 32 | // Permission is a simple permission structure. 33 | // It is meant to describe a single specific permission. 34 | // A Sub permission can be specified. 35 | // It can safely be converted back and forth to json 36 | type Permission struct { 37 | // Name of the permission 38 | Name string 39 | 40 | // Sub permission is optional 41 | Sub string 42 | } 43 | 44 | // Equal reports whether p and q represents the same permission 45 | func (p Permission) Equal(q Permission) bool { 46 | return p.Name == q.Name && p.Sub == q.Sub 47 | } 48 | 49 | // IsZero reports wether the permission is a zero value 50 | func (p Permission) IsZero() bool { 51 | return p.Name == "" && p.Sub == "" 52 | } 53 | 54 | // MarshalText implements the encoding.TextMarshaler interface 55 | func (p Permission) MarshalText() (text []byte, err error) { 56 | if p.Name == "" { 57 | return nil, ErrEmptyName 58 | } 59 | 60 | if p.Sub == "" { 61 | return []byte(p.Name), nil 62 | } 63 | 64 | return []byte(fmt.Sprintf("%s%s%s", p.Name, delimiter, p.Sub)), nil 65 | } 66 | 67 | // UnmarshalText implements the encoding.TextUnmarshaler interface 68 | func (p *Permission) UnmarshalText(text []byte) error { 69 | if len(text) == 0 { 70 | return ErrEmptyInput 71 | } 72 | 73 | perm := string(text) 74 | if !strings.Contains(perm, delimiter) { 75 | p.Name = perm 76 | return nil 77 | } 78 | 79 | frags := strings.Split(perm, delimiter) 80 | if len(frags) != 2 { 81 | return ErrBadFormat 82 | } 83 | 84 | if frags[0] == "" || frags[1] == "" { 85 | return ErrBadFormat 86 | } 87 | 88 | p.Name = frags[0] 89 | p.Sub = frags[1] 90 | return nil 91 | } 92 | 93 | // String returns the string representation of the Permission 94 | func (p Permission) String() string { 95 | if p.Sub != "" { 96 | return fmt.Sprintf("%s%s%s", p.Name, delimiter, p.Sub) 97 | } 98 | 99 | return p.Name 100 | } 101 | -------------------------------------------------------------------------------- /permission_test.go: -------------------------------------------------------------------------------- 1 | package permission 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestMarshalling(t *testing.T) { 11 | p := Permission{Name: "a"} 12 | 13 | output, err := p.MarshalText() 14 | assert.NoError(t, err) 15 | assert.Equal(t, "a", string(output)) 16 | 17 | p = Permission{Name: "a", Sub: "b"} 18 | 19 | output, err = p.MarshalText() 20 | assert.NoError(t, err) 21 | assert.Equal(t, "a.b", string(output)) 22 | 23 | p = Permission{} 24 | 25 | output, err = p.MarshalText() 26 | assert.Error(t, err) 27 | assert.Equal(t, ErrEmptyName, err) 28 | assert.Nil(t, output) 29 | } 30 | 31 | func TestUnmarshalling(t *testing.T) { 32 | text := []byte("a") 33 | p := Permission{} 34 | 35 | err := p.UnmarshalText(text) 36 | assert.NoError(t, err) 37 | assert.Equal(t, "a", p.Name) 38 | assert.Empty(t, p.Sub) 39 | 40 | text = []byte("a.b") 41 | p = Permission{} 42 | 43 | err = p.UnmarshalText(text) 44 | assert.NoError(t, err) 45 | assert.Equal(t, "a", p.Name) 46 | assert.Equal(t, "b", p.Sub) 47 | 48 | err = p.UnmarshalText(nil) 49 | assert.Error(t, err) 50 | assert.Equal(t, ErrEmptyInput, err) 51 | 52 | text = []byte("a.") 53 | err = p.UnmarshalText(text) 54 | assert.Error(t, err) 55 | assert.Equal(t, ErrBadFormat, err) 56 | 57 | text = []byte(".b") 58 | err = p.UnmarshalText(text) 59 | assert.Error(t, err) 60 | assert.Equal(t, ErrBadFormat, err) 61 | 62 | text = []byte("a.b.c") 63 | err = p.UnmarshalText(text) 64 | assert.Error(t, err) 65 | assert.Equal(t, ErrBadFormat, err) 66 | 67 | text = []byte(".") 68 | err = p.UnmarshalText(text) 69 | assert.Error(t, err) 70 | assert.Equal(t, ErrBadFormat, err) 71 | } 72 | 73 | func TestPermToJSON(t *testing.T) { 74 | p := Permission{Name: "a"} 75 | 76 | val, err := json.Marshal(p) 77 | assert.NoError(t, err) 78 | assert.Equal(t, `"a"`, string(val)) 79 | 80 | p = Permission{Name: "a", Sub: "b"} 81 | 82 | val, err = json.Marshal(p) 83 | assert.NoError(t, err) 84 | assert.Equal(t, `"a.b"`, string(val)) 85 | 86 | p = Permission{} 87 | val, err = json.Marshal(p) 88 | assert.Error(t, err) 89 | } 90 | 91 | func TestPermFromJSON(t *testing.T) { 92 | s := []byte(`"a"`) 93 | p := Permission{} 94 | 95 | err := json.Unmarshal(s, &p) 96 | assert.NoError(t, err) 97 | assert.Equal(t, "a", p.Name) 98 | 99 | s = []byte(`"a.b"`) 100 | p = Permission{} 101 | 102 | err = json.Unmarshal(s, &p) 103 | assert.NoError(t, err) 104 | assert.Equal(t, "a", p.Name) 105 | assert.Equal(t, "b", p.Sub) 106 | 107 | s = []byte(`""`) 108 | p = Permission{} 109 | 110 | err = json.Unmarshal(s, &p) 111 | assert.Error(t, err) 112 | } 113 | 114 | func TestPermToFromJSON(t *testing.T) { 115 | s := Permission{Name: "a", Sub: "b"} 116 | 117 | val, err := json.Marshal(s) 118 | assert.NoError(t, err) 119 | 120 | d := Permission{} 121 | err = json.Unmarshal(val, &d) 122 | assert.NoError(t, err) 123 | assert.Equal(t, s, d) 124 | } 125 | 126 | func TestDelimiter(t *testing.T) { 127 | Delimiter(":") 128 | defer Delimiter(".") 129 | 130 | p := Permission{Name: "a", Sub: "b"} 131 | 132 | output, err := p.MarshalText() 133 | assert.NoError(t, err) 134 | assert.Equal(t, "a:b", string(output)) 135 | 136 | text := []byte("a:b") 137 | p = Permission{} 138 | 139 | err = p.UnmarshalText(text) 140 | assert.NoError(t, err) 141 | assert.Equal(t, "a", p.Name) 142 | assert.Equal(t, "b", p.Sub) 143 | 144 | text = []byte("a.b") 145 | p = Permission{} 146 | 147 | err = p.UnmarshalText(text) 148 | assert.NoError(t, err) 149 | assert.Equal(t, "a.b", p.Name) 150 | assert.Empty(t, p.Sub) 151 | } 152 | 153 | func TestParse(t *testing.T) { 154 | p, err := Parse("a.b") 155 | assert.NoError(t, err) 156 | assert.NotNil(t, p) 157 | assert.Equal(t, "a", p.Name) 158 | assert.Equal(t, "b", p.Sub) 159 | val, err := json.Marshal(p) 160 | assert.NoError(t, err) 161 | assert.Equal(t, `"a.b"`, string(val)) 162 | 163 | Delimiter(":") 164 | defer Delimiter(".") 165 | 166 | p, err = Parse("a.b") 167 | assert.NoError(t, err) 168 | assert.NotNil(t, p) 169 | assert.Equal(t, "a.b", p.Name) 170 | assert.Empty(t, p.Sub) 171 | 172 | p, err = Parse("a:b") 173 | assert.NoError(t, err) 174 | assert.NotNil(t, p) 175 | assert.Equal(t, "a", p.Name) 176 | assert.Equal(t, "b", p.Sub) 177 | 178 | p, err = Parse("a:") 179 | assert.Error(t, err) 180 | assert.True(t, p.IsZero()) 181 | } 182 | 183 | func TestEqual(t *testing.T) { 184 | p, _ := Parse("a") 185 | assert.True(t, p.Equal(p)) 186 | 187 | q, _ := Parse("b") 188 | assert.False(t, p.Equal(q)) 189 | 190 | q, _ = Parse("a") 191 | assert.True(t, p.Equal(q)) 192 | assert.True(t, q.Equal(p)) 193 | 194 | p, _ = Parse("a.b") 195 | assert.False(t, p.Equal(q)) 196 | 197 | q, _ = Parse("a.b") 198 | assert.True(t, p.Equal(q)) 199 | assert.True(t, q.Equal(p)) 200 | 201 | q, _ = Parse("a.c") 202 | assert.False(t, p.Equal(q)) 203 | 204 | r := Permission{Name: "a", Sub: "b"} 205 | assert.True(t, p.Equal(r)) 206 | } 207 | -------------------------------------------------------------------------------- /scope.go: -------------------------------------------------------------------------------- 1 | package permission 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | "sync" 7 | ) 8 | 9 | var separator = "," 10 | 11 | var sepLock sync.Mutex 12 | 13 | // Separator is a thread-safe function that sets a global separator for a set of Permissions. 14 | // Defaults to "," 15 | func Separator(sep string) { 16 | sepLock.Lock() 17 | defer sepLock.Unlock() 18 | separator = sep 19 | } 20 | 21 | // ParseScope takes a string representation and returns the corresponding Scope 22 | func ParseScope(repr string) (Scope, error) { 23 | s := Scope{} 24 | err := s.UnmarshalText([]byte(repr)) 25 | if err != nil { 26 | return nil, err 27 | } 28 | return s, nil 29 | } 30 | 31 | // Scope is a set of Permissions. 32 | // It can safely be converted back and forth to json 33 | type Scope []Permission 34 | 35 | // MarshalText implements the encoding.TextMarshaler interface 36 | func (s Scope) MarshalText() (text []byte, err error) { 37 | var buffer bytes.Buffer 38 | 39 | for i, perm := range s { 40 | raw, err := perm.MarshalText() 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | buffer.Write(raw) 46 | if i < len(s)-1 { 47 | buffer.WriteString(separator) 48 | } 49 | } 50 | 51 | return buffer.Bytes(), nil 52 | } 53 | 54 | // UnmarshalText implements the encoding.TextUnmarshaler interface 55 | func (s *Scope) UnmarshalText(text []byte) error { 56 | if len(text) == 0 { 57 | return ErrEmptyInput 58 | } 59 | 60 | scope := strings.Split(string(text), separator) 61 | *s = make([]Permission, len(scope)) 62 | for i, perm := range scope { 63 | err := (*s)[i].UnmarshalText([]byte(perm)) 64 | if err != nil { 65 | return err 66 | } 67 | } 68 | return nil 69 | } 70 | 71 | // HasPermission checks if the scope has the given permission 72 | func (s *Scope) HasPermission(p Permission) bool { 73 | for _, perm := range *s { 74 | if perm.Equal(p) { 75 | return true 76 | } 77 | } 78 | 79 | return false 80 | } 81 | 82 | // Has checks if the scope has the given permission 83 | func (s *Scope) Has(repr string) bool { 84 | p, err := Parse(repr) 85 | if err != nil { 86 | return false 87 | } 88 | 89 | return s.HasPermission(p) 90 | } 91 | -------------------------------------------------------------------------------- /scope_test.go: -------------------------------------------------------------------------------- 1 | package permission 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestScopeMarshalling(t *testing.T) { 11 | s := Scope{ 12 | Permission{Name: "a"}, 13 | Permission{Name: "b", Sub: "i"}, 14 | Permission{Name: "c", Sub: "j"}, 15 | } 16 | 17 | output, err := s.MarshalText() 18 | assert.NoError(t, err) 19 | assert.Equal(t, "a,b.i,c.j", string(output)) 20 | 21 | s = Scope{} 22 | output, err = s.MarshalText() 23 | assert.NoError(t, err) 24 | 25 | s = Scope{Permission{}} 26 | output, err = s.MarshalText() 27 | assert.Error(t, err) 28 | assert.Equal(t, ErrEmptyName, err) 29 | } 30 | 31 | func TestScopeUnmarshalling(t *testing.T) { 32 | input := []byte("a,b.i,c.j, d") 33 | expected := Scope{ 34 | Permission{Name: "a"}, 35 | Permission{Name: "b", Sub: "i"}, 36 | Permission{Name: "c", Sub: "j"}, 37 | Permission{Name: " d"}, 38 | } 39 | 40 | scope := Scope{} 41 | err := scope.UnmarshalText(input) 42 | assert.NoError(t, err) 43 | assert.Equal(t, expected, scope) 44 | 45 | input = []byte("") 46 | scope = Scope{} 47 | err = scope.UnmarshalText(input) 48 | assert.Error(t, err) 49 | assert.Equal(t, ErrEmptyInput, err) 50 | 51 | input = []byte("a,b.") 52 | scope = Scope{} 53 | err = scope.UnmarshalText(input) 54 | assert.Error(t, err) 55 | assert.Equal(t, ErrBadFormat, err) 56 | } 57 | 58 | func TestScopeToJSON(t *testing.T) { 59 | s := Scope{ 60 | Permission{Name: "a"}, 61 | Permission{Name: "b", Sub: "i"}, 62 | Permission{Name: "c", Sub: "j"}, 63 | } 64 | 65 | val, err := json.Marshal(s) 66 | assert.NoError(t, err) 67 | assert.Equal(t, `"a,b.i,c.j"`, string(val)) 68 | } 69 | 70 | func TestScopeFromJSON(t *testing.T) { 71 | input := []byte(`"a,b.i,c.j, d, e.k"`) 72 | expected := Scope{ 73 | Permission{Name: "a"}, 74 | Permission{Name: "b", Sub: "i"}, 75 | Permission{Name: "c", Sub: "j"}, 76 | Permission{Name: " d"}, 77 | Permission{Name: " e", Sub: "k"}, 78 | } 79 | 80 | s := Scope{} 81 | err := json.Unmarshal(input, &s) 82 | assert.NoError(t, err) 83 | assert.Equal(t, expected, s) 84 | } 85 | 86 | func TestSepatator(t *testing.T) { 87 | Separator(":") 88 | defer Separator(",") 89 | 90 | s := Scope{ 91 | Permission{Name: "a"}, 92 | Permission{Name: "b", Sub: "i"}, 93 | } 94 | 95 | output, err := s.MarshalText() 96 | assert.NoError(t, err) 97 | assert.Equal(t, "a:b.i", string(output)) 98 | } 99 | 100 | func TestNewScope(t *testing.T) { 101 | s, err := ParseScope("a.b") 102 | assert.NoError(t, err) 103 | assert.NotNil(t, s) 104 | assert.Len(t, s, 1) 105 | assert.Equal(t, Permission{Name: "a", Sub: "b"}, s[0]) 106 | val, err := json.Marshal(s) 107 | assert.NoError(t, err) 108 | assert.Equal(t, `"a.b"`, string(val)) 109 | 110 | s, err = ParseScope("a.b,c") 111 | assert.NoError(t, err) 112 | assert.NotNil(t, s) 113 | assert.Len(t, s, 2) 114 | assert.True(t, s[1].Equal(Permission{Name: "c"})) 115 | val, err = json.Marshal(s) 116 | assert.NoError(t, err) 117 | assert.Equal(t, `"a.b,c"`, string(val)) 118 | 119 | Separator(":") 120 | defer Separator(",") 121 | 122 | s, err = ParseScope("a,b") 123 | assert.NoError(t, err) 124 | assert.NotNil(t, s) 125 | assert.Len(t, s, 1) 126 | assert.Equal(t, "a,b", s[0].Name) 127 | 128 | s, err = ParseScope("a:b") 129 | assert.NoError(t, err) 130 | assert.NotNil(t, s) 131 | assert.Len(t, s, 2) 132 | assert.Equal(t, "a", s[0].Name) 133 | assert.Equal(t, "b", s[1].Name) 134 | 135 | s, err = ParseScope("a:") 136 | assert.Error(t, err) 137 | assert.Nil(t, s) 138 | 139 | Separator(".") 140 | Delimiter(".") 141 | 142 | s, err = ParseScope("a,b.c.c.e") 143 | assert.NoError(t, err) 144 | assert.NotNil(t, s) 145 | assert.Len(t, s, 4) 146 | assert.Equal(t, "a,b", s[0].Name) 147 | assert.Equal(t, "c", s[1].Name) 148 | assert.Equal(t, "c", s[2].Name) 149 | assert.Equal(t, "e", s[3].Name) 150 | } 151 | 152 | func TestScopeHasPermission(t *testing.T) { 153 | s := Scope{ 154 | Permission{Name: "a"}, 155 | Permission{Name: "b", Sub: "i"}, 156 | } 157 | 158 | assert.True(t, s.HasPermission(Permission{Name: "a"})) 159 | assert.False(t, s.HasPermission(Permission{Name: "b"})) 160 | assert.False(t, s.HasPermission(Permission{Name: "a", Sub: "i"})) 161 | assert.True(t, s.HasPermission(Permission{Name: "b", Sub: "i"})) 162 | assert.False(t, s.HasPermission(Permission{Name: "c"})) 163 | assert.False(t, s.HasPermission(Permission{Name: "c", Sub: "i"})) 164 | } 165 | 166 | func TestScopeHas(t *testing.T) { 167 | s := Scope{ 168 | Permission{Name: "a"}, 169 | Permission{Name: "b", Sub: "i"}, 170 | } 171 | 172 | assert.True(t, s.Has("a")) 173 | assert.False(t, s.Has("b")) 174 | assert.False(t, s.Has("a.i")) 175 | assert.True(t, s.Has("b.i")) 176 | assert.False(t, s.Has("c")) 177 | assert.False(t, s.Has("c.i")) 178 | } 179 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package permission 2 | 3 | // InStringSlice checks if the given string is in the given slice of string 4 | func InStringSlice(haystack []string, needle string) bool { 5 | for _, str := range haystack { 6 | if needle == str { 7 | return true 8 | } 9 | } 10 | 11 | return false 12 | } 13 | --------------------------------------------------------------------------------