gorbac

package module
v3.0.0-...-104a1f6 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 18, 2026 License: MIT Imports: 6 Imported by: 0

README

goRBAC

Build Status Go Reference Coverage Status

goRBAC provides a lightweight role-based access control implementation in Golang.

For the purposes of this package:

* an identity has one or more roles.
* a role requests access to a permission.
* a permission is given to a role.

Thus, RBAC has the following model:

* many to many relationship between identities and roles.
* many to many relationship between roles and permissions.
* roles can have parent roles (inheriting permissions).

Version

The current version of goRBAC uses Go generics (Go 1.18+) and is under active development.

About this fork

This fork includes a few behavior/API adjustments:

  • Role is now an interface and the default implementation is StdRole (constructed via NewRole).
  • RBAC is now an interface and the default implementation is StdRBAC (constructed via New).
  • RBAC/Role APIs use context.Context as the first parameter, aligned with modern Go practices.
  • Role.Assign, Role.Permit, and Role.Revoke now accept variadic permissions for batch usage.
  • RBAC.Get now returns only Role; use RBAC.GetParents when parent IDs are needed.
  • RBAC.SetParent has been removed; use RBAC.SetParents(ctx, id, parentID) instead.
  • RBAC.SetParents now accepts variadic parent IDs (parents ...T).
  • RBAC.RemoveParents now accepts variadic parent IDs (parents ...T).
  • AnyGranted and AllGranted now accept variadic permissions for batch checks.
  • The data-scope filter helpers focus on composing CEL filters across roles; permission checks are expected to happen elsewhere.

Install

Install the package:

$ go get github.com/fy0/gorbac/v3

Usage

Although you can adjust the RBAC instance anytime and it's absolutely safe, the library is designed for use with two phases:

  1. Preparing

  2. Checking

Preparing

Import the library:

import (
	"context"

	"github.com/fy0/gorbac/v3"
)

Get a new instance of RBAC (using string as the ID type):

ctx := context.Background()
rbac := gorbac.New[string]()

Get some new roles:

rA := gorbac.NewRole("role-a")
rB := gorbac.NewRole("role-b")
rC := gorbac.NewRole("role-c")
rD := gorbac.NewRole("role-d")
rE := gorbac.NewRole("role-e")

Get some new permissions:

pA := gorbac.NewPermission("permission-a")
pB := gorbac.NewPermission("permission-b")
pC := gorbac.NewPermission("permission-c")
pD := gorbac.NewPermission("permission-d")
pE := gorbac.NewPermission("permission-e")

Add the permissions to roles:

rA.Assign(ctx, pA)
rB.Assign(ctx, pB)
rC.Assign(ctx, pC)
rD.Assign(ctx, pD)
rE.Assign(ctx, pE)

Also, you can implement gorbac.Permission for your own data structure.

After initialization, add the roles to the RBAC instance:

rbac.Add(ctx, rA)
rbac.Add(ctx, rB)
rbac.Add(ctx, rC)
rbac.Add(ctx, rD)
rbac.Add(ctx, rE)

And set the inheritance:

rbac.SetParents(ctx, "role-a", "role-b")
rbac.SetParents(ctx, "role-b", "role-c", "role-d")
rbac.SetParents(ctx, "role-e", "role-d")

Checking

Checking the permission is easy:

if rbac.IsGranted(ctx, "role-a", pA) &&
	rbac.IsGranted(ctx, "role-a", pB) &&
	rbac.IsGranted(ctx, "role-a", pC) &&
	rbac.IsGranted(ctx, "role-a", pD) {
	fmt.Println("The role-a has been granted permis-a, b, c and d.")
}

Conditional Filters (Data Scope)

This repo also includes an optional CEL -> SQL filter engine for row-level data scoping:

  • github.com/fy0/gorbac/v3/filter: a CEL -> SQL filter engine (ported from memos).
  • Helpers in package gorbac to attach per-permission CEL filters (FilterPermission) and combine them across roles (FilterExprsForRoles, NewFilterProgramFromCEL).
  • Optional: AND an extra CEL filter via filter.WithExtraFilterCEL(...) (useful for user query/search filters).

