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 fromjson File loading rule configuration |
WithYaml(path, interval) | On a regular basis fromyaml File loading rule configuration |
WithRules(Rules) | fromgrbac.Rules Loading rule configuration |
WithAdvancedRules(loader.AdvancedRules) | Define rules in a more compact way and useloader.AdvancedRules loading |
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).