Help us shape the future of Go! Take the 2026 Go Developer Survey

rbac

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Oct 13, 2026 License: MIT Imports: 8 Imported by: 0

README

go-rbac

Go Reference

A lightweight Role-Based Access Control (RBAC) engine for Go.

  • Zero dependencies — standard library only
  • Roles and permissions are plain strings; no schema imposed
  • Wildcard grants: "posts:*" covers "posts:read", "posts:write", etc.
  • Deep role inheritance with cycle detection
  • O(1) permission checks via eager resolution at startup
  • Hot-reload via Rebuild / RebuildFromLoader — lock-free reads, atomic swaps, serialized writers
  • net/http-compatible middleware included

Installation

go get github.com/martinsuchenak/go-rbac

Quick Start

import rbac "github.com/martinsuchenak/go-rbac"

manager := rbac.New(map[rbac.Role]rbac.RoleDefinition{
    "viewer": {Permissions: []rbac.Permission{"posts:read"}},
    "editor": {Permissions: []rbac.Permission{"posts:read", "posts:write"}},
    "admin": {
        Permissions: []rbac.Permission{"users:read", "users:write"},
        Inherits:    []rbac.Role{"editor"},
    },
})

manager.Can(user, "posts:write")               // true/false
manager.CanAll(user, "posts:read", "users:read") // must hold all
manager.CanAny(user, "posts:write", "users:read") // at least one
manager.Cannot(user, "settings:write")           // negation guard

Implement rbac.User on your existing user struct:

func (u *AppUser) GetRoles() []rbac.Role { return u.Roles }

API Reference

Permission checks
Method Description
Can(user, perm) User holds this permission (exact or wildcard)
Cannot(user, perm) Negation of Can
CanAll(user, perms...) User holds every listed permission
CanAny(user, perms...) User holds at least one listed permission
Role checks
Method Description
HasRole(user, role) Direct assignment only — not inherited
HasAnyRole(user, roles...) At least one role matches
HasAllRoles(user, roles...) All roles present
Introspection
Method Description
Roles() All defined role names, sorted
RolePermissions(role) All permissions for a role including inherited and wildcards
AllPermissions(user) All permissions across all of the user's roles
RoleExists(role) Whether a role is registered
PermissionExists(perm) Whether any role can satisfy this permission
ValidateUser(user) Returns any roles the user holds that are unknown to the manager

Wildcard Permissions

A permission ending in * is a prefix grant:

"posts:*"  // grants posts:read, posts:write, posts:delete, …
"*"        // grants everything

Matching is a plain string prefix, not segment-aware: "posts*" would also grant "postscript:read". Keep the separator in the grant ("posts:*").

Wildcards inherit transitively and appear in RolePermissions / AllPermissions output.

If role definitions come from data you don't fully control (e.g. an admin UI writing to a database), disable wildcards so every permission matches exactly:

manager := rbac.New(defs, rbac.WithoutWildcards())

Role Inheritance

Inheritance is transitive and resolves at startup. Cycles and diamond patterns are handled safely.

"admin": {
    Permissions: []rbac.Permission{"users:write"},
    Inherits:    []rbac.Role{"editor"},  // gets all editor permissions too
},

Dynamic Roles

Load definitions from a database or any source, then call rbac.New:

manager, err := rbac.NewFromLoader(ctx, rbac.RoleLoaderFunc(func(ctx context.Context) (map[rbac.Role]rbac.RoleDefinition, error) {
    return loadFromDB(ctx, db)
}))

For hot-reload when roles change at runtime:

if err := manager.RebuildFromLoader(ctx, loader); err != nil {
    // handle — manager is unchanged on error
}

Rebuild and RebuildFromLoader resolve the new definitions without blocking readers, then swap them in atomically: checks see either the old or the new definitions, never a mix. Concurrent rebuilds are serialized (for RebuildFromLoader, including the load), so the most recent one wins. Both return an error — and leave the current definitions in place — for a nil map or an over-deep inheritance chain. See docs/dynamic-roles.md for a full DB schema and loading example.

If you already have role names at hand, rbac.Roles satisfies rbac.User without a wrapper type:

manager.Can(rbac.Roles{"editor", "billing"}, "posts:write")

HTTP Middleware

import mw "github.com/martinsuchenak/go-rbac/middleware"

extractor := func(r *http.Request) (rbac.User, bool) { /* read from JWT/session */ }

mux.Handle("/posts",  mw.Require(mgr, extractor, "posts:read")(handler))
mux.Handle("/admin",  mw.RequireRole(mgr, extractor, "admin")(handler))
mux.Handle("/",       mw.InjectUser(extractor)(router))   // user available via mw.UserFromContext