At a high level, permissions still decide whether a role is granted, and the attached filter decides which rows are accessible for that permission. FilterExprsForRoles does not perform permission checks; it only selects which filter expressions to compose based on the required filter permission IDs. Missing filters are treated as allow-all.

The filter engine supports (subset):

  • Scalar columns (FieldKindScalar), including comparisons (==, !=, <, ...), in, and string helpers (contains, startsWith, endsWith)
  • JSON boolean fields (FieldKindJSONBool)
  • JSON string lists with membership + comprehensions (FieldKindJSONList, e.g. "foo" in tags, tags.exists(t, t.contains(q)), t.startsWith(q))
  • Extension hooks: register custom CEL macros/env options (filter.WithEnvOptions, filter.WithMacros) and rewrite the compiled condition tree (filter.WithCompileHook)
  • Custom predicates: register dialect-aware SQL snippets and reference them from CEL via sql("name", [...]) (filter.WithSQLPredicate)

Tip: if your schema matches a Go struct, you can build it via filter.SchemaFromStruct(...).

Utility Functions

goRBAC provides several built-in utility functions:

InherCircle

Detects circular inheritance in the role hierarchy:

rbac.SetParents(ctx, "role-c", "role-a")
if err := gorbac.InherCircle(ctx, rbac); err != nil {
	fmt.Println("A circle inheritance occurred.")
}
AnyGranted

Checks if the role set grants any of the required permissions:

roles := []string{"role-a", "role-b", "role-c"}
if gorbac.AnyGranted(ctx, rbac, roles, pA, pB) {
	fmt.Println("The role set grants at least one of permission-a or permission-b.")
}
AllGranted

Checks if the role set grants all required permissions:

roles := []string{"role-a", "role-b", "role-c"}
if gorbac.AllGranted(ctx, rbac, roles, pA, pB) {
	fmt.Println("The role set covers both permission-a and permission-b.")
}
Walk

Iterates through all roles in the RBAC instance:

handler := func(r gorbac.Role[string], parents []string) error {
	fmt.Printf("Role: %s, Parents: %v\n", r.ID(), parents)
	return nil
}
gorbac.Walk(ctx, rbac, handler)

Custom Types

goRBAC supports custom types for role and permission IDs through Go generics:

// Using integer IDs
rbacInt := gorbac.New[int]()
role1 := gorbac.NewRole(1)
permission1 := gorbac.NewPermission(100)

// Using custom struct IDs
type RoleID struct {
	Name string
	Type string
}

rbacStruct := gorbac.New[RoleID]()
roleCustom := gorbac.NewRole(RoleID{Name: "admin", Type: "system"})
permissionCustom := gorbac.NewPermission(RoleID{Name: "read", Type: "data"})

Persistence

The most asked question is how to persist the goRBAC instance. Please check the post HOW TO PERSIST GORBAC INSTANCE for the details.

Authors

Open Source - MIT Software License

See LICENSE.

Documentation

Overview

Package gorbac provides a lightweight role-based access control implementation in Golang.

For the purposes of this package:

  • an identity has one or more roles.
  • a role requests access to a permission.
  • a permission is given to a role.

Thus, RBAC has the following model:

  • many to many relationship between identities and roles.
  • many to many relationship between roles and permissions.
  • roles can have parent roles.

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrRoleNotExist occurred if a role cann't be found
	ErrRoleNotExist = errors.New("Role does not exist")
	// ErrRoleExist occurred if a role shouldn't be found
	ErrRoleExist = errors.New("Role has already existed")
)
View Source
var (
	ErrFoundCircle = fmt.Errorf("found circle")
)

Functions

func AllGranted

func AllGranted[T comparable](ctx context.Context, rbac RBAC[T], roles []T,
	permissions ...Permission[T]) (ok bool)

AllGranted checks whether the role set grants all specified permissions.

func AnyGranted

func AnyGranted[T comparable](ctx context.Context, rbac RBAC[T], roles []T,
	permissions ...Permission[T]) (ok bool)

AnyGranted checks whether the role set grants any specified permission.

func FilterExprsForRoles

func FilterExprsForRoles[T comparable](
	ctx context.Context,
	rbac RBAC[T],
	roles []T,
	requiredFilterPermissions []Permission[T],
) ([]string, error)

FilterExprsForRoles returns combined CEL expressions per role.

