Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / Go

GRBAC

4.00/5 (1 vote)
4 Jul 2019Apache5 min read 3.6K   17  
grbac is a fast, elegant and concise RBAC (role-based access control) framework

grbac

中文文档

Grbac is a fast, elegant and concise RBAC framework. It supports enhanced wildcards and matches HTTP requests using Radix trees. Even more amazing is that you can easily use it in any existing database and data structure.

What grbac does is ensure that the specified resource can only be accessed by the specified role. Please note that grbac is not responsible for the storage of rule configurations and "what roles the current request initiator has". It means you should configure the rule information first and provide the roles that the initiator of each request has.

grbac treats the combination of Host, Path, and Method as a Resource, and binds the Resource to a set of role rules (called Permission). Only users who meet these rules can access the corresponding Resource.

The component that reads the rule information is called Loader. grbac presets some loaders, you can also customize a loader by implementing func()(grbac.Rules, error) and load it via grbac.WithLoader.

  1. Most Common Use Case
  2. Concept
    1. Rule
    2. Resource
    3. Permission
    4. Loader
  3. Other Examples
    1. gin && grbac.WithJSON
    2. echo && grbac.WithYaml
    3. iris && grbac.WithRules
    4. ace && grbac.WithAdvancedRules
    5. gin && grbac.WithLoader
  4. Enhanced wildcards
  5. BenchMark

1. Most Common Use Case

Below is the most common use case, which uses gin and wraps grbac as a middleware. With this example, you can easily know how to use grbac in other http frameworks (like echo, iris, ace, etc.):

JavaScript
package main

import (
    "github.com/gin-gonic/gin"
    "github.com/storyicon/grbac"
    "net/http"
    "time"
)

func LoadAuthorizationRules() (rules grbac.Rules, err error) {
    // Implement your logic here
    // ...
    // You can load authorization rules from database or file
    // But you need to return your authentication rules in the form of grbac.Rules
    // Tips: You can also bind this function to a golang struct
    return
}

func QueryRolesByHeaders(header http.Header) (roles []string,err error){
    // Implement your logic here
    // ...
    // This logic may be takes a token from the headers and
    // queries the user's corresponding roles from the database based on the token.
    return roles, err
}

func Authorization() gin.HandlerFunc {
    // Here, we use a custom Loader function via "grbac.WithLoader"
    // and specify that this function should be called every minute 
    // to update the authentication rules.
    // Grbac also offers some ready-made Loaders:
    // grbac.WithYAML
    // grbac.WithRules
    // grbac.WithJSON
    // ...
    rbac, err := grbac.New(grbac.WithLoader(LoadAuthorizationRules, time.Minute))
    if err != nil {
        panic(err)
    }
    return func(c *gin.Context) {
        roles, err := QueryRolesByHeaders(c.Request.Header)
        if err != nil {
            c.AbortWithError(http.StatusInternalServerError, err)
            return
        }
        state, _ := rbac.IsRequestGranted(c.Request, roles)
        if !state.IsGranted() {
            c.AbortWithStatus(http.StatusUnauthorized)
            return
        }
    }
}

func main(){
    c := gin.New()
    c.Use(Authorization())

    // Bind your API here
    // ...

    c.Run(":8080")
}

2. Concept

Here are some concepts about grbac. It's very simple, you may only need three minutes to understand.

2.1. Rule

JavaScript
// Rule is used to define the relationship between "resource" and "permission"
type Rule struct {
    // The ID controls the priority of the rule.
    // The higher the ID means the higher the priority of the rule.
    // When a request is matched to more than one rule,
    // then authentication will only use the permission configuration 
    // for the rule with the highest ID value.
    // If there are multiple rules that are the largest ID, 
    // then one of them will be used randomly.
    ID int `json:"id"`
    *Resource
    *Permission
}

As you can see, the Rule consists of three parts: ID, Resource, and Permission.
The ID determines the priority of the Rule.
When a request meets multiple rules at the same time (such as in a wildcard), grbac will select the one with the highest ID, then authenticate with its Permission definition.
If multiple rules of the same ID are matched at the same time, grbac will randomly select one from them.

Here is a very simple example:

#Rule
- id: 0
  # Resource
  host: "*"
  path: "**"
  method: "*"
  # Permission
  authorized_roles:
  - "*"
  forbidden_roles: []
  allow_anyone: false

#Rule 
- id: 1
  # Resource
  host: domain.com
  path: "/article"
  method: "{DELETE,POST,PUT}"
  # Permission
  authorized_roles:
  - editor
  forbidden_roles: []
  allow_anyone: false