Custom error responses, RequireAll, RequireAny, RequireAnyRole, RequireAllRoles, and inline Guard are also available — see godoc.

Development Options

WithStrictMode() causes Can to panic when the user holds an unregistered role. Useful in tests:

manager := rbac.New(defs, rbac.WithStrictMode())

Examples

Example Description
examples/basic Core API walkthrough
examples/inheritance Deep and diamond inheritance
examples/dynamic Wildcards, dynamic loading, hot-reload, strict mode, Guard
examples/webserver HTTP server with middleware
go run ./examples/basic
go run ./examples/dynamic
go run ./examples/webserver

Development

task test    # go test -race ./...
task vet     # go vet ./...
task lint    # golangci-lint
task tidy    # go mod tidy
task check   # vet + lint + test

License

MIT

Documentation

Overview

Package rbac provides a lightweight, runtime-configurable Role-Based Access Control (RBAC) engine for Go applications.

Core concepts

Roles and permissions are plain strings — the library imposes no vocabulary. Applications define them as typed constants for compile-time safety:

const (
    RoleAdmin  rbac.Role       = "admin"
    PermWrite  rbac.Permission = "posts:write"
)

The Manager is constructed once (or rebuilt on demand) from a map of RoleDefinition values, each declaring direct permissions and optional parent roles to inherit from. Permission checks are O(1) after construction.

Quick Start

manager := rbac.New(map[rbac.Role]rbac.RoleDefinition{
    "viewer": {Permissions: []rbac.Permission{"posts:read"}},
    "editor": {Permissions: []rbac.Permission{"posts:read", "posts:write"}},
    "admin": {
        Permissions: []rbac.Permission{"users:write"},
        Inherits:    []rbac.Role{"editor"},
    },
})

manager.Can(user, "posts:write")    // true/false
manager.HasRole(user, "admin")      // direct assignment only

Wildcard permissions

A permission ending in "*" is a prefix grant matched against the requested permission at check time:

"posts:*"  →  grants "posts:read", "posts:write", "posts:delete", …
"*"        →  grants every permission

Matching is a plain string prefix test and is not aware of any separator: "posts*" (no colon) also grants "postscript:read". Include the separator in the grant ("posts:*") to scope it to a single segment.

Wildcards are inherited transitively and appear verbatim in AllPermissions and RolePermissions output. When definitions come from untrusted or user-editable data, WithoutWildcards makes every permission match exactly.

Dynamic and database-driven roles

Implement RoleLoader (or use RoleLoaderFunc) to load definitions from any external source:

manager, err := rbac.NewFromLoader(ctx, rbac.RoleLoaderFunc(loadFromDB))

To update a live manager without downtime, call Rebuild or RebuildFromLoader. Both are safe to call concurrently with ongoing permission checks, which are lock-free and never observe a partially built state. Concurrent rebuilds are serialized, so the definitions applied last always win.

Development options

WithStrictMode causes Can (and all methods that delegate to it) to panic when the user holds an unregistered role. Intended for tests.

ValidateUser returns unknown roles without panicking, and is safe for production observability.

Index

Constants

This section is empty.

Variables

View Source
var ErrInheritanceTooDeep = errors.New("rbac: inheritance depth exceeded")

ErrInheritanceTooDeep is returned when a role's (non-cyclic) inheritance chain is longer than the supported maximum depth.

View Source
var ErrNilRoles = errors.New("rbac: roles map must not be nil")

ErrNilRoles is returned by Rebuild and the loader functions when the role definitions map is nil.

Functions

This section is empty.

Types

type Manager

type Manager struct {
	// contains filtered or unexported fields
}

Manager is the RBAC engine. Create one with New or NewFromLoader, then use its methods to perform access checks throughout your application.

Manager is safe for concurrent use. Read operations are lock-free: each one works against an immutable snapshot of the resolved definitions. Rebuild and RebuildFromLoader build a complete new snapshot without blocking readers and then publish it atomically.

func New

func New(roles map[Role]RoleDefinition, opts ...Option) *Manager

New constructs a Manager from the provided role definitions and optional configuration options. It resolves role inheritance eagerly so that runtime permission checks are O(roles assigned to the user) with a cheap map lookup per role.

New panics if roles is nil or an inheritance chain exceeds the maximum depth. Use NewFromLoader to get these as errors instead.

rbac.New(defs)
rbac.New(defs, rbac.WithStrictMode())

func NewFromLoader

func NewFromLoader(ctx context.Context, loader RoleLoader, opts ...Option) (*Manager, error)

NewFromLoader constructs a Manager by loading role definitions from the provided loader. It returns an error if the loader fails, returns a nil map, or the definitions cannot be resolved.