The required filter permissions are used only to select which filter expressions to compose; missing filters are treated as allow-all. Permission checks are expected to be handled separately.

func InherCircle

func InherCircle[T comparable](ctx context.Context, rbac RBAC[T]) (err error)

InherCircle returns an error when detecting any circle inheritance.

func NewFilterProgramFromCEL

func NewFilterProgramFromCEL(
	schema filter.Schema,
	exprs []string,
	engineOpts ...filter.EngineOption,
) (*filter.Program, error)

NewFilterProgramFromCEL compiles CEL expressions into a single filter.Program.

Expressions are OR-ed together. Optional `filter.EngineOption` values are forwarded to `filter.NewEngine`, including `filter.WithExtraFilterCEL(...)` which is AND-ed to the final condition.

func Walk

func Walk[T comparable](ctx context.Context, rbac RBAC[T], h WalkHandler[T]) (err error)

Walk passes each Role to WalkHandler

Types

type FilterPermission

type FilterPermission[T comparable] struct {
	StdPermission[T]

	// Filter is a raw CEL boolean expression.
	//
	// It can reference:
	//   - schema fields (rendered as SQL columns)
	//   - extra CEL variables provided at runtime via filter.Bindings (rendered as placeholders)
	Filter string `json:"filter,omitempty"`
}

FilterPermission is a Permission with an attached CEL filter expression.

The filter is intended for data-scope filtering (row-level filtering). The RBAC permission check remains unchanged: the permission ID still decides whether a role is granted; the filter decides which rows are accessible.

func NewFilterPermission

func NewFilterPermission[T comparable](id T, celExpr string) FilterPermission[T]

func (FilterPermission[T]) CEL

func (p FilterPermission[T]) CEL() (string, error)

CEL returns the attached CEL expression.

type LayerPermission

type LayerPermission struct {
	SID string `json:"id"`
	Sep string `json:"sep"`
}

LayerPermission uses string as a layered ID. Each layer splits by "/".

func NewLayerPermission

func NewLayerPermission(id, sep string) LayerPermission

NewLayerPermission returns an instance of layered permission with `id`

func (LayerPermission) ID

func (p LayerPermission) ID() string

ID returns id

func (LayerPermission) Match

func (p LayerPermission) Match(parent Permission[string]) bool

Match another permission

type Permission

type Permission[T comparable] interface {
	ID() T
	Match(Permission[T]) bool
}

Permission interface T is the type of permission ID

func NewPermission

func NewPermission[T comparable](id T) Permission[T]

type Permissions

type Permissions[T comparable] map[T]Permission[T]

Permissions list

type RBAC

type RBAC[T comparable] interface {
	Add(ctx context.Context, role Role[T]) error
	Remove(ctx context.Context, id T) error
	Get(ctx context.Context, id T) (Role[T], error)
	RoleIDs(ctx context.Context) []T
	SetParents(ctx context.Context, id T, parents ...T) error
	GetParents(ctx context.Context, id T) ([]T, error)
	RemoveParents(ctx context.Context, id T, parents ...T) error
	IsGranted(ctx context.Context, roleID T, permission Permission[T]) bool
}

RBAC defines the role-based access control contract.

Example (Int)
ctx := context.Background()
rbac := gorbac.New[int]()
rA := gorbac.NewRole(1)
rB := gorbac.NewRole(2)
rC := gorbac.NewRole(3)
rD := gorbac.NewRole(4)
rE := gorbac.NewRole(5)

pA := gorbac.NewPermission(1)
pB := gorbac.NewPermission(2)
pC := gorbac.NewPermission(3)
pD := gorbac.NewPermission(4)
pE := gorbac.NewPermission(5)

must := func(err error) {
	if err != nil {
		panic(err)
	}
}

must(rA.Assign(ctx, pA))
must(rB.Assign(ctx, pB))
must(rC.Assign(ctx, pC))
must(rD.Assign(ctx, pD))
must(rE.Assign(ctx, pE))

must(rbac.Add(ctx, rA))
must(rbac.Add(ctx, rB))
must(rbac.Add(ctx, rC))
must(rbac.Add(ctx, rD))
must(rbac.Add(ctx, rE))
must(rbac.SetParents(ctx, 1, 2))
must(rbac.SetParents(ctx, 2, 3, 4))
must(rbac.SetParents(ctx, 5, 4))

