Project address: github.com/storyicon/g…

Grbac is a fast, elegant and concise RBAC framework. It supports enhanced wildcards and matches HTTP requests using the Radix tree. Surprisingly, you can use it with ease in any existing database and data structure.

Grbac ensures that the specified resource is accessible only by the specified role. Note that GRBAC is not responsible for storing authentication rules and determining “which roles the current request originator has,” much less for role creation, assignment, and so on. This means that you should first configure the rule information and provide the role that the originator of each request has.

Grbac treats a combination of Host, Path, and Method as a Resource and binds the Resource to a set of role rules called permissions. Only users that meet these rules can access the corresponding Resource.

The component that reads the authentication rules is called Loader. Grbac presets some loaders, you can also define loaders according to your design by implementing func()(grbac.rules, error), and load it via grbac.withloader.

  • 1. Most common use cases
  • Concept 2.
    • 2.1. Rule
    • 2.2. The Resource
    • 2.3. The Permission
    • 2.4. Loader
  • Other examples
    • 3.1. gin && grbac.WithJSON
    • 3.2. The echo && grbac WithYaml
    • 3.3. iris && grbac.WithRules
    • 3.4. The ace && grbac WithAdvancedRules
    • 3.5. gin && grbac.WithLoader
  • 4. Enhanced wildcards
  • 5. Operating efficiency

1. Most common use cases

The following is the most common use case, which uses GIN and wraps GRBAC as middleware. With this example, you can easily see how to use GRbac in other HTTP frameworks (echo, Iris, ACE, etc.) :

package main

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

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

func QueryRolesByHeaders(header http.Header) (roles []string,err error){
    // Implement your logic here
    // ...
    // This logic may retrieve the token from the Headers request and query the user's role from the database based on the token.
    return roles, err
}

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

    // This is where you bind your API with functions such as c. gust, c. gust, etc
    // ...

    c.Run(": 8080")}Copy the code

Concept 2.

Here are some ideas about GRBAC. It’s so simple, you probably only need three minutes to understand it.

2.1. Rule

// Rule is a Rule that defines the relationship between Resource and Permission
type Rule struct {
    // ID determines the priority of the Rule.
    // A larger ID indicates a higher priority for the Rule.
    // When the request is matched by multiple rules simultaneously, GRbac will only use the rule with the highest ID value.
    // If more than one rule has the maximum ID, one of the rules will be used randomly.
    ID int `json:"id"`
    *Resource
    *Permission
}
Copy the code

As you can see, a Rule consists of three parts: ID, Resource, and Permission. ID determines the priority of the rule. When the request satisfies multiple rules simultaneously (for example, in a wildcard), GRBAC selects the one with the highest ID and uses its permission definition for authentication. If there are multiple rules with the maximum ID, one of them will be used at random (so avoid this).

Here’s 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
Copy the code

In this configuration file, written in YAML format, the rule of ID=0 indicates that anyone with any role can access all resources. However, the ID=1 rule indicates that only the editor can add or delete articles. In this way, anyone with any role can access all other resources, except that the actions of articles are only accessible to the editor.

2.2. The Resource

type Resource struct {
    // Host Defines the Host of the resource, allowing enhanced wildcard characters.
    Host string `json:"host"`
    // Path defines the Path of the resource, allowing enhanced wildcard characters.
    Path string `json:"path"`
    // Method Defines the Method of the resource, allowing enhanced wildcard characters.
    Method string `json:"method"`
}
Copy the code

Resource Describes the resources applicable to the Rule. When IsRequestGranted(C. rank, roles) is executed, GRbac first matches the current Request with the Resources in all of the rules.

Each field in Resource supports enhanced wildcards

2.3. The Permission

// Permission Defines Permission control information
type Permission struct {
    // AuthorizedRoles defines roles that allow access to resources
    // Supported types: non-empty string, *
    // *: means any role, but the visitor should have at least one role,
    // Non-empty string: specifies the role
    AuthorizedRoles []string `json:"authorized_roles"`
    // ForbiddenRoles defines roles that are not allowed to access the specified resource
    // ForbiddenRoles takes precedence over AuthorizedRoles
    // Supported types: non-empty string, *
    // *: means any role, but the visitor should have at least one role,
    // Non-empty string: specifies the role
    //
    ForbiddenRoles []string `json:"forbidden_roles"`
    // AllowAnyone has a higher priority than ForbiddenRoles and AuthorizedRoles
    // If set to true, anyone can pass the validation.
    // Note that this will include "people with no roles".
    AllowAnyone bool `json:"allow_anyone"`
}
Copy the code

Permission is used to define authorization rules for the Resource bound to. It is easy to understand that when the requestor’s role meets the definition of “Permission”, he will be allowed access to the Resource, otherwise he will be denied access.

To speed up validation, fields in a Permission do not support “enhanced wildcards.” Only * is allowed to indicate all in AuthorizedRoles and ForbiddenRoles.

2.4. Loader

Loader Loads the Rule. Grbac presets some loaders. You can also define loaders by implementing func()(grbac.rules, error) and load them via grbac.withloader.

method description
WithJSON(path, interval) On a regular basis fromjsonFile loading rule configuration
WithYaml(path, interval) On a regular basis fromyamlFile loading rule configuration
WithRules(Rules) fromgrbac.RulesLoading rule configuration
WithAdvancedRules(loader.AdvancedRules) Define rules in a more compact way and useloader.AdvancedRulesloading
WithLoader(loader func()(Rules, error), interval) Use custom functions to load rules periodically

Interval defines the overload period for Rules. When interval <0, GRbac will abandon the periodic loading of Rules configuration; When intervalโˆˆ[0,1s), GRBAC will automatically set interval to 5s;

Other examples