func (*Manager) AllPermissions

func (m *Manager) AllPermissions(user User) []Permission

AllPermissions returns the complete, deduplicated set of permissions the user holds across all their roles (including inherited permissions and wildcard grants such as "posts:*"). The result is sorted for deterministic output.

func (*Manager) Can

func (m *Manager) Can(user User, permission Permission) bool

Can returns true if the user holds the given permission via any of their assigned roles (including inherited permissions and wildcard grants).

if manager.Can(user, "posts:write") {
    // allow
}

In strict mode (WithStrictMode), Can panics if the user holds a role that is not registered with this manager.

func (*Manager) CanAll

func (m *Manager) CanAll(user User, permissions ...Permission) bool

CanAll returns true only if the user holds every one of the given permissions. Useful for operations that require multiple capabilities.

func (*Manager) CanAny

func (m *Manager) CanAny(user User, permissions ...Permission) bool

CanAny returns true if the user holds at least one of the given permissions.

func (*Manager) Cannot

func (m *Manager) Cannot(user User, permission Permission) bool

Cannot is the logical negation of Can. Useful for guard clauses.

if manager.Cannot(user, "settings:write") {
    return ErrForbidden
}

func (*Manager) HasAllRoles

func (m *Manager) HasAllRoles(user User, roles ...Role) bool

HasAllRoles returns true only if the user has every one of the listed roles.

func (*Manager) HasAnyRole

func (m *Manager) HasAnyRole(user User, roles ...Role) bool

HasAnyRole returns true if the user has at least one of the listed roles.

func (*Manager) HasRole

func (m *Manager) HasRole(user User, role Role) bool

HasRole returns true if the user has been directly assigned the given role and that role is registered with this manager. It does not consider inherited roles — only the exact roles returned by user.GetRoles(). If the role is not registered with this manager, HasRole returns false even if the user carries that role string.

if manager.HasRole(user, "admin") {
    showAdminPanel()
}

func (*Manager) PermissionExists

func (m *Manager) PermissionExists(perm Permission) bool

PermissionExists returns true if the given permission is satisfiable by at least one role registered with this manager — either as an exact permission (directly or via inheritance) or via a wildcard grant (e.g. "posts:*" satisfies "posts:read").

Passing a wildcard string itself (e.g. "posts:*") returns true only if a role has that exact wildcard grant, not because a broader wildcard like "*" covers it.

func (*Manager) Rebuild

func (m *Manager) Rebuild(roles map[Role]RoleDefinition) error

Rebuild replaces the manager's role definitions with the new set. The new definitions are fully resolved before being published, so concurrent permission checks see either the old or the new definitions, never a mix, and are not blocked while the rebuild runs.

Rebuild returns ErrNilRoles if roles is nil and ErrInheritanceTooDeep if an inheritance chain is too long. On error the previous definitions remain in effect.

func (*Manager) RebuildFromLoader

func (m *Manager) RebuildFromLoader(ctx context.Context, loader RoleLoader) error

RebuildFromLoader loads new role definitions from the given loader and applies them as Rebuild does. The load and the swap happen under the same lock as Rebuild, so when rebuilds race the definitions loaded last are the ones that stay in effect. On error the previous definitions remain in effect.

func (*Manager) RoleExists

func (m *Manager) RoleExists(role Role) bool

RoleExists returns true if the given role is explicitly defined in this manager (i.e. was present in the map passed to New or the most recent Rebuild call).

func (*Manager) RoleHasPermission added in v0.0.2

func (m *Manager) RoleHasPermission(role Role, permission Permission) bool

RoleHasPermission returns true if the given role has the specified permission, either directly or through inheritance (including wildcard grants). Returns false for unknown roles. This is an O(1) alternative to constructing a fake user and calling Can().

func (*Manager) RolePermissions

func (m *Manager) RolePermissions(role Role) []Permission

RolePermissions returns all permissions for the given role, including permissions inherited from parent roles and wildcard grants. Returns nil for unknown roles. The result is sorted for deterministic output.

func (*Manager) Roles

func (m *Manager) Roles() []Role

Roles returns all role names known to this manager, sorted.

func (*Manager) ValidateUser

func (m *Manager) ValidateUser(user User) []Role