if rbac.IsGranted(ctx, 1, pA) &&
	rbac.IsGranted(ctx, 1, pB) &&
	rbac.IsGranted(ctx, 1, pC) &&
	rbac.IsGranted(ctx, 1, pD) {
	fmt.Println("The role-a has been granted permis-a, b, c and d.")
}
if rbac.IsGranted(ctx, 2, pB) &&
	rbac.IsGranted(ctx, 2, pC) &&
	rbac.IsGranted(ctx, 2, pD) {
	fmt.Println("The role-b has been granted permis-b, c and d.")
}
// When a circle inheratance occurred,
must(rbac.SetParents(ctx, 3, 1))
// it could be detected as following code:
if err := gorbac.InherCircle(ctx, rbac); err != nil {
	fmt.Println("A circle inheratance occurred.")
}
Output:
The role-a has been granted permis-a, b, c and d.
The role-b has been granted permis-b, c and d.
A circle inheratance occurred.
Example (String)

Suppose:

The role-a is inheriting from role-b. The role-b is inheriting from role-c, role-d. The role-c is individual. The role-d is individual. The role-e is inheriting from role-d. Every roles have their own permissions.

ctx := context.Background()
rbac := gorbac.New[string]()
rA := gorbac.NewRole("role-a")
rB := gorbac.NewRole("role-b")
rC := gorbac.NewRole("role-c")
rD := gorbac.NewRole("role-d")
rE := gorbac.NewRole("role-e")

pA := gorbac.NewPermission("permission-a")
pB := gorbac.NewPermission("permission-b")
pC := gorbac.NewPermission("permission-c")
pD := gorbac.NewPermission("permission-d")
pE := gorbac.NewPermission("permission-e")

must := func(err error) {
	if err != nil {
		panic(err)
	}
}

must(rA.Assign(ctx, pA))
must(rB.Assign(ctx, pB))
must(rC.Assign(ctx, pC))
must(rD.Assign(ctx, pD))
must(rE.Assign(ctx, pE))

must(rbac.Add(ctx, rA))
must(rbac.Add(ctx, rB))
must(rbac.Add(ctx, rC))
must(rbac.Add(ctx, rD))
must(rbac.Add(ctx, rE))
must(rbac.SetParents(ctx, "role-a", "role-b"))
must(rbac.SetParents(ctx, "role-b", "role-c", "role-d"))
must(rbac.SetParents(ctx, "role-e", "role-d"))

if rbac.IsGranted(ctx, "role-a", pA) &&
	rbac.IsGranted(ctx, "role-a", pB) &&
	rbac.IsGranted(ctx, "role-a", pC) &&
	rbac.IsGranted(ctx, "role-a", pD) {
	fmt.Println("The role-a has been granted permis-a, b, c and d.")
}
if rbac.IsGranted(ctx, "role-b", pB) &&
	rbac.IsGranted(ctx, "role-b", pC) &&
	rbac.IsGranted(ctx, "role-b", pD) {
	fmt.Println("The role-b has been granted permis-b, c and d.")
}
// When a circle inheratance occurred,
must(rbac.SetParents(ctx, "role-c", "role-a"))
// it could be detected as following code:
if err := gorbac.InherCircle(ctx, rbac); err != nil {
	fmt.Println("A circle inheratance occurred.")
}
Output:
The role-a has been granted permis-a, b, c and d.
The role-b has been granted permis-b, c and d.
A circle inheratance occurred.

type Role

type Role[T comparable] interface {
	ID() T
	Assign(context.Context, ...Permission[T]) error
	Permit(context.Context, ...Permission[T]) bool
	Revoke(context.Context, ...Permission[T]) error
	Permissions(context.Context) []Permission[T]
	PermissionsMap(context.Context) map[T]Permission[T]
	Get(context.Context, T) (Permission[T], bool)
	FilterPermissions(context.Context) map[T]Permission[T]
}

Role describes the role contract.

type Roles

type Roles[T comparable] map[T]Role[T]

Roles is a map

type StdPermission

type StdPermission[T comparable] struct {
	SID T `json:"id"`
}