In this configuration file written in yaml format, the rule with ID=0 states that all resources can be accessed by anyone with any role. But the rule with ID=1 states that only the editor can operate on the article.
Then, except that the operation of the article can only be accessed by the editor, all other resources can be accessed by anyone with any role.

2.2. Resource

JavaScript
// Resource defines resources
type Resource struct {
    // Host defines the host of the resource, allowing wildcards to be used.
    Host string `json:"host"`
    // Path defines the path of the resource, allowing wildcards to be used.
    Path string `json:"path"`
    // Method defines the method of the resource, allowing wildcards to be used.
    Method string `json:"method"`
}

Resource is used to describe which resources a rule applies to. When IsRequestGranted(c.Request, roles) is executed, grbac first matches the current Request with the Resources in all Rules.

Each field of Resource supports enhanced wildcards.

2.3. Permission

JavaScript
// Permission is used to define permission control information
type Permission struct {
    // AuthorizedRoles defines roles that allow access to specified resource
    // Accepted type: non-empty string, *
    //      *: means any role, but visitors should have at least one role,
    //      non-empty string: specified role
    AuthorizedRoles []string `json:"authorized_roles"`
    // ForbiddenRoles defines roles that not allow access to specified resource
    // ForbiddenRoles has a higher priority than AuthorizedRoles
    // Accepted type: non-empty string, *
    //      *: means any role, but visitors should have at least one role,
    //      non-empty string: specified role
    //
    ForbiddenRoles []string `json:"forbidden_roles"`
    // AllowAnyone has a higher priority than ForbiddenRoles/AuthorizedRoles
    // If set to true, anyone will be able to pass authentication.
    // Note that this will include people without any role.
    AllowAnyone bool `json:"allow_anyone"`
}

Permission is used to define the authorization rules of the Resource to which it is bound. That's understandable. When the roles of the requester meets the definition of Permission, he will be allowed access, otherwise he will be denied access.

For faster speeds, fields in Permission do not support enhanced wildcards. Only * is allowed in AuthorizedRoles and ForbiddenRoles to indicate all.

2.4. Loader

Loader is used to load authorization rules. grbac presets some loaders, you can also customize a loader by implementing func()(grbac.Rules, error) and load it via grbac.WithLoader.

Method Description
WithJSON(path, interval) Periodically load rules configuration from json file
WithYaml(path, interval) Periodically load rules configuration from yaml file
WithRules(Rules) Load rules configuration from grbac.Rules
WithAdvancedRules(loader.AdvancedRules) Load advanced rules from loader.AdvancedRules
WithLoader(loader func()(Rules, error), interval) Periodically load rules with custom functions

interval defines the reload period of the authentication rule.
When interval < 0, grbac will abandon periodically loading the configuration file.
When interval∈[0,1s), grbac will automatically set the interval to 5s.

3. Other Examples

Here are some simple examples to make it easier to understand how grbac works.
Although grbac works well in most http frameworks, I am sorry that I only use gin now, so if there are some flaws in the example below, please let me know.

3.1. gin && grbac.WithJSON

If you want to write the configuration file in a JSON file, you can load it via grbac.WithJSON(file, interval), file is your json file path, and grbac will reload the file every interval.

JavaScript
[
    {
        "id": 0,
        "host": "*",
        "path": "**",
        "method": "*",
        "authorized_roles": [
            "*"
        ],
        "forbidden_roles": [
            "black_user"
        ],
        "allow_anyone": false
    },
    {
        "id":1,
        "host": "domain.com",
        "path": "/article",
        "method": "{DELETE,POST,PUT}",
        "authorized_roles": ["editor"],
        "forbidden_roles": [],
        "allow_anyone": false
    }
]

The above is an example of authentication rule in JSON format. It's structure is based on grbac.Rules.

JavaScript
func QueryRolesByHeaders(header http.Header) (roles []string,err error){
    // Implement your logic here
    // ...
    // This logic may be takes a token from the headers and
    // query the user's corresponding roles from the database based on the token.
    return roles, err
}

func Authentication() gin.HandlerFunc {
    rbac, err := grbac.New(grbac.WithJSON("config.json", time.Minute * 10))
    if err != nil {
        panic(err)
    }
    return func(c *gin.Context) {
        roles, err := QueryRolesByHeaders(c.Request.Header)
        if err != nil {
            c.AbortWithError(http.StatusInternalServerError, err)
            return
        }

        state, err := rbac.IsRequestGranted(c.Request, roles)
        if err != nil {
            c.AbortWithStatus(http.StatusInternalServerError)
            return
        }

        if !state.IsGranted() {
            c.AbortWithStatus(http.StatusInternalServerError)
            return
        }
    }
}