ValidateUser returns the roles assigned to the user that are not explicitly defined in this manager. Returns nil if all roles are known. A role is considered "known" only if it was present in the map passed to New or the most recent Rebuild call — roles that exist solely as inherited references (i.e. appear in another role's Inherits but have no own definition) are treated as unknown.

Unlike strict mode, ValidateUser never panics and is safe to call in production for observability or pre-flight checks.

type Option

type Option func(*Manager)

Option configures a Manager at construction time.

func WithStrictMode

func WithStrictMode() Option

WithStrictMode returns an Option that causes Can (and all methods that delegate to it) to panic when the user holds a role not registered with the manager. Intended for use in development and testing to catch misconfigured role assignments early.

For production validation without panics, use ValidateUser instead.

func WithoutWildcards added in v0.1.0

func WithoutWildcards() Option

WithoutWildcards returns an Option that disables wildcard grants: a permission ending in "*" is then an ordinary permission that only matches a check for that exact string. Use it when role definitions come from data that should not be able to grant broad access by accident.

type Permission

type Permission string

Permission is the type for permission identifiers. Permissions are arbitrary strings; the library imposes no structure. Common conventions include:

  • Resource + action: "posts:write", "users:delete"
  • Flat strings: "can_publish", "manage_billing"
  • Hierarchical: "org:team:resource:action"
  • Wildcard grants: "posts:*", "*"

type Role

type Role string

Role is the type for role identifiers. Consuming applications should define their roles as typed constants of this type for compile-time safety:

const RoleAdmin rbac.Role = "admin"

type RoleDefinition

type RoleDefinition struct {
	// Permissions granted directly to this role. A permission ending in "*"
	// is treated as a wildcard prefix grant (see package doc).
	Permissions []Permission

	// Inherits lists roles whose resolved permissions are folded into this
	// role's effective permission set. Circular inheritance is handled
	// gracefully (cycles are detected and skipped).
	Inherits []Role
}

RoleDefinition declares the permissions granted to a role and, optionally, which other roles it inherits from. Inheritance is deep — inherited roles may themselves have their own Inherits, and the engine resolves the full transitive closure at startup.

type RoleLoader

type RoleLoader interface {
	Load(ctx context.Context) (map[Role]RoleDefinition, error)
}

RoleLoader is the interface for loading role definitions from an external source such as a database, configuration file, or remote service. Implement it to drive the RBAC manager from dynamic data:

type DBLoader struct{ db *sql.DB }

func (l *DBLoader) Load(ctx context.Context) (map[rbac.Role]rbac.RoleDefinition, error) {
    // query DB and return definitions
}

manager, err := rbac.NewFromLoader(ctx, &DBLoader{db: db})

type RoleLoaderFunc

type RoleLoaderFunc func(ctx context.Context) (map[Role]RoleDefinition, error)

RoleLoaderFunc is a function that implements RoleLoader. It allows a plain function to be used wherever a RoleLoader is expected:

loader := rbac.RoleLoaderFunc(func(ctx context.Context) (map[rbac.Role]rbac.RoleDefinition, error) {
    return loadFromDB(ctx, db)
})

func (RoleLoaderFunc) Load

Load implements RoleLoader.

type Roles added in v0.1.0

type Roles []Role

Roles is a list of roles that satisfies User. Use it when the caller already has the role names at hand and no richer user type is needed:

manager.Can(rbac.Roles{"editor", "billing"}, "posts:write")

func (Roles) GetRoles added in v0.1.0

func (r Roles) GetRoles() []Role

GetRoles implements User.

type User

type User interface {
	GetRoles() []Role
}

User is the interface that consuming applications must implement so the RBAC engine can inspect which roles are assigned to a given principal. Typically this is satisfied by an existing user/session struct:

type AppUser struct {
    ID    int
    Name  string
    Roles []rbac.Role
}

func (u *AppUser) GetRoles() []rbac.Role { return u.Roles }

Directories

Path Synopsis
examples
basic command
Package main demonstrates the core RBAC API without any HTTP layer.
Package main demonstrates the core RBAC API without any HTTP layer.
dynamic command
Package main demonstrates wildcard permissions, dynamic role loading and hot-reload, strict mode, and inline Guard usage.
Package main demonstrates wildcard permissions, dynamic role loading and hot-reload, strict mode, and inline Guard usage.
inheritance command
Package main demonstrates deep role inheritance hierarchies, including diamond inheritance (a role inheriting from two parents that share a common ancestor) and cycle detection.
Package main demonstrates deep role inheritance hierarchies, including diamond inheritance (a role inheriting from two parents that share a common ancestor) and cycle detection.
webserver command
Package main demonstrates using the RBAC library with a net/http web server.
Package main demonstrates using the RBAC library with a net/http web server.
Package middleware provides net/http-compatible RBAC middleware built on top of the github.com/martinsuchenak/go-rbac core engine.
Package middleware provides net/http-compatible RBAC middleware built on top of the github.com/martinsuchenak/go-rbac core engine.

Jump to

Keyboard shortcuts

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