BasicPermission has `SID` is shorten for `serialisable ID`

func (StdPermission[T]) ID

func (p StdPermission[T]) ID() T

ID returns id

func (StdPermission[T]) Match

func (p StdPermission[T]) Match(a Permission[T]) bool

Match another permission

type StdRBAC

type StdRBAC[T comparable] struct {
	// contains filtered or unexported fields
}

StdRBAC object, in most cases it should be used as a singleton.

func New

func New[T comparable]() *StdRBAC[T]

New returns a StdRBAC structure. The default role structure will be used.

func (*StdRBAC[T]) Add

func (rbac *StdRBAC[T]) Add(ctx context.Context, r Role[T]) (err error)

Add a role `r`.

func (*StdRBAC[T]) Get

func (rbac *StdRBAC[T]) Get(_ context.Context, id T) (r Role[T], err error)

Get returns the role by `id`.

func (*StdRBAC[T]) GetParents

func (rbac *StdRBAC[T]) GetParents(_ context.Context, id T) ([]T, error)

GetParents return `parents` of the role `id`. If the role is not existing, an error will be returned. Or the role doesn't have any parents, a nil slice will be returned.

func (*StdRBAC[T]) IsGranted

func (rbac *StdRBAC[T]) IsGranted(ctx context.Context, id T, p Permission[T]) (ok bool)

IsGranted tests if the role `id` has permission `p`.

func (*StdRBAC[T]) Remove

func (rbac *StdRBAC[T]) Remove(_ context.Context, id T) (err error)

Remove the role by `id`.

func (*StdRBAC[T]) RemoveParents

func (rbac *StdRBAC[T]) RemoveParents(_ context.Context, id T, parents ...T) error

RemoveParents unbind `parents` from the role `id`. If the role or any parent is not existing, an error will be returned.

func (*StdRBAC[T]) RoleIDs

func (rbac *StdRBAC[T]) RoleIDs(_ context.Context) []T

RoleIDs returns all role IDs.

func (*StdRBAC[T]) SetParents

func (rbac *StdRBAC[T]) SetParents(_ context.Context, id T, parents ...T) error

SetParents bind `parents` to the role `id`. If the role or any of parents is not existing, an error will be returned.

type StdRole

type StdRole[T comparable] struct {

	// ID is the serialisable identity of role
	IDValue T `json:"id"`
	// contains filtered or unexported fields
}

StdRole is the default role implementation. You can embed this struct into your own role implementation. T is the type of ID.

func NewRole

func NewRole[T comparable](id T) *StdRole[T]

NewRole is the default role factory function.

func (*StdRole[T]) Assign

func (role *StdRole[T]) Assign(_ context.Context, perms ...Permission[T]) error

Assign permissions to the role.

func (*StdRole[T]) FilterPermissions

func (role *StdRole[T]) FilterPermissions(_ context.Context) map[T]Permission[T]

FilterPermissions returns a raw ref of CEL-carrying permissions keyed by ID.

func (*StdRole[T]) Get

func (role *StdRole[T]) Get(_ context.Context, id T) (Permission[T], bool)

Get returns a permission by ID.

func (*StdRole[T]) ID

func (role *StdRole[T]) ID() T

ID returns the role ID.

func (*StdRole[T]) Permissions

func (role *StdRole[T]) Permissions(_ context.Context) []Permission[T]

Permissions returns all permissions into a slice.

func (*StdRole[T]) PermissionsMap

func (role *StdRole[T]) PermissionsMap(_ context.Context) map[T]Permission[T]

PermissionsMap returns a raw ref of permissions keyed by ID.

func (*StdRole[T]) Permit

func (role *StdRole[T]) Permit(_ context.Context, perms ...Permission[T]) bool

Permit returns true if the role has all specified permissions.

func (*StdRole[T]) Revoke

func (role *StdRole[T]) Revoke(_ context.Context, perms ...Permission[T]) error

Revoke the specific permissions.

type WalkHandler

type WalkHandler[T comparable] func(Role[T], []T) error

WalkHandler is a function defined by user to handle role

Directories

Path Synopsis
examples
rbac-wrapper command
Package filter compiles CEL filters into a dialect-agnostic condition tree.
Package filter compiles CEL filters into a dialect-agnostic condition tree.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL