├── .env ├── .github └── workflows │ ├── build-develop.yml │ ├── build-main.yml │ ├── coverage-main.yml │ ├── test-develop.yml │ └── test-main.yml ├── .gitignore ├── LICENSE ├── README.md ├── authority.go ├── authority_test.go ├── go.mod ├── go.sum ├── permission.go ├── role-permissions.go ├── roles.go └── user-roles.go /.env: -------------------------------------------------------------------------------- 1 | env=testing -------------------------------------------------------------------------------- /.github/workflows/build-develop.yml: -------------------------------------------------------------------------------- 1 | name: build-develop 2 | 3 | on: 4 | push: 5 | branches: [ develop ] 6 | pull_request: 7 | branches: [ develop ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: 1.16 20 | 21 | - name: Build 22 | run: go build -v ./... -------------------------------------------------------------------------------- /.github/workflows/build-main.yml: -------------------------------------------------------------------------------- 1 | name: build-main 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: 1.16 20 | 21 | - name: Build 22 | run: go build -v ./... 23 | -------------------------------------------------------------------------------- /.github/workflows/coverage-main.yml: -------------------------------------------------------------------------------- 1 | name: Code Coverage 2 | on: [push, pull_request] 3 | jobs: 4 | test: 5 | name: Test with Coverage 6 | runs-on: ubuntu-latest 7 | env: 8 | DB_DATABASE: db_test 9 | DB_USER: root 10 | DB_PASSWORD: root 11 | steps: 12 | - name: Set up Go 13 | uses: actions/setup-go@v1 14 | with: 15 | go-version: '1.16' 16 | 17 | - name: Set up Mysql 18 | run: | 19 | sudo /etc/init.d/mysql start 20 | mysql -e 'CREATE DATABASE ${{ env.DB_DATABASE }};' -u${{ env.DB_USER }} -p${{ env.DB_PASSWORD }} 21 | 22 | - name: Check out code 23 | uses: actions/checkout@v2 24 | 25 | - name: Install dependencies 26 | run: | 27 | go mod download 28 | 29 | - name: Run Unit tests 30 | env: 31 | ROOT_PASSWORD: ${{ env.DB_PASSWORD }} 32 | run: | 33 | go test -race -covermode atomic -coverprofile=covprofile ./... 34 | 35 | - name: Install goveralls 36 | env: 37 | GO111MODULE: off 38 | run: go get github.com/mattn/goveralls 39 | # - name: Send coverage 40 | # env: 41 | # COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} 42 | # run: goveralls -coverprofile=covprofile -service=github 43 | # or use shogo82148/actions-goveralls 44 | 45 | - name: Send coverage 46 | uses: shogo82148/actions-goveralls@v1 47 | with: 48 | path-to-profile: covprofile -------------------------------------------------------------------------------- /.github/workflows/test-develop.yml: -------------------------------------------------------------------------------- 1 | name: test-develop 2 | 3 | on: 4 | push: 5 | branches: [develop] 6 | pull_request: 7 | branches: [develop] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | env: 13 | DB_DATABASE: db_test 14 | DB_USER: root 15 | DB_PASSWORD: root 16 | steps: 17 | - uses: actions/checkout@v2 18 | 19 | - name: Set up Go 20 | uses: actions/setup-go@v2 21 | with: 22 | go-version: 1.16 23 | 24 | - name: Set up Mysql 25 | run: | 26 | sudo /etc/init.d/mysql start 27 | mysql -e 'CREATE DATABASE ${{ env.DB_DATABASE }};' -u${{ env.DB_USER }} -p${{ env.DB_PASSWORD }} 28 | 29 | - name: Test 30 | env: 31 | ROOT_PASSWORD: ${{ env.DB_PASSWORD }} 32 | run: go test -v ./... 33 | -------------------------------------------------------------------------------- /.github/workflows/test-main.yml: -------------------------------------------------------------------------------- 1 | name: test-main 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | env: 14 | DB_DATABASE: db_test 15 | DB_USER: root 16 | DB_PASSWORD: root 17 | steps: 18 | - uses: actions/checkout@v2 19 | 20 | - name: Set up Go 21 | uses: actions/setup-go@v2 22 | with: 23 | go-version: 1.16 24 | 25 | - name: Set up Mysql 26 | run: | 27 | sudo /etc/init.d/mysql start 28 | mysql -e 'CREATE DATABASE ${{ env.DB_DATABASE }};' -u${{ env.DB_USER }} -p${{ env.DB_PASSWORD }} 29 | 30 | - name: Test 31 | env: 32 | ROOT_PASSWORD: ${{ env.DB_PASSWORD }} 33 | run: go test -v ./... 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | test/* 2 | .env -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 harranali 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 | # Authority 2 | 3 | ![Build Status](https://github.com/harranali/authority/actions/workflows/build-main.yml/badge.svg) 4 | ![Test Status](https://github.com/harranali/authority/actions/workflows/test-main.yml/badge.svg) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/harranali/authority)](https://goreportcard.com/report/github.com/harranali/authority) 6 | [![GoDoc](https://godoc.org/github.com/harranali/authority?status.svg)](https://godoc.org/github.com/harranali/authority) 7 | [![Coverage Status](https://coveralls.io/repos/github/harranali/authority/badge.svg?branch=main)](https://coveralls.io/github/harranali/authority?branch=main&cache=false) 8 | 9 | Role Based Access Control (RBAC) Go package with database persistence 10 | # Features 11 | - Database Transactions 12 | - Create Roles 13 | - Create Permissions 14 | - Assign Permissions to Roles 15 | - Supports Assigning Multiple Roles to Users 16 | - Check if a user have a given roles 17 | - Check if a user have a given permission 18 | - Check if a role have a given permission 19 | - Revoke User's Roles 20 | - Revoke Role's permissions 21 | - List all roles assigned to a given user 22 | - List all roles in the database 23 | - List all permissions assigned to a given role 24 | - List all Permissions in the database 25 | - Delete a given role 26 | - Delete a given permission 27 | 28 | # Install 29 | 1. Go get the package 30 | ```bash 31 | go get github.com/harranali/authority 32 | ``` 33 | 2. `Authority` uses the `orm` [gorm](https://gorm.io) to communicate with the database. [gorm](https://gorm.io) needs a database driver in order to work properly. you can install the database driver by runnig a command from the list below, for example if you are using `mysql` database, simply run `go get gorm.io/driver/mysql` and so. 34 | ```bash 35 | # mysql 36 | go get gorm.io/driver/mysql 37 | # or postgres 38 | go get gorm.io/driver/postgres 39 | # or sqlite 40 | go get gorm.io/driver/sqlite 41 | # or sqlserver 42 | go get gorm.io/driver/sqlserver 43 | # or clickhouse 44 | go get gorm.io/driver/clickhouse 45 | ``` 46 | 47 | # Usage 48 | To initiate `authority` you need to pass two variables the first one is the the database table names prefix, the second is an instance of [gorm](https://github.com/go-gorm/gorm) 49 | ```go 50 | // initiate the database (using mysql) 51 | dsn := "dbuser:dbpassword@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local" 52 | db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{}) 53 | 54 | // initiate authority 55 | auth := authority.New(authority.Options{ 56 | TablesPrefix: "authority_", 57 | DB: db, 58 | }) 59 | 60 | // create role 61 | err = auth.CreateRole(authority.Role{ 62 | Name: "Role 1", 63 | Slug: "role-1", 64 | }) 65 | 66 | // create permissions 67 | err = auth.CreatePermission(authority.Permission{ 68 | Name: "Permission 1", 69 | Slug: "permission-1", 70 | }) 71 | err = auth.CreatePermission(authority.Permission{ 72 | Name: "Permission 2", 73 | Slug: "permission-2", 74 | }) 75 | err = auth.CreatePermission(authority.Permission{ 76 | Name: "Permission 3", 77 | Slug: "permission-3", 78 | }) 79 | 80 | // assign the permissions to the role 81 | err = auth.AssignPermissionsToRole("role-1", []string{ 82 | "permission-1", 83 | "permission-2", 84 | "permission-3", 85 | }) 86 | 87 | // assign a role to user (user id = 1) 88 | err = auth.AssignRoleToUser(1, "role-1") 89 | 90 | // check if the user have a given role 91 | ok, err := auth.CheckUserRole(1, "role-a") 92 | if ok { 93 | fmt.Println("yes, user has the role assigned") 94 | } 95 | 96 | // check if a user have a given permission 97 | ok, err = auth.CheckUserPermission(1, "permission-d") 98 | if ok { 99 | fmt.Println("yes, user has the permission assigned") 100 | } 101 | 102 | // check if a role have a given permission 103 | ok, err = auth.CheckRolePermission("role-a", "permission-a") 104 | if ok { 105 | fmt.Println("yes, role has the permission assigned") 106 | } 107 | ``` 108 | 109 | # Docs 110 | ### func New(opts Options) *Authority 111 | New initiates authority 112 | ```go 113 | dsn := "dbuser:dbpassword@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local" 114 | db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{}) 115 | 116 | auth := authority.New(authority.Options{ 117 | TablesPrefix: "authority_", 118 | DB: db, 119 | }) 120 | ``` 121 | 122 | ### func Resolve() *Authority 123 | Resolve returns the initiated instance 124 | ```go 125 | auth := authority.Resolve() 126 | ``` 127 | 128 | ### func (a *Authority) CreateRole(r authority.Role) error 129 | Add a new role to the database 130 | it accepts the Role struct as a parameter 131 | it returns an error in case of any 132 | it returns an error if the role is already exists 133 | 134 | ```go 135 | // create role 136 | err = auth.CreateRole(authority.Role{ 137 | Name: "Role 1", 138 | Slug: "role-1", 139 | }) 140 | ``` 141 | 142 | ### func (a *Authority) CreatePermission(p authority.Permission) error 143 | Add a new permission to the database 144 | it accepts the Permission struct as a parameter 145 | it returns an error in case of any 146 | it returns an error if the permission is already exists 147 | ```go 148 | // create a permission 149 | err = auth.CreatePermission(authority.Permission{ 150 | Name: "Permission 1", 151 | Slug: "permission-1", 152 | }) 153 | ``` 154 | 155 | 156 | ### func (a *Authority) AssignPermissionsToRole(roleSlug string, permSlugs []string) error 157 | Assigns a group of permissions to a given role 158 | it accepts the the role slug as the first parameter 159 | the second parameter is a slice of permission slugs (strings) to be assigned to the role 160 | it returns an error in case of any 161 | it returns an error in case the role does not exists 162 | it returns an error in case any of the permissions does not exists 163 | it returns an error in case any of the permissions is already assigned 164 | ```go 165 | // assign the permissions to the role 166 | err := auth.AssignPermissions("role-1", []string{ 167 | "permission-1", 168 | "permission-2", 169 | "permission-3", 170 | }) 171 | ``` 172 | 173 | 174 | ### func (a *Authority) AssignRoleToUser(userID interface{}, roleSlug string) error 175 | Assigns a role to a given user 176 | it accepts the user id as the first parameter 177 | the second parameter the role slug 178 | it returns an error in case of any 179 | it returns an error in case the role does not exists 180 | it returns an error in case the role is already assigned 181 | ```go 182 | // assign a role to user (user id) 183 | err = auth.AssignRoleToUser(1, "role-a") 184 | ``` 185 | 186 | 187 | ### func (a *Authority) CheckUserRole(userID interface{}, roleSlug string) (bool, error) 188 | Checks if a role is assigned to a user 189 | it accepts the user id as the first parameter 190 | the second parameter the role slug 191 | it returns two parameters 192 | the first parameter of the return is a boolean represents whether the role is assigned or not 193 | the second is an error in case of any 194 | in case the role does not exists, an error is returned 195 | ```go 196 | // check if the user have a given role 197 | ok, err := auth.CheckUserRole(1, "role-a") 198 | ``` 199 | 200 | ### func (a *Authority) CheckUserPermission(userID interface{}, permSlug string) (bool, error) 201 | Checks if a permission is assigned to a user 202 | it accepts in the user id as the first parameter 203 | the second parameter the role slug 204 | it returns two parameters 205 | the first parameter of the return is a boolean represents whether the role is assigned or not 206 | the second is an error in case of any 207 | in case the role does not exists, an error is returned 208 | ```go 209 | // check if a user have a given permission 210 | ok, err := auth.CheckUserPermission(1, "permission-d") 211 | ``` 212 | 213 | ### func (a *Authority) CheckRolePermission(roleSlug string, permSlug string) (bool, error) 214 | Checks if a permission is assigned to a role 215 | it accepts in the role slug as the first parameter 216 | the second parameter the permission slug 217 | it returns two parameters 218 | the first parameter of the return is a boolean represents whether the permission is assigned or not 219 | the second is an error in case of any 220 | in case the role does not exists, an error is returned 221 | in case the permission does not exists, an error is returned 222 | ```go 223 | // check if a role have a given permission 224 | ok, err := auth.CheckRolePermission("role-a", "permission-a") 225 | ``` 226 | 227 | ### func (a *Authority) RevokeUserRole(userID interface{}, roleSlug string) error 228 | Revokes a user's role 229 | it returns a error in case of any 230 | in case the role does not exists, an error is returned 231 | ```go 232 | err = auth.RevokeUserRole(1, "role-a") 233 | ``` 234 | 235 | ### func (a *Authority) RevokeRolePermission(roleSlug string, permSlug string) error 236 | Revokes a roles's permission 237 | it returns a error in case of any 238 | in case the role does not exists, an error is returned 239 | in case the permission does not exists, an error is returned 240 | ```go 241 | err = auth.RevokeRolePermission("role-a", "permission-a") 242 | ``` 243 | 244 | ### func (a *Authority) GetAllRoles() ([]Role, error) 245 | Returns all stored roles 246 | it returns an error in case of any 247 | ```go 248 | roles, err := auth.GetAllRoles() 249 | ``` 250 | 251 | ### func (a *Authority) GetUserRoles(userID interface{}) ([]Role, error) 252 | Returns all user assigned roles 253 | it returns an error in case of any 254 | ```go 255 | roles, err := auth.GetUserRoles(1) 256 | ``` 257 | 258 | ### func (a *Authority) GetRolePermissions(roleSlug string) ([]Permission, error) 259 | Returns all role assigned permissions 260 | it returns an error in case of any 261 | ```go 262 | permissions, err := auth.GetRolePermissions("role-a") 263 | ``` 264 | 265 | ### func (a *Authority) GetAllPermissions() ([]Permission, error) 266 | Returns all stored permissions 267 | it returns an error in case of any 268 | ```go 269 | permissions, err := auth.GetAllPermissions() 270 | ``` 271 | 272 | ### func (a *Authority) DeleteRole(roleSlug string) error 273 | Deletes a given role even if it's has assigned permissions 274 | it first deassign the permissions and then proceed with deleting the role 275 | it accepts the role slug as a parameter 276 | it returns an error in case of any 277 | if the role is assigned to a user it returns an error 278 | ```go 279 | err := auth.DeleteRole("role-b") 280 | ``` 281 | 282 | ### func (a *Authority) DeletePermission(permSlug string) error 283 | Deletes a given permission 284 | it accepts the permission slug as a parameter 285 | it returns an error in case of any 286 | if the permission is assigned to a role it returns an error 287 | ```go 288 | err := auth.DeletePermission("permission-c") 289 | ``` 290 | 291 | ### Transactions 292 | `authority` supports database transactions by implementing 3 methods `BeginTX()`, `Rollback()`, and `Commit()` 293 | here is an example of how to use transactions 294 | ```go 295 | 296 | // begin a transaction session 297 | tx := auth.BeginTX() 298 | // create role 299 | err = tx.CreateRole(authority.Role{ 300 | Name: "Role 1", 301 | Slug: "role-1", 302 | }) 303 | if err != nil { 304 | tx.Rollback() // transaction rollback incase of error 305 | fmt.Println("error creating role", err) 306 | } 307 | 308 | // create permissions 309 | err = tx.CreatePermission(authority.Permission{ 310 | Name: "Permission 1", 311 | Slug: "permission-1", 312 | }) 313 | if err != nil { 314 | tx.Rollback() // transaction rollback incase of error 315 | fmt.Println("error creating permission", err) 316 | } 317 | 318 | // assign the permissions to the role 319 | err = tx.AssignPermissionsToRole("role-1", []string{ 320 | "permission-1", 321 | "permission-2", 322 | "permission-3", 323 | }) 324 | 325 | if err != nil { 326 | tx.Rollback() // transaction rollback incase of error 327 | fmt.Println("error assigning permission to role", err) 328 | } 329 | // assign a role to user (user id = 1) 330 | err = tx.AssignRoleToUser(1, "role-1") 331 | if err != nil { 332 | tx.Rollback() // transaction rollback incase of error 333 | fmt.Println("error assigning role to user", err) 334 | } 335 | 336 | // commit the operations to the database 337 | tx.Commit() 338 | ``` -------------------------------------------------------------------------------- /authority.go: -------------------------------------------------------------------------------- 1 | package authority 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "gorm.io/gorm" 8 | ) 9 | 10 | // Authority helps deal with permissions 11 | type Authority struct { 12 | TablesPrefix string 13 | DB *gorm.DB 14 | } 15 | 16 | // Options has the options for initiating the package 17 | type Options struct { 18 | TablesPrefix string 19 | DB *gorm.DB 20 | } 21 | 22 | var ( 23 | ErrPermissionInUse = errors.New("cannot delete assigned permission") 24 | ErrPermissionNotFound = errors.New("permission not found") 25 | ErrRoleInUse = errors.New("cannot delete assigned role") 26 | ErrRoleNotFound = errors.New("role not found") 27 | ) 28 | 29 | var tablePrefix string 30 | 31 | var auth *Authority 32 | var options Options 33 | var tx *gorm.DB 34 | 35 | // New initiates authority 36 | func New(opts Options) *Authority { 37 | options = opts 38 | auth = &Authority{ 39 | TablesPrefix: options.TablesPrefix, 40 | DB: opts.DB, 41 | } 42 | 43 | migrateTables(opts.DB) 44 | return auth 45 | } 46 | 47 | // New initiates new instance of authority 48 | func newInstance(opts Options) *Authority { 49 | newAuth := &Authority{ 50 | DB: opts.DB, 51 | } 52 | 53 | migrateTables(opts.DB) 54 | return newAuth 55 | } 56 | 57 | // Resolve returns the initiated instance 58 | func Resolve() *Authority { 59 | return auth 60 | } 61 | 62 | // Add a new role to the database 63 | // it accepts the Role struct as a parameter 64 | // it returns an error in case of any 65 | // it returns an error if the role is already exists 66 | func (a *Authority) CreateRole(r Role) error { 67 | roleSlug := r.Slug 68 | var dbRole Role 69 | res := a.DB.Where("slug = ?", roleSlug).First(&dbRole) 70 | if res.Error != nil { 71 | if errors.Is(res.Error, gorm.ErrRecordNotFound) { 72 | // create 73 | createRes := a.DB.Create(&r) 74 | if createRes.Error != nil { 75 | return createRes.Error 76 | } 77 | return nil 78 | } 79 | return res.Error 80 | } 81 | 82 | return errors.New(fmt.Sprintf("role '%v' already exists", roleSlug)) 83 | } 84 | 85 | // Add a new permission to the database 86 | // it accepts the Permission struct as a parameter 87 | // it returns an error in case of any 88 | // it returns an error if the permission is already exists 89 | func (a *Authority) CreatePermission(p Permission) error { 90 | permSlug := p.Slug 91 | var dbPerm Permission 92 | res := a.DB.Where("slug = ?", permSlug).First(&dbPerm) 93 | if res.Error != nil { 94 | if errors.Is(res.Error, gorm.ErrRecordNotFound) { 95 | // create 96 | createRes := a.DB.Create(&p) 97 | if createRes.Error != nil { 98 | return createRes.Error 99 | } 100 | return nil 101 | } 102 | return res.Error 103 | } 104 | 105 | return errors.New(fmt.Sprintf("permission '%v' already exists", permSlug)) 106 | } 107 | 108 | // Assigns a group of permissions to a given role 109 | // it accepts the the role slug as the first parameter 110 | // the second parameter is a slice of permission slugs (strings) to be assigned to the role 111 | // it returns an error in case of any 112 | // it returns an error in case the role does not exists 113 | // it returns an error in case any of the permissions does not exists 114 | // it returns an error in case any of the permissions is already assigned 115 | func (a *Authority) AssignPermissionsToRole(roleSlug string, permSlugs []string) error { 116 | var role Role 117 | rRes := a.DB.Where("slug = ?", roleSlug).First(&role) 118 | if rRes.Error != nil { 119 | if errors.Is(rRes.Error, gorm.ErrRecordNotFound) { 120 | return ErrRoleNotFound 121 | } 122 | return rRes.Error 123 | } 124 | var perms []Permission 125 | for _, permSlug := range permSlugs { 126 | var perm Permission 127 | pRes := a.DB.Where("slug = ?", permSlug).First(&perm) 128 | if pRes.Error != nil { 129 | if errors.Is(pRes.Error, gorm.ErrRecordNotFound) { 130 | return ErrPermissionNotFound 131 | } 132 | return pRes.Error 133 | } 134 | perms = append(perms, perm) 135 | } 136 | tx := a.DB.Begin() 137 | for _, perm := range perms { 138 | var rolePerm RolePermission 139 | res := a.DB.Where("role_id = ?", role.ID).Where("permission_id =?", perm.ID).First(&rolePerm) 140 | if res.Error != nil && errors.Is(res.Error, gorm.ErrRecordNotFound) { 141 | cRes := tx.Create(&RolePermission{RoleID: role.ID, PermissionID: perm.ID}) 142 | if cRes.Error != nil { 143 | tx.Rollback() 144 | return cRes.Error 145 | } 146 | } 147 | if res.Error != nil && !errors.Is(res.Error, gorm.ErrRecordNotFound) { 148 | tx.Rollback() 149 | return res.Error 150 | } 151 | if rolePerm != (RolePermission{}) { 152 | tx.Rollback() 153 | return errors.New(fmt.Sprintf("permission '%v' is aleady assigned to the role '%v'", perm.Name, role.Name)) 154 | } 155 | rolePerm = RolePermission{} 156 | } 157 | return tx.Commit().Error 158 | } 159 | 160 | // Assigns a role to a given user 161 | // it accepts the user id as the first parameter 162 | // the second parameter the role slug 163 | // it returns an error in case of any 164 | // it returns an error in case the role does not exists 165 | // it returns an error in case the role is already assigned 166 | func (a *Authority) AssignRoleToUser(userID interface{}, roleSlug string) error { 167 | userIDStr := fmt.Sprintf("%v", userID) 168 | var role Role 169 | res := a.DB.Where("slug = ?", roleSlug).First(&role) 170 | if res.Error != nil { 171 | if errors.Is(res.Error, gorm.ErrRecordNotFound) { 172 | return ErrRoleNotFound 173 | } 174 | return res.Error 175 | } 176 | var userRole UserRole 177 | res = a.DB.Where("user_id = ?", userIDStr).Where("role_id = ?", role.ID).First(&userRole) 178 | if res.Error != nil && errors.Is(res.Error, gorm.ErrRecordNotFound) { 179 | a.DB.Create(&UserRole{UserID: userIDStr, RoleID: role.ID}) 180 | return nil 181 | } 182 | if res.Error != nil && !errors.Is(res.Error, gorm.ErrRecordNotFound) { 183 | return res.Error 184 | } 185 | 186 | return errors.New(fmt.Sprintf("this role '%v' is aleady assigned to the user", roleSlug)) 187 | } 188 | 189 | // Checks if a role is assigned to a user 190 | // it accepts the user id as the first parameter 191 | // the second parameter the role slug 192 | // it returns two parameters 193 | // the first parameter of the return is a boolean represents whether the role is assigned or not 194 | // the second is an error in case of any 195 | // in case the role does not exists, an error is returned 196 | func (a *Authority) CheckUserRole(userID interface{}, roleSlug string) (bool, error) { 197 | userIDStr := fmt.Sprintf("%v", userID) 198 | // find the role 199 | var role Role 200 | res := a.DB.Where("slug = ?", roleSlug).First(&role) 201 | if res.Error != nil { 202 | if errors.Is(res.Error, gorm.ErrRecordNotFound) { 203 | return false, ErrRoleNotFound 204 | } 205 | return false, res.Error 206 | } 207 | 208 | // check if the role is a assigned 209 | var userRole UserRole 210 | res = a.DB.Where("user_id = ?", userIDStr).Where("role_id = ?", role.ID).First(&userRole) 211 | if res.Error != nil { 212 | if errors.Is(res.Error, gorm.ErrRecordNotFound) { 213 | return false, nil 214 | } 215 | return false, res.Error 216 | } 217 | 218 | return true, nil 219 | } 220 | 221 | // Checks if a permission is assigned to a user 222 | // it accepts in the user id as the first parameter 223 | // the second parameter the role slug 224 | // it returns two parameters 225 | // the first parameter of the return is a boolean represents whether the role is assigned or not 226 | // the second is an error in case of any 227 | // in case the role does not exists, an error is returned 228 | func (a *Authority) CheckUserPermission(userID interface{}, permSlug string) (bool, error) { 229 | userIDStr := fmt.Sprintf("%v", userID) 230 | // the user role 231 | var userRoles []UserRole 232 | res := a.DB.Where("user_id = ?", userIDStr).Find(&userRoles) 233 | if res.Error != nil { 234 | if errors.Is(res.Error, gorm.ErrRecordNotFound) { 235 | return false, nil 236 | } 237 | return false, res.Error 238 | } 239 | 240 | //prepare an array of role ids 241 | var roleIDs []interface{} 242 | for _, r := range userRoles { 243 | roleIDs = append(roleIDs, r.RoleID) 244 | } 245 | 246 | // find the permission 247 | var perm Permission 248 | res = a.DB.Where("slug = ?", permSlug).First(&perm) 249 | if res.Error != nil { 250 | if errors.Is(res.Error, gorm.ErrRecordNotFound) { 251 | return false, ErrPermissionNotFound 252 | } 253 | return false, res.Error 254 | } 255 | 256 | // find the role permission 257 | var rolePermission RolePermission 258 | res = a.DB.Where("role_id IN (?)", roleIDs).Where("permission_id = ?", perm.ID).First(&rolePermission) 259 | if res.Error != nil && errors.Is(res.Error, gorm.ErrRecordNotFound) { 260 | return false, nil 261 | } 262 | if res.Error != nil && !errors.Is(res.Error, gorm.ErrRecordNotFound) { 263 | return false, res.Error 264 | } 265 | 266 | return true, nil 267 | } 268 | 269 | // Checks if a permission is assigned to a role 270 | // it accepts in the role slug as the first parameter 271 | // the second parameter the permission slug 272 | // it returns two parameters 273 | // the first parameter of the return is a boolean represents whether the permission is assigned or not 274 | // the second is an error in case of any 275 | // in case the role does not exists, an error is returned 276 | // in case the permission does not exists, an error is returned 277 | func (a *Authority) CheckRolePermission(roleSlug string, permSlug string) (bool, error) { 278 | // find the role 279 | var role Role 280 | res := a.DB.Where("slug = ?", roleSlug).First(&role) 281 | if res.Error != nil { 282 | if errors.Is(res.Error, gorm.ErrRecordNotFound) { 283 | return false, ErrRoleNotFound 284 | } 285 | return false, res.Error 286 | } 287 | 288 | // find the permission 289 | var perm Permission 290 | res = a.DB.Where("slug = ?", permSlug).First(&perm) 291 | if res.Error != nil { 292 | if errors.Is(res.Error, gorm.ErrRecordNotFound) { 293 | return false, ErrPermissionNotFound 294 | } 295 | return false, res.Error 296 | } 297 | 298 | // find the rolePermission 299 | var rolePermission RolePermission 300 | res = a.DB.Where("role_id = ?", role.ID).Where("permission_id = ?", perm.ID).First(&rolePermission) 301 | if res.Error != nil { 302 | if errors.Is(res.Error, gorm.ErrRecordNotFound) { 303 | return false, nil 304 | } 305 | return false, res.Error 306 | } 307 | 308 | return true, nil 309 | } 310 | 311 | // Revokes a user's role 312 | // it returns a error in case of any 313 | // in case the role does not exists, an error is returned 314 | func (a *Authority) RevokeUserRole(userID interface{}, roleSlug string) error { 315 | userIDStr := fmt.Sprintf("%v", userID) 316 | // find the role 317 | var role Role 318 | res := a.DB.Where("slug = ?", roleSlug).First(&role) 319 | if res.Error != nil { 320 | if errors.Is(res.Error, gorm.ErrRecordNotFound) { 321 | return ErrRoleNotFound 322 | } 323 | return res.Error 324 | } 325 | 326 | // revoke the role 327 | rRes := a.DB.Where("user_id = ?", userIDStr).Where("role_id = ?", role.ID).Delete(UserRole{}) 328 | if rRes.Error != nil { 329 | return rRes.Error 330 | } 331 | 332 | return nil 333 | } 334 | 335 | // Revokes a roles's permission 336 | // it returns a error in case of any 337 | // in case the role does not exists, an error is returned 338 | // in case the permission does not exists, an error is returned 339 | func (a *Authority) RevokeRolePermission(roleSlug string, permSlug string) error { 340 | // find the role 341 | var role Role 342 | res := a.DB.Where("slug = ?", roleSlug).First(&role) 343 | if res.Error != nil { 344 | if errors.Is(res.Error, gorm.ErrRecordNotFound) { 345 | return ErrRoleNotFound 346 | } 347 | return res.Error 348 | } 349 | 350 | // find the permission 351 | var perm Permission 352 | res = a.DB.Where("slug = ?", permSlug).First(&perm) 353 | if res.Error != nil { 354 | if errors.Is(res.Error, gorm.ErrRecordNotFound) { 355 | return ErrPermissionNotFound 356 | } 357 | return res.Error 358 | } 359 | 360 | // revoke the permission 361 | rRes := a.DB.Where("role_id = ?", role.ID).Where("permission_id = ?", perm.ID).Delete(RolePermission{}) 362 | if rRes.Error != nil { 363 | return rRes.Error 364 | } 365 | 366 | return nil 367 | } 368 | 369 | // Returns all stored roles 370 | // it returns an error in case of any 371 | func (a *Authority) GetAllRoles() ([]Role, error) { 372 | var roles []Role 373 | res := a.DB.Find(&roles) 374 | if res.Error != nil { 375 | return nil, res.Error 376 | } 377 | 378 | return roles, nil 379 | } 380 | 381 | // Returns all user assigned roles 382 | // it returns an error in case of any 383 | func (a *Authority) GetUserRoles(userID interface{}) ([]Role, error) { 384 | userIDStr := fmt.Sprintf("%v", userID) 385 | var userRoles []UserRole 386 | res := a.DB.Where("user_id = ?", userIDStr).Find(&userRoles) 387 | if res.Error != nil { 388 | return nil, res.Error 389 | } 390 | 391 | var roleIDs []interface{} 392 | for _, r := range userRoles { 393 | roleIDs = append(roleIDs, r.RoleID) 394 | } 395 | 396 | var roles []Role 397 | res = a.DB.Where("id IN (?)", roleIDs).Find(&roles) 398 | if res.Error != nil { 399 | return nil, res.Error 400 | } 401 | 402 | return roles, nil 403 | } 404 | 405 | // Returns all role assigned permissions 406 | // it returns an error in case of any 407 | func (a *Authority) GetRolePermissions(roleSlug string) ([]Permission, error) { 408 | var role Role 409 | res := a.DB.Where("slug = ?", roleSlug).Find(&role) 410 | if res.Error != nil { 411 | return nil, res.Error 412 | } 413 | 414 | var rolePerms []RolePermission 415 | res = a.DB.Where("role_id = ?", role.ID).Find(&rolePerms) 416 | if res.Error != nil { 417 | return nil, res.Error 418 | } 419 | var permIDs []interface{} 420 | for _, rolePerm := range rolePerms { 421 | permIDs = append(permIDs, rolePerm.PermissionID) 422 | } 423 | 424 | var perms []Permission 425 | res = a.DB.Where("id IN (?)", permIDs).Find(&perms) 426 | if res.Error != nil { 427 | return nil, res.Error 428 | } 429 | 430 | return perms, nil 431 | } 432 | 433 | // Returns all stored permissions 434 | // it returns an error in case of any 435 | func (a *Authority) GetAllPermissions() ([]Permission, error) { 436 | var perms []Permission 437 | res := a.DB.Find(&perms) 438 | if res.Error != nil { 439 | return nil, res.Error 440 | } 441 | 442 | return perms, nil 443 | } 444 | 445 | // Deletes a given role even if it's has assigned permissions 446 | // it first deassign the permissions and then proceed with deleting the role 447 | // it accepts the role slug as a parameter 448 | // it returns an error in case of any 449 | // if the role is assigned to a user it returns an error 450 | func (a *Authority) DeleteRole(roleSlug string) error { 451 | // find the role 452 | var role Role 453 | res := a.DB.Where("slug = ?", roleSlug).First(&role) 454 | if res.Error != nil { 455 | return res.Error 456 | } 457 | 458 | // check if the role is assigned to a user 459 | var c int64 460 | res = a.DB.Model(UserRole{}).Where("role_id = ?", role.ID).Count(&c) 461 | if res.Error != nil { 462 | return res.Error 463 | } 464 | 465 | if c != 0 { 466 | // role is assigned 467 | return ErrRoleInUse 468 | } 469 | tx := a.DB.Begin() 470 | // revoke the assignment of permissions before deleting the role 471 | dRes := tx.Where("role_id = ?", role.ID).Delete(RolePermission{}) 472 | if dRes.Error != nil { 473 | tx.Rollback() 474 | return dRes.Error 475 | } 476 | 477 | // delete the role 478 | dRes = a.DB.Where("slug = ?", roleSlug).Delete(Role{}) 479 | if dRes.Error != nil { 480 | tx.Rollback() 481 | return dRes.Error 482 | } 483 | 484 | return tx.Commit().Error 485 | } 486 | 487 | // Deletes a given permission 488 | // it accepts the permission slug as a parameter 489 | // it returns an error in case of any 490 | // if the permission is assigned to a role it returns an error 491 | func (a *Authority) DeletePermission(permSlug string) error { 492 | // find the permission 493 | var perm Permission 494 | res := a.DB.Where("slug = ?", permSlug).First(&perm) 495 | if res.Error != nil { 496 | return res.Error 497 | } 498 | 499 | // check if the permission is assigned to a role 500 | var rolePermission RolePermission 501 | res = a.DB.Where("permission_id = ?", perm.ID).First(&rolePermission) 502 | if res.Error != nil && !errors.Is(res.Error, gorm.ErrRecordNotFound) { 503 | return res.Error 504 | } 505 | 506 | if res.Error == nil { 507 | return ErrPermissionInUse 508 | } 509 | 510 | // delete the permission 511 | dRes := a.DB.Where("slug = ?", permSlug).Delete(Permission{}) 512 | if dRes.Error != nil { 513 | return dRes.Error 514 | } 515 | 516 | return nil 517 | } 518 | 519 | // Begin a transaction session 520 | func (a *Authority) BeginTX() *Authority { 521 | tx = options.DB.Begin() 522 | newAuth := newInstance(Options{ 523 | TablesPrefix: options.TablesPrefix, 524 | DB: tx, 525 | }) 526 | 527 | return newAuth 528 | } 529 | 530 | // Rolback previous queries 531 | func (a *Authority) Rollback() error { 532 | return tx.Rollback().Error 533 | } 534 | 535 | // Commit queries to the database 536 | func (a *Authority) Commit() error { 537 | return tx.Commit().Error 538 | } 539 | 540 | func migrateTables(db *gorm.DB) { 541 | db.AutoMigrate(&Role{}) 542 | db.AutoMigrate(&Permission{}) 543 | db.AutoMigrate(&RolePermission{}) 544 | db.AutoMigrate(&UserRole{}) 545 | } 546 | -------------------------------------------------------------------------------- /authority_test.go: -------------------------------------------------------------------------------- 1 | package authority_test 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "testing" 8 | 9 | "github.com/harranali/authority" 10 | "github.com/joho/godotenv" 11 | "gorm.io/driver/mysql" 12 | "gorm.io/gorm" 13 | "gorm.io/gorm/logger" 14 | ) 15 | 16 | var db *gorm.DB 17 | 18 | func TestMain(m *testing.M) { 19 | err := godotenv.Load() 20 | if err != nil { 21 | log.Fatal("Error loading .env file") 22 | } 23 | var dsn string 24 | if os.Getenv("env") == "testing" { 25 | fmt.Println("preparing testing config...") 26 | dsn = fmt.Sprintf("root:%s@tcp(127.0.0.1:3306)/db_test?charset=utf8mb4&parseTime=True&loc=Local", 27 | os.Getenv("ROOT_PASSWORD")) 28 | } else { 29 | dsn = "root:root@tcp(127.0.0.1:3306)/db_test?charset=utf8mb4&parseTime=True&loc=Local" 30 | } 31 | 32 | db, _ = gorm.Open(mysql.Open(dsn), &gorm.Config{ 33 | Logger: logger.Default.LogMode(logger.Silent), 34 | }) 35 | 36 | // call flag.Parse() here if TestMain uses flags 37 | os.Exit(m.Run()) 38 | } 39 | 40 | func TestCreateRole(t *testing.T) { 41 | auth := authority.New(authority.Options{ 42 | TablesPrefix: "authority_", 43 | DB: db, 44 | }) 45 | 46 | // test create role 47 | err := auth.CreateRole(authority.Role{ 48 | Name: "Role A", 49 | Slug: "role-a", 50 | }) 51 | if err != nil { 52 | t.Error("an error was not expected while creating role ", err) 53 | } 54 | 55 | var c int64 56 | res := db.Model(authority.Role{}).Where("slug = ?", "role-a").Count(&c) 57 | if res.Error != nil { 58 | t.Error("failed test create role", res.Error) 59 | } 60 | if c == 0 { 61 | t.Error("failed test create role ") 62 | } 63 | 64 | // test duplicated entries 65 | err = auth.CreateRole(authority.Role{ 66 | Name: "Role A", 67 | Slug: "role-a", 68 | }) 69 | if err == nil { 70 | t.Error("failed test create role") 71 | } 72 | 73 | t.Cleanup(func() { 74 | // clean up 75 | db.Where("slug = ?", "role-a").Delete(authority.Role{}) 76 | }) 77 | 78 | } 79 | 80 | func TestCreatePermission(t *testing.T) { 81 | auth := authority.New(authority.Options{ 82 | TablesPrefix: "authority_", 83 | DB: db, 84 | }) 85 | 86 | err := auth.CreatePermission(authority.Permission{ 87 | Name: "Permission A", 88 | Slug: "permission-a", 89 | }) 90 | if err != nil { 91 | t.Error("failed test create permission", err) 92 | } 93 | 94 | var c int64 95 | res := db.Model(authority.Permission{}).Where("slug = ?", "permission-a").Count(&c) 96 | if res.Error != nil { 97 | t.Error("failed test create permission", res.Error) 98 | } 99 | if c != 1 { 100 | t.Error("permission has not been stored") 101 | } 102 | 103 | // test duplicated entries 104 | err = auth.CreatePermission(authority.Permission{ 105 | Name: "Permission A", 106 | Slug: "permission-a", 107 | }) 108 | if err == nil { 109 | t.Error("failed test create permission") 110 | } 111 | 112 | db.Model(authority.Role{}).Where("slug = ?", "permission-a").Count(&c) 113 | if c > 1 { 114 | t.Error("failed test create permission") 115 | } 116 | 117 | t.Cleanup(func() { 118 | // clean up 119 | db.Where("slug = ?", "permission-a").Delete(authority.Permission{}) 120 | }) 121 | 122 | } 123 | 124 | func TestAssignPermissionToRole(t *testing.T) { 125 | auth := authority.New(authority.Options{ 126 | TablesPrefix: "authority_", 127 | DB: db, 128 | }) 129 | err := auth.CreateRole(authority.Role{ 130 | Name: "Role A", 131 | Slug: "role-a", 132 | }) 133 | if err != nil { 134 | t.Error("failed test assign permission to role", err) 135 | } 136 | err = auth.CreatePermission(authority.Permission{ 137 | Name: "Permission A", 138 | Slug: "permission-a", 139 | }) 140 | if err != nil { 141 | t.Error("failed test assign permission to role", err) 142 | } 143 | err = auth.CreatePermission(authority.Permission{ 144 | Name: "Permission B", 145 | Slug: "permission-b", 146 | }) 147 | if err != nil { 148 | t.Error("failed test assign permission to role", err) 149 | } 150 | 151 | // assign the permissions 152 | err = auth.AssignPermissionsToRole("role-a", []string{"permission-a", "permission-b"}) 153 | if err != nil { 154 | t.Error("failed test assign permission to role", err) 155 | } 156 | 157 | // assign to missing role 158 | err = auth.AssignPermissionsToRole("role-aa", []string{"permission-a", "permission-b"}) 159 | if err == nil { 160 | t.Error("failed test assign permission to role") 161 | } 162 | 163 | // assign missing permission 164 | err = auth.AssignPermissionsToRole("role-a", []string{"permission-aa"}) 165 | if err == nil { 166 | t.Error("failed test assign permission to role") 167 | } 168 | 169 | var r authority.Role 170 | db.Where("slug = ?", "role-a").First(&r) 171 | var rolePermsCount int64 172 | res := db.Model(authority.RolePermission{}).Where("role_id = ?", r.ID).Count(&rolePermsCount) 173 | if res.Error != nil { 174 | t.Error("failed test assign permission to role", res.Error) 175 | } 176 | if rolePermsCount != 2 { 177 | t.Error("failed test assign permission to role", err) 178 | } 179 | 180 | t.Cleanup(func() { 181 | // clean up 182 | db.Where("role_id = ?", r.ID).Delete(authority.RolePermission{}) 183 | db.Where("slug = ?", "role-a").Delete(authority.Role{}) 184 | db.Where("slug = ?", "permission-a").Delete(authority.Permission{}) 185 | db.Where("slug = ?", "permission-b").Delete(authority.Permission{}) 186 | }) 187 | } 188 | 189 | func TestAssignRoleToUser(t *testing.T) { 190 | auth := authority.New(authority.Options{ 191 | TablesPrefix: "authority_", 192 | DB: db, 193 | }) 194 | // first create a role 195 | err := auth.CreateRole(authority.Role{ 196 | Name: "Role A", 197 | Slug: "role-a", 198 | }) 199 | if err != nil { 200 | t.Error("failed test assign role to user", err) 201 | } 202 | 203 | // assign the role 204 | err = auth.AssignRoleToUser(1, "role-a") 205 | if err != nil { 206 | t.Error("failed test assign role to user", err) 207 | } 208 | 209 | // double assign the role 210 | err = auth.AssignRoleToUser(1, "role-a") 211 | if err == nil { 212 | t.Error("failed test assign role to user") 213 | } 214 | 215 | // assign a second role 216 | auth.CreateRole(authority.Role{ 217 | Name: "Role B", 218 | Slug: "role-b", 219 | }) 220 | err = auth.AssignRoleToUser(1, "role-b") 221 | if err != nil { 222 | t.Error("failed test assign role to user", err) 223 | } 224 | 225 | // assign missing role 226 | err = auth.AssignRoleToUser(1, "role-aa") 227 | if err == nil { 228 | t.Error("failed test assign role to user") 229 | } 230 | 231 | var r authority.Role 232 | res := db.Where("slug = ?", "role-a").First(&r) 233 | if res.Error != nil { 234 | t.Error("failed test assign role to user", res.Error) 235 | } 236 | var userRoles int64 237 | res = db.Model(authority.UserRole{}).Where("user_id = ?", 1).Count(&userRoles) 238 | if res.Error != nil { 239 | t.Error("failed test assign role to user", err) 240 | } 241 | if userRoles != 2 { 242 | t.Error("failed test assign role to user") 243 | } 244 | 245 | t.Cleanup(func() { 246 | //clean up 247 | db.Where("user_id = ?", 1).Delete(authority.UserRole{}) 248 | db.Where("slug = ?", "role-a").Delete(authority.Role{}) 249 | db.Where("slug = ?", "role-b").Delete(authority.Role{}) 250 | }) 251 | 252 | } 253 | 254 | func TestCheckUserRole(t *testing.T) { 255 | auth := authority.New(authority.Options{ 256 | TablesPrefix: "authority_", 257 | DB: db, 258 | }) 259 | 260 | // first create a role and assign it to a user 261 | err := auth.CreateRole(authority.Role{ 262 | Name: "Role A", 263 | Slug: "role-a", 264 | }) 265 | if err != nil { 266 | t.Error("failed test check user role", err) 267 | } 268 | // assign the role 269 | err = auth.AssignRoleToUser(1, "role-a") 270 | if err != nil { 271 | t.Error("failed test check user role", err) 272 | } 273 | 274 | // assert 275 | ok, err := auth.CheckUserRole(1, "role-a") 276 | if err != nil { 277 | t.Error("failed test check user role", err) 278 | } 279 | if !ok { 280 | t.Error("failed test check user role") 281 | } 282 | 283 | // check not exist assigned role 284 | err = auth.CreateRole(authority.Role{ 285 | Name: "Role B", 286 | Slug: "role-b", 287 | }) 288 | if err != nil { 289 | t.Error("failed test check user role", err) 290 | } 291 | ok, err = auth.CheckUserRole(1, "role-b") 292 | if err != nil { 293 | t.Error("failed test check user role", err) 294 | } 295 | if ok { 296 | t.Error("failed test check user role") 297 | } 298 | 299 | // check aa missing role 300 | _, err = auth.CheckUserRole(1, "role-aa") 301 | if err == nil { 302 | t.Error("failed test check user role") 303 | } 304 | 305 | // check a missing user 306 | ok, _ = auth.CheckUserRole(11, "role-a") 307 | if ok { 308 | t.Error("failed test check user role") 309 | } 310 | 311 | t.Cleanup(func() { 312 | // clean up 313 | var r authority.Role 314 | db.Where("slug = ?", "role-a").First(&r) 315 | db.Where("role_id = ?", r.ID).Delete(authority.UserRole{}) 316 | db.Where("slug = ?", "role-a").Delete(authority.Role{}) 317 | db.Where("slug = ?", "role-b").Delete(authority.Role{}) 318 | }) 319 | } 320 | 321 | // check user permission 322 | func TestCheckUserPermission(t *testing.T) { 323 | auth := authority.New(authority.Options{ 324 | TablesPrefix: "authority_", 325 | DB: db, 326 | }) 327 | 328 | // first create a role 329 | err := auth.CreateRole(authority.Role{ 330 | Name: "Role A", 331 | Slug: "role-a", 332 | }) 333 | if err != nil { 334 | t.Error("failed test check user permission", err) 335 | } 336 | 337 | //create permissions 338 | err = auth.CreatePermission(authority.Permission{ 339 | Name: "Permission A", 340 | Slug: "permission-a", 341 | }) 342 | if err != nil { 343 | t.Error("failed test check user permission", err) 344 | } 345 | err = auth.CreatePermission(authority.Permission{ 346 | Name: "Permission B", 347 | Slug: "permission-b", 348 | }) 349 | if err != nil { 350 | t.Error("failed test check user permission", err) 351 | } 352 | 353 | // assign the permissions 354 | err = auth.AssignPermissionsToRole("role-a", []string{"permission-a", "permission-b"}) 355 | if err != nil { 356 | t.Error("failed test check user permission", err) 357 | } 358 | 359 | // test when no role is a ssigned 360 | ok, err := auth.CheckUserPermission(1, "permission-a") 361 | if err != nil { 362 | t.Error("failed test check user permission", err) 363 | } 364 | if ok { 365 | t.Error("failed test check user permission") 366 | } 367 | 368 | // assign the role 369 | err = auth.AssignRoleToUser(1, "role-a") 370 | if err != nil { 371 | t.Error("failed test check user permission", err) 372 | } 373 | 374 | // test a permission of an assigned role 375 | ok, err = auth.CheckUserPermission(1, "permission-a") 376 | if err != nil { 377 | t.Error("failed test check user permission", err) 378 | } 379 | if !ok { 380 | t.Error("failed test check user permission") 381 | } 382 | 383 | // test assigning missing permission 384 | _, err = auth.CheckUserPermission(1, "permission-aa") 385 | if err == nil { 386 | t.Error("failed test check user permission") 387 | } 388 | 389 | t.Cleanup(func() { 390 | // clean up 391 | var r authority.Role 392 | db.Where("slug = ?", "role-a").First(&r) 393 | db.Where("role_id = ?", r.ID).Delete(authority.UserRole{}) 394 | db.Where("role_id = ?", r.ID).Delete(authority.RolePermission{}) 395 | db.Where("slug = ?", "permission-a").Delete(authority.Permission{}) 396 | db.Where("slug = ?", "permission-b").Delete(authority.Permission{}) 397 | db.Where("slug = ?", "role-a").Delete(authority.Role{}) 398 | }) 399 | 400 | } 401 | 402 | func TestCheckRolePermission(t *testing.T) { 403 | auth := authority.New(authority.Options{ 404 | TablesPrefix: "authority_", 405 | DB: db, 406 | }) 407 | 408 | // first create a role 409 | err := auth.CreateRole(authority.Role{ 410 | Name: "Role A", 411 | Slug: "role-a", 412 | }) 413 | if err != nil { 414 | t.Error("failed test check role permission", err) 415 | } 416 | 417 | // second test create permissions 418 | err = auth.CreatePermission(authority.Permission{ 419 | Name: "Permission A", 420 | Slug: "permission-a", 421 | }) 422 | if err != nil { 423 | t.Error("failed test check role permission", err) 424 | } 425 | err = auth.CreatePermission(authority.Permission{ 426 | Name: "Permission B", 427 | Slug: "permission-b", 428 | }) 429 | if err != nil { 430 | t.Error("failed test check role permission", err) 431 | } 432 | 433 | // third assign the permissions 434 | err = auth.AssignPermissionsToRole("role-a", []string{"permission-a", "permission-b"}) 435 | if err != nil { 436 | t.Error("failed test check role permission", err) 437 | } 438 | 439 | // check the role permission 440 | ok, err := auth.CheckRolePermission("role-a", "permission-a") 441 | if err != nil { 442 | t.Error("failed test check role permission", err) 443 | } 444 | if !ok { 445 | t.Error("failed test check role permission") 446 | } 447 | 448 | // check a missing role 449 | _, err = auth.CheckRolePermission("role-aa", "permission-a") 450 | if err == nil { 451 | t.Error("failed test check role permission") 452 | } 453 | 454 | // check with missing permission 455 | _, err = auth.CheckRolePermission("role-a", "permission-aa") 456 | if err == nil { 457 | t.Error("failed test check role permission", err) 458 | } 459 | 460 | // check with not assigned permission 461 | auth.CreatePermission(authority.Permission{ 462 | Name: "Permission C", 463 | Slug: "permission-c", 464 | }) 465 | ok, _ = auth.CheckRolePermission("role-a", "permission-c") 466 | if ok { 467 | t.Error("failed test check role permission") 468 | } 469 | 470 | t.Cleanup(func() { 471 | //clean up 472 | var r authority.Role 473 | db.Where("slug = ?", "role-a").First(&r) 474 | db.Where("role_id = ?", r.ID).Delete(authority.RolePermission{}) 475 | db.Where("slug = ?", "permission-a").Delete(authority.Permission{}) 476 | db.Where("slug = ?", "permission-b").Delete(authority.Permission{}) 477 | db.Where("slug = ?", "permission-c").Delete(authority.Permission{}) 478 | db.Where("slug = ?", "role-a").Delete(authority.Role{}) 479 | }) 480 | 481 | } 482 | 483 | func TestRevokeUserRole(t *testing.T) { 484 | auth := authority.New(authority.Options{ 485 | TablesPrefix: "authority_", 486 | DB: db, 487 | }) 488 | 489 | // first create a role 490 | err := auth.CreateRole(authority.Role{ 491 | Name: "Role A", 492 | Slug: "role-a", 493 | }) 494 | if err != nil { 495 | t.Error("failed test revoke user role", err) 496 | } 497 | 498 | // assign the role 499 | err = auth.AssignRoleToUser(1, "role-a") 500 | if err != nil { 501 | t.Error("failed test revoke user role", err) 502 | } 503 | 504 | //test 505 | err = auth.RevokeUserRole(1, "role-a") 506 | if err != nil { 507 | t.Error("failed test revoke user role", err) 508 | } 509 | 510 | // revoke missing role 511 | err = auth.RevokeUserRole(1, "role-aa") 512 | if err == nil { 513 | t.Error("failed test revoke user role") 514 | } 515 | 516 | var c int64 517 | db.Model(authority.UserRole{}).Where("user_id = ?", 1).Count(&c) 518 | if c != 0 { 519 | t.Error("failed test revoke user role") 520 | } 521 | 522 | t.Cleanup(func() { 523 | var r authority.Role 524 | db.Where("slug = ?", "role-a").First(&r) 525 | db.Where("role_id = ?", r.ID).Delete(authority.UserRole{}) 526 | db.Where("slug = ?", "role-a").Delete(authority.Role{}) 527 | }) 528 | } 529 | 530 | func TestRevokeRolePermission(t *testing.T) { 531 | auth := authority.New(authority.Options{ 532 | TablesPrefix: "authority_", 533 | DB: db, 534 | }) 535 | 536 | // first create a role 537 | err := auth.CreateRole(authority.Role{ 538 | Name: "Role A", 539 | Slug: "role-a", 540 | }) 541 | if err != nil { 542 | t.Error("failed test revoke role permission", err) 543 | } 544 | // second test create permissions 545 | err = auth.CreatePermission(authority.Permission{ 546 | Name: "Permission A", 547 | Slug: "permission-a", 548 | }) 549 | if err != nil { 550 | t.Error("failed test revoke role permission", err) 551 | } 552 | err = auth.CreatePermission(authority.Permission{ 553 | Name: "Permission B", 554 | Slug: "permission-b", 555 | }) 556 | if err != nil { 557 | t.Error("failed test revoke role permission", err) 558 | } 559 | 560 | // third assign the permissions 561 | err = auth.AssignPermissionsToRole("role-a", []string{"permission-a", "permission-b"}) 562 | if err != nil { 563 | t.Error("failed test revoke role permission", err) 564 | } 565 | 566 | // test revoke missing role 567 | err = auth.RevokeRolePermission("role-aa", "permission-a") 568 | if err == nil { 569 | t.Error("failed test revoke role permission") 570 | } 571 | 572 | // test revoke missing permission 573 | err = auth.RevokeRolePermission("role-a", "permission-aa") 574 | if err == nil { 575 | t.Error("failed test revoke role permission") 576 | } 577 | 578 | err = auth.RevokeRolePermission("role-a", "permission-a") 579 | if err != nil { 580 | t.Error("failed test revoke role permission") 581 | } 582 | // assert, count assigned permission, should be one 583 | var r authority.Role 584 | res := db.Where("slug = ?", "role-a").First(&r) 585 | if res.Error != nil { 586 | t.Error("failed test revoke role permission", res.Error) 587 | } 588 | var c int64 589 | db.Model(authority.RolePermission{}).Where("role_id = ?", r.ID).Count(&c) 590 | if c != 1 { 591 | t.Error("failed test revoke role permission") 592 | } 593 | 594 | t.Cleanup(func() { 595 | // clean up 596 | db.Where("role_id = ?", r.ID).Delete(authority.RolePermission{}) 597 | db.Where("slug = ?", "permission-a").Delete(authority.Permission{}) 598 | db.Where("slug = ?", "permission-b").Delete(authority.Permission{}) 599 | db.Where("slug = ?", "role-a").Delete(authority.Role{}) 600 | }) 601 | 602 | } 603 | 604 | func TestGetAllRoles(t *testing.T) { 605 | auth := authority.New(authority.Options{ 606 | TablesPrefix: "authority_", 607 | DB: db, 608 | }) 609 | 610 | // first create roles 611 | err := auth.CreateRole(authority.Role{ 612 | Name: "Role A", 613 | Slug: "role-a", 614 | }) 615 | if err != nil { 616 | t.Error("failed test get roles", err) 617 | } 618 | err = auth.CreateRole(authority.Role{ 619 | Name: "Role B", 620 | Slug: "role-b", 621 | }) 622 | if err != nil { 623 | t.Error("failed test get roles", err) 624 | } 625 | 626 | // test 627 | roles, err := auth.GetAllRoles() 628 | if err != nil { 629 | t.Error("failed test get roles", err) 630 | } 631 | 632 | // check 633 | if len(roles) != 2 { 634 | t.Error("failed test get roles") 635 | } 636 | db.Where("slug = ?", "role-a").Delete(authority.Role{}) 637 | db.Where("slug = ?", "role-b").Delete(authority.Role{}) 638 | } 639 | 640 | func TestGetAllPermissions(t *testing.T) { 641 | auth := authority.New(authority.Options{ 642 | TablesPrefix: "authority_", 643 | DB: db, 644 | }) 645 | 646 | // first create permission 647 | err := auth.CreatePermission(authority.Permission{ 648 | Name: "Permission A", 649 | Slug: "permission-a", 650 | }) 651 | if err != nil { 652 | t.Error("failed test get permissions", err) 653 | } 654 | err = auth.CreatePermission(authority.Permission{ 655 | Name: "Permission B", 656 | Slug: "permission-b", 657 | }) 658 | if err != nil { 659 | t.Error("failed test get permissions", err) 660 | } 661 | 662 | // test 663 | perms, err := auth.GetAllPermissions() 664 | // check 665 | if len(perms) != 2 { 666 | t.Error("failed test get permissions") 667 | } 668 | db.Where("slug = ?", "permission-a").Delete(authority.Permission{}) 669 | db.Where("slug = ?", "permission-b").Delete(authority.Permission{}) 670 | } 671 | 672 | func TestDeleteRole(t *testing.T) { 673 | auth := authority.New(authority.Options{ 674 | TablesPrefix: "authority_", 675 | DB: db, 676 | }) 677 | 678 | err := auth.CreateRole(authority.Role{ 679 | Name: "Role A", 680 | Slug: "role-a", 681 | }) 682 | if err != nil { 683 | t.Error("failed test delete role", err) 684 | } 685 | 686 | // test delete a missing role 687 | err = auth.DeleteRole("role-aa") 688 | if err == nil { 689 | t.Error("failed test delete role") 690 | } 691 | 692 | // test delete an assigned role 693 | err = auth.AssignRoleToUser(1, "role-a") 694 | if err != nil { 695 | t.Error("failed test delete role", err) 696 | } 697 | err = auth.DeleteRole("role-a") 698 | if err == nil { 699 | t.Error("failed test delete role") 700 | } 701 | err = auth.RevokeUserRole(1, "role-a") 702 | if err != nil { 703 | t.Error("failed test delete role", err) 704 | } 705 | err = auth.DeleteRole("role-a") 706 | if err != nil { 707 | t.Error("failed test delete role", err) 708 | } 709 | 710 | var c int64 711 | db.Model(authority.Role{}).Where("slug = ?", "role-a").Count(&c) 712 | if c != 0 { 713 | t.Error("failed test delete role") 714 | } 715 | } 716 | 717 | func TestDeletePermission(t *testing.T) { 718 | auth := authority.New(authority.Options{ 719 | TablesPrefix: "authority_", 720 | DB: db, 721 | }) 722 | 723 | err := auth.CreatePermission(authority.Permission{ 724 | Name: "Permission A", 725 | Slug: "permission-a", 726 | }) 727 | if err != nil { 728 | t.Error("failed test delete permission", err) 729 | } 730 | 731 | // delete missing permission 732 | err = auth.DeletePermission("permission-aa") 733 | if err == nil { 734 | t.Error("failed test delete permission", err) 735 | } 736 | 737 | // delete an assigned permission 738 | auth.CreateRole(authority.Role{ 739 | Name: "Role A", 740 | Slug: "role-a", 741 | }) 742 | auth.AssignPermissionsToRole("role-a", []string{"permission-a"}) 743 | 744 | // delete assinged permission 745 | err = auth.DeletePermission("permission-a") 746 | if err == nil { 747 | t.Error("failed test delete permission") 748 | } 749 | 750 | err = auth.RevokeRolePermission("role-a", "permission-a") 751 | if err != nil { 752 | t.Error("failed test delete permission", err) 753 | } 754 | 755 | err = auth.DeletePermission("permission-a") 756 | if err != nil { 757 | t.Error("failed test delete permission", err) 758 | } 759 | 760 | var c int64 761 | db.Model(authority.Permission{}).Count(&c) 762 | if c != 0 { 763 | t.Error("failed test delete permission") 764 | } 765 | 766 | // clean up 767 | auth.DeleteRole("role-a") 768 | } 769 | 770 | func TestGetUserRoles(t *testing.T) { 771 | auth := authority.New(authority.Options{ 772 | TablesPrefix: "authority_", 773 | DB: db, 774 | }) 775 | 776 | // first create a role 777 | err := auth.CreateRole(authority.Role{ 778 | Name: "Role A", 779 | Slug: "role-a", 780 | }) 781 | if err != nil { 782 | t.Error("failed test get user roles", err) 783 | } 784 | 785 | err = auth.CreateRole(authority.Role{ 786 | Name: "Role B", 787 | Slug: "role-b", 788 | }) 789 | if err != nil { 790 | t.Error("failed test get user roles", err) 791 | } 792 | err = auth.AssignRoleToUser(1, "role-a") 793 | if err != nil { 794 | t.Error("failed test get user roles", err) 795 | } 796 | err = auth.AssignRoleToUser(1, "role-b") 797 | if err != nil { 798 | t.Error("failed test get user roles", err) 799 | } 800 | 801 | roles, err := auth.GetUserRoles(1) 802 | if err != nil { 803 | t.Error("failed test get user roles", err) 804 | } 805 | 806 | if len(roles) != 2 { 807 | t.Error("failed test get user roles") 808 | } 809 | for _, role := range roles { 810 | if !(role.Slug == "role-a" || role.Slug == "role-b") { 811 | t.Error("failed test get user roles") 812 | } 813 | } 814 | 815 | db.Where("user_id = ?", 1).Delete(authority.UserRole{}) 816 | db.Where("slug = ?", "role-a").Delete(authority.Role{}) 817 | db.Where("slug = ?", "role-b").Delete(authority.Role{}) 818 | } 819 | 820 | func TestGetRolePermissions(t *testing.T) { 821 | auth := authority.New(authority.Options{ 822 | TablesPrefix: "authority_", 823 | DB: db, 824 | }) 825 | 826 | err := auth.CreateRole(authority.Role{ 827 | Name: "Role A", 828 | Slug: "role-a", 829 | }) 830 | if err != nil { 831 | t.Error("failed test get role permissions", err) 832 | } 833 | err = auth.CreatePermission(authority.Permission{ 834 | Name: "Permission A", 835 | Slug: "permission-a", 836 | }) 837 | if err != nil { 838 | t.Error("failed test get role permissions", err) 839 | } 840 | err = auth.CreatePermission(authority.Permission{ 841 | Name: "Permission B", 842 | Slug: "permission-b", 843 | }) 844 | if err != nil { 845 | t.Error("failed test get role permissions", err) 846 | } 847 | err = auth.AssignPermissionsToRole("role-a", []string{"permission-a", "permission-b"}) 848 | rolePermissions, err := auth.GetRolePermissions("role-a") 849 | if err != nil { 850 | t.Error("failed test get role permissions", err) 851 | } 852 | if len(rolePermissions) != 2 { 853 | t.Error("failed test get role permissions", err) 854 | } 855 | var r authority.Role 856 | db.Where("slug = ?", "role-a").First(&r) 857 | db.Where("role_id = ?", r.ID).Delete(authority.RolePermission{}) 858 | db.Where("slug = ?", "role-a").Delete(authority.Role{}) 859 | db.Where("slug = ?", "permission-a").Delete(authority.Permission{}) 860 | db.Where("slug = ?", "permission-b").Delete(authority.Permission{}) 861 | } 862 | 863 | func TestTransaction(t *testing.T) { 864 | auth := authority.New(authority.Options{ 865 | TablesPrefix: "authority_", 866 | DB: db, 867 | }) 868 | tx := auth.BeginTX() 869 | err := tx.CreateRole(authority.Role{ 870 | Name: "Role A", 871 | Slug: "role-a", 872 | }) 873 | if err != nil { 874 | t.Error("failed test transactions", err) 875 | } 876 | 877 | err = tx.CreatePermission(authority.Permission{ 878 | Name: "Permission A", 879 | Slug: "permission-a", 880 | }) 881 | if err != nil { 882 | t.Error("failed test transactions", err) 883 | } 884 | err = tx.CreatePermission(authority.Permission{ 885 | Name: "Permission B", 886 | Slug: "permission-b", 887 | }) 888 | if err != nil { 889 | t.Error("failed test transactions", err) 890 | } 891 | tx.Rollback() 892 | 893 | var rCount int64 894 | db.Model(authority.Role{}).Count(&rCount) 895 | if rCount != 0 { 896 | t.Error("failed test transactions") 897 | } 898 | var permCount int64 899 | db.Model(authority.Permission{}).Count(&permCount) 900 | if permCount != 0 { 901 | t.Error("failed test transactions") 902 | } 903 | 904 | tx = auth.BeginTX() 905 | err = tx.CreateRole(authority.Role{ 906 | Name: "Role A", 907 | Slug: "role-a", 908 | }) 909 | if err != nil { 910 | t.Error("failed test transactions", err) 911 | } 912 | 913 | err = tx.CreatePermission(authority.Permission{ 914 | Name: "Permission A", 915 | Slug: "permission-a", 916 | }) 917 | if err != nil { 918 | t.Error("failed test transactions", err) 919 | } 920 | err = tx.CreatePermission(authority.Permission{ 921 | Name: "Permission B", 922 | Slug: "permission-b", 923 | }) 924 | if err != nil { 925 | t.Error("failed test transactions", err) 926 | } 927 | tx.Commit() 928 | 929 | db.Model(authority.Role{}).Count(&rCount) 930 | if rCount != 1 { 931 | t.Error("failed test transactions") 932 | } 933 | db.Model(authority.Permission{}).Count(&permCount) 934 | if permCount != 2 { 935 | t.Error("failed test transactions") 936 | } 937 | 938 | t.Cleanup(func() { 939 | db.Where("slug = ?", "role-a").Delete(&authority.Role{}) 940 | db.Where("slug = ?", "permission-a").Delete(&authority.Permission{}) 941 | db.Where("slug = ?", "permission-b").Delete(&authority.Permission{}) 942 | }) 943 | } 944 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/harranali/authority 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/joho/godotenv v1.3.0 // indirect 7 | gorm.io/driver/mysql v1.0.6 8 | gorm.io/gorm v1.21.9 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= 2 | github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 3 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 4 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 5 | github.com/jinzhu/now v1.1.2 h1:eVKgfIdy9b6zbWBMgFpfDPoAMifwSZagU9HmEU6zgiI= 6 | github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 7 | github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= 8 | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= 9 | gorm.io/driver/mysql v1.0.6 h1:mA0XRPjIKi4bkE9nv+NKs6qj6QWOchqUSdWOcpd3x1E= 10 | gorm.io/driver/mysql v1.0.6/go.mod h1:KdrTanmfLPPyAOeYGyG+UpDys7/7eeWT1zCq+oekYnU= 11 | gorm.io/gorm v1.21.9 h1:INieZtn4P2Pw6xPJ8MzT0G4WUOsHq3RhfuDF1M6GW0E= 12 | gorm.io/gorm v1.21.9/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0= 13 | -------------------------------------------------------------------------------- /permission.go: -------------------------------------------------------------------------------- 1 | package authority 2 | 3 | // Permission represents the database model of permissions 4 | type Permission struct { 5 | ID uint // The permission id (it gets set automatically by the database) 6 | Name string // The permission name 7 | Slug string // String based unique identifier of the permission, (use hyphen seperated permission name '-', instead of space) 8 | } 9 | 10 | // TableName sets the table name 11 | func (p Permission) TableName() string { 12 | return auth.TablesPrefix + "permissions" 13 | } 14 | -------------------------------------------------------------------------------- /role-permissions.go: -------------------------------------------------------------------------------- 1 | package authority 2 | 3 | // The link between the roles and permissions 4 | type RolePermission struct { 5 | ID uint // Unique id (it gets set automatically by the database) 6 | RoleID uint // Role id 7 | PermissionID uint // Permission id 8 | } 9 | 10 | // TableName sets the table name 11 | func (r RolePermission) TableName() string { 12 | return auth.TablesPrefix + "role_permissions" 13 | } 14 | -------------------------------------------------------------------------------- /roles.go: -------------------------------------------------------------------------------- 1 | package authority 2 | 3 | // The database model of a role 4 | type Role struct { 5 | ID uint // The role id (it gets set automatically by the database) 6 | Name string // The name of the role 7 | Slug string // String based unique identifier of the role, (use hyphen seperated role name '-', instead of space) 8 | } 9 | 10 | // TableName sets the table name 11 | func (r Role) TableName() string { 12 | return auth.TablesPrefix + "roles" 13 | } 14 | -------------------------------------------------------------------------------- /user-roles.go: -------------------------------------------------------------------------------- 1 | package authority 2 | 3 | // The link between the users and roles 4 | type UserRole struct { 5 | ID uint // Unique id (it gets set automatically by the database) 6 | UserID string // The user id 7 | RoleID uint // The role id 8 | } 9 | 10 | // TableName sets the table name 11 | func (u UserRole) TableName() string { 12 | return auth.TablesPrefix + "user_roles" 13 | } 14 | --------------------------------------------------------------------------------