func main(){
    c := gin.New()
    c.Use(Authentication())

    // Bind your API here
    // ...
    
    c.Run(":8080")
}

3.2. echo && grbac.WithYaml

If you want to write the configuration file in a YAML file, you can load it via grbac.WithYAML(file, interval), file is your yaml file path, and grbac will reload the file every interval.

#Rule
- id: 0
  # Resource
  host: "*"
  path: "**"
  method: "*"
  # Permission
  authorized_roles:
  - "*"
  forbidden_roles: []
  allow_anyone: false

#Rule 
- id: 1
  # Resource
  host: domain.com
  path: "/article"
  method: "{DELETE,POST,PUT}"
  # Permission
  authorized_roles:
  - editor
  forbidden_roles: []
  allow_anyone: false

The above is an example of authentication rule in YAML format. Its structure is based on grbac.Rules.

JavaScript
func QueryRolesByHeaders(header http.Header) (roles []string,err error){
    // Implement your logic here
    // ...
    // This logic may be takes a token from the headers and
    // queries the user's corresponding roles from the database based on the token.
    return roles, err
}

func Authentication() echo.MiddlewareFunc {
    rbac, err := grbac.New(grbac.WithYAML("config.yaml", time.Minute * 10))
    if err != nil {
            panic(err)
    }
    return func(echo.HandlerFunc) echo.HandlerFunc {
        return func(c echo.Context) error {
            roles, err := QueryRolesByHeaders(c.Request().Header)
            if err != nil {
                    c.NoContent(http.StatusInternalServerError)
                    return nil
            }
            state, err := rbac.IsRequestGranted(c.Request(), roles)
            if err != nil {
                    c.NoContent(http.StatusInternalServerError)
                    return nil
            }
            if state.IsGranted() {
                    return nil
            }
            c.NoContent(http.StatusUnauthorized)
            return nil
        }
    }
}

func main(){
    c := echo.New()
    c.Use(Authentication())

    // Implement your logic here
    // ...
}

3.3. iris && grbac.WithRules

If you want to write the authentication rules directly in the code, grbac.WithRules(rules) provides this way, you can use it like this:

JavaScript
func QueryRolesByHeaders(header http.Header) (roles []string,err error){
    // Implement your logic here
    // ...
    // This logic may be takes a token from the headers and
    // queries the user's corresponding roles from the database based on the token.
    return roles, err
}

func Authentication() iris.Handler {
    var rules = grbac.Rules{
        {
            ID: 0,
            Resource: &grbac.Resource{
                        Host: "*",
                Path: "**",
                Method: "*",
            },
            Permission: &grbac.Permission{
                AuthorizedRoles: []string{"*"},
                ForbiddenRoles: []string{"black_user"},
                AllowAnyone: false,
            },
        },
        {
            ID: 1,
            Resource: &grbac.Resource{
                    Host: "domain.com",
                Path: "/article",
                Method: "{DELETE,POST,PUT}",
            },
            Permission: &grbac.Permission{
                    AuthorizedRoles: []string{"editor"},
                ForbiddenRoles: []string{},
                AllowAnyone: false,
            },
        },
    }
    rbac, err := grbac.New(grbac.WithRules(rules))
    if err != nil {
        panic(err)
    }
    return func(c context.Context) {
        roles, err := QueryRolesByHeaders(c.Request().Header)
        if err != nil {
                c.StatusCode(http.StatusInternalServerError)
            c.StopExecution()
            return
        }
        state, err := rbac.IsRequestGranted(c.Request(), roles)
        if err != nil {
                c.StatusCode(http.StatusInternalServerError)
            c.StopExecution()
            return
        }
        if !state.IsGranted() {
                c.StatusCode(http.StatusUnauthorized)
            c.StopExecution()
            return
        }
    }
}

func main(){
    c := iris.New()
    c.Use(Authentication())

    // Implement your logic here
    // ...
}

3.4. ace && grbac.WithAdvancedRules

If you want to write the authentication rules directly in the code, grbac.WithAdvancedRules(rules) provides this way, you can use it like this:

JavaScript
func QueryRolesByHeaders(header http.Header) (roles []string,err error){
    // Implement your logic here
    // ...
    // This logic may be takes a token from the headers and
    // queries the user's corresponding roles from the database based on the token.
    return roles, err
}