Here are some simple examples to make it easier to understand how GRBAC works. While GRBAC works fine in most HTTP frameworks, I’m sorry I only use GIN now, so let me know if there are any bugs in the example below.

3.1. gin && grbac.WithJSON

If you want to write a configuration file in a JSON file, you can load it with grbac.withjson (filepath, interval), filepath is your JSON filepath, and grbac will reload the file every interval. .

[{"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}]Copy the code

This is an example of an authentication rule in the “JSON” format. Its structure is based on grbac.rules.


func QueryRolesByHeaders(header http.Header) (roles []string,err error){
    // Implement your logic here
    // ...
    // This logic may retrieve the token from the Headers request and query the user's role from the database based on the token.
    return roles, err
}

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

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

        if! state.IsGranted() { c.AbortWithStatus(http.StatusInternalServerError)return}}}func main(a){
    c := gin.New()
    c.Use(Authentication())

    // This is where you bind your API with functions such as c. gust, c. gust, etc
    // ...
    
    c.Run(": 8080")}Copy the code

3.2. The echo && grbac WithYaml

If you want to write a configuration file in a YAML file, you can load it with 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
Copy the code

The above is an example of an authentication rule in the “YAML” format. Its structure is based on grbac.rules.

func QueryRolesByHeaders(header http.Header) (roles []string,err error){
    // Implement your logic here
    // ...
    // This logic may retrieve the token from the Headers request and query the user's role from the database based on the token.
    return roles, err
}

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

    // This is where you bind your API with functions such as c. gust, c. gust, etc
    // ...
    
}
Copy the code

3.3. iris && grbac.WithRules

If you want to write authentication rules directly in your code, grbac.withrules (rules) provides this. You can use it like this:


func QueryRolesByHeaders(header http.Header) (roles []string,err error){
    // Implement your logic here
    // ...
    // This logic may retrieve the token from the Headers request and query the user's role from the database based on the token.
    return roles, err
}

func Authentication(a) 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))
    iferr ! =nil {
        panic(err)
    }
    return func(c context.Context) {
        roles, err := QueryRolesByHeaders(c.Request().Header)
        iferr ! =nil {
                c.StatusCode(http.StatusInternalServerError)
            c.StopExecution()
            return
        }
        state, err := rbac.IsRequestGranted(c.Request(), roles)
        iferr ! =nil {
                c.StatusCode(http.StatusInternalServerError)
            c.StopExecution()
            return
        }
        if! state.IsGranted() { c.StatusCode(http.StatusUnauthorized) c.StopExecution()return}}}func main(a){
    c := iris.New()
    c.Use(Authentication())

    // This is where you bind your API with functions such as c. gust, c. gust, etc
    // ...
    
}
Copy the code

3.4. The ace && grbac WithAdvancedRules

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


func QueryRolesByHeaders(header http.Header) (roles []string,err error){
    // Implement your logic here
    // ...
    // This logic may retrieve the token from the Headers request and query the user's role from the database based on the token.
    return roles, err
}

func Authentication(a) 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))
    iferr ! =nil {
        panic(err)
    }
    return func(c *ace.C) {
        roles, err := QueryRolesByHeaders(c.Request.Header)
        iferr ! =nil {
        c.AbortWithStatus(http.StatusInternalServerError)
            return
        }
        state, err := auth.IsRequestGranted(c.Request, roles)
        iferr ! =nil {
            c.AbortWithStatus(http.StatusInternalServerError)
            return
        }
        if! state.IsGranted() { c.AbortWithStatus(http.StatusUnauthorized)return}}}func main(a){
    c := ace.New()
    c.Use(Authentication())

    // This is where you bind your API with functions such as c. gust, c. gust, etc
    // ...
    
}

Copy the code

Loader.advancedrules attempts to provide a more compact way of defining authentication Rules than grbac.rules.

3.5. gin && grbac.WithLoader


func QueryRolesByHeaders(header http.Header) (roles []string,err error){
    // Implement your logic here
    // ...
    // This logic may retrieve the token from the Headers request and query the user's role 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)
    iferr ! =nil {
        return nil, err
    }
    loader.session = db
    return loader, nil
}

func (loader *MySQLLoader) LoadRules(a) (rules grbac.Rules, err error) {
    // Implement your logic here
    // ...
    // You can load authorization rules from a database or file
    // But you need to return your authentication Rules in the grbac.rules format
    // Tip: You can also bind this function to the Golang structure
    return
}

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

    // This is where you bind your API with functions such as c. gust, c. gust, etc
    // ...

    c.Run(": 8080")}Copy the code

4. Enhanced wildcards

Syntax supported by Wildcard:

Pattern: {term} term: '*' matches any string other than the path separator. '**' matches any string, including the path separator. '? '[' [' ^'] {character-range} ']' character class (must be non-empty) '{' {term} [',' {term}...] '}' c matches the character c (c! = '*' and '? ', '\\', '[') '\\' c match character c character-range: C character c (c! = '\ \', '-' and '] '), '\ \' characters c lo c '-' characters c hi for lo < = c < = hiCopy the code

5. Operating efficiency

โžœ gos test - bench =. Goos: Linux goarch: amd64 PKG: github.com/storyicon/grbac/pkg/tree BenchmarkTree_Query2000           541397 ns/op
BenchmarkTree_Foreach_Query 	      2000           1360719 ns/op
PASS
ok      github.com/storyicon/grbac/pkg/tree     13.182 s
Copy the code

The test case contains 1000 random rules. The “BenchmarkTree_Query” and “BenchmarkTree_Foreach_Query” functions each test four requests:

541397 / (4 * 1 e9) = 0.0001 sCopy the code

When there are 1000 rules, the average validation time per request is “0.0001s”, which is fast (most of the time on wildcard matches).