func Authentication() ace.HandlerFunc {
    var advancedRules = loader.AdvancedRules{
        {
            Host: []string{"*"},
            Path: []string{"**"},
            Method: []string{"*"},
            Permission: &grbac.Permission{
                AuthorizedRoles: []string{},
                ForbiddenRoles: []string{"black_user"},
                AllowAnyone: false,
            },
        },
        {
            Host: []string{"domain.com"},
            Path: []string{"/article"},
            Method: []string{"PUT","DELETE","POST"},
            Permission: &grbac.Permission{
                AuthorizedRoles: []string{"editor"},
                ForbiddenRoles: []string{},
                AllowAnyone: false,
            },
        },
    }
    auth, err := grbac.New(grbac.WithAdvancedRules(advancedRules))
    if err != nil {
        panic(err)
    }
    return func(c *ace.C) {
        roles, err := QueryRolesByHeaders(c.Request.Header)
        if err != nil {
        c.AbortWithStatus(http.StatusInternalServerError)
            return
        }
        state, err := auth.IsRequestGranted(c.Request, roles)
        if err != nil {
            c.AbortWithStatus(http.StatusInternalServerError)
            return
        }
        if !state.IsGranted() {
            c.AbortWithStatus(http.StatusUnauthorized)
            return
        }
    }
}
func main(){
    c := ace.New()
    c.Use(Authentication())

    // Implement your logic here
    // ...
}

loader.AdvancedRules attempts to provide a simpler way to define authentication rules than grbac.Rules.

3.5. gin && grbac.WithLoader

JavaScript
func QueryRolesByHeaders(header http.Header) (roles []string,err error){
    // Implement your logic here
    // ...
    // This logic may be takes a token from the headers and
    // queries the user's corresponding roles from the database based on the token.
    return roles, err
}

type MySQLLoader struct {
    session *gorm.DB
}

func NewMySQLLoader(dsn string) (*MySQLLoader, error) {
    loader := &MySQLLoader{}
    db, err := gorm.Open("mysql", dsn)
    if err  != nil {
        return nil, err
    }
    loader.session = db
    return loader, nil
}

func (loader *MySQLLoader) LoadRules() (rules grbac.Rules, err error) {
    // Implement your logic here
    // ...
    // Extract data from the database, assemble it into grbac.Rules and return
    return
}

func Authentication() gin.HandlerFunc {
    loader, err := NewMySQLLoader("user:password@/dbname?charset=utf8&parseTime=True&loc=Local")
    if err != nil {
        panic(err)
    }
    rbac, err := grbac.New(grbac.WithLoader(loader.LoadRules, time.Second * 5))
    if err != nil {
        panic(err)
    }
    return func(c *gin.Context) {
        roles, err := QueryRolesByHeaders(c.Request.Header)
        if err != nil {
            c.AbortWithStatus(http.StatusInternalServerError)
            return
        }
            
        state, err := rbac.IsRequestGranted(c.Request, roles)
        if err != nil {
            c.AbortWithStatus(http.StatusInternalServerError)
            return
        }
        if !state.IsGranted() {
            c.AbortWithStatus(http.StatusUnauthorized)
            return
        }
    }
}

func main(){
    c := gin.New()
    c.Use(Authorization())

    // Bind your API here
    // ...

    c.Run(":8080")
}

4. Enhanced Wildcards

Wildcard supported syntax:

pattern:
  { term }
term:
  '*'         matches any sequence of non-path-separators
  '**'        matches any sequence of characters, including
              path separators.
  '?'         matches any single non-path-separator character
  '[' [ '^' ] { character-range } ']'
        character class (must be non-empty)
  '{' { term } [ ',' { term } ... ] '}'
  c           matches character c (c != '*', '?', '\\', '[')
  '\\' c      matches character c

character-range:
  c           matches character c (c != '\\', '-', ']')
  '\\' c      matches character c
  lo '-' hi   matches character c for lo <= c <= hi

5. BenchMark

➜ gos test -bench=. 
goos: linux
goarch: amd64
pkg: github.com/storyicon/grbac/pkg/tree
BenchmarkTree_Query         	      2000           541397 ns/op
BenchmarkTree_Foreach_Query 	      2000           1360719 ns/op
PASS
ok      github.com/storyicon/grbac/pkg/tree     13.182s

The test case contains 1000 random rules, and the BenchmarkTree_Query and BenchmarkTree_Foreach_Query functions test four requests separately, after calculation:

541397/(4*1e9)=0.0001s

When there are 1000 rules, the average verification time per request is 0.0001s.

License

This article, along with any associated source code and files, is licensed under The Apache License, Version 2.0