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 18, 2026 License: MIT Imports: 16 Imported by: 0

README

rbac

Role-based access control for Go services that authenticate users with signed tokens such as JWTs. Define roles and permissions, optionally embed them in access tokens, and check them with HasRole, Can or net/http middleware.

rbac works with any authentication library and has no dependency on one. The separate authrbac module connects it to github.com/tpyle/auth/v2.

go get github.com/tpyle/rbac
go get github.com/tpyle/rbac/authrbac # if you use github.com/tpyle/auth/v2

Features

  • Policies: roles hold permission grants and can inherit other roles. Inheritance cycles and references to undefined roles are rejected up front. A role can carve out exceptions (files:* except files:delete).
  • Wildcards: permissions are colon-separated (posts:comments:delete); grants can use * for one segment and ** for any number (posts:*, posts:**, **:read, **).
  • Scoped permissions: bind a role on one resource or a group of resources (editor of projects/42, auditor of projects/*/docs) and check it with CanOn / HasRoleOn or scoped middleware. Bindings are looked up, never embedded, so they apply immediately and don't grow tokens. A service can turn global roles off entirely and rely on bindings alone.
  • Your choice of token contents: embed roles, permissions, both or neither. Checks use the token's claims when present and fall back to your database otherwise, so every combination works.
  • Hooks for your storage: implement RoleStore (user → roles), optionally ScopedRoleStore (user → roles on resources) and PolicyProvider (role → permissions) on your database. In-memory and cached implementations are included.
  • Policy files: JSON with the standard library, YAML through the rbacyaml subpackage.
  • HTTP middleware: RequireAnyRole, RequireAllPermissions, RequireAnyPermission and their scoped ...On variants, with 401/403 JSON responses.

Quick start

policy, err := rbacyaml.LoadPolicy(f) // or rbac.ParsePolicyJSON / rbac.NewPolicy

enforcer, err := rbac.New(
	authrbac.Option(),                // read the claims auth verifies
	rbac.WithRoleStore(myRoleStore),  // reads user → roles from your database
	rbac.WithPolicy(policy),
	rbac.WithEmbedRoles(true),        // put roles in the token
	rbac.WithEmbedPermissions(false), // resolve permissions from roles instead
)

a, err := auth.New(ctx,
	auth.WithUserStore(users),
	auth.WithClaimsProvider(enforcer.ClaimsProvider(nil)), // embeds the claims
)

mux := http.NewServeMux()
mux.Handle("DELETE /posts/{id}", enforcer.RequireAllPermissions("posts:delete")(deletePost))
mux.Handle("GET /admin", enforcer.RequireAnyRole("admin")(adminPage))
http.ListenAndServe(":8080", a.RequireAuthHandler(mux)) // auth runs first

// Or check inside a handler:
ok, err := enforcer.Can(r.Context(), "posts:publish")

// Scoped: add rbac.WithScopedRoleStore(myBindings) to rbac.New, then
doc := rbac.PathResource("projects/{project}/docs/{doc}")
mux.Handle("PUT /projects/{project}/docs/{doc}", enforcer.RequireAllPermissionsOn(doc, "docs:write")(putDoc))
ok, err = enforcer.CanOn(r.Context(), "projects/42/docs/7", "docs:write")
roles:
  viewer: { permissions: ["posts:read"] }
  editor: { inherits: [viewer], permissions: ["posts:*"] }
  admin:  { inherits: [editor], permissions: ["users:*"] }

How checks are answered

Configuration HasRole Can
nothing embedded RoleStore + policy RoleStore + policy
roles (default) token (+ policy, if configured) token roles + policy
permissions RoleStore + policy token
both token (+ policy, if configured) token

If a token lacks an enabled claim (for example it was issued before the claim was enabled), the check falls back to the RoleStore. Embedded claims are refreshed whenever the access token is refreshed, so they can be up to one access-token lifetime out of date. See Token Claims and Trade-offs.

Examples

Without an authentication library:

  • examples/basic: compute token claims and check them, no HTTP (go run ./examples/basic).
  • examples/middleware: rbac middleware behind your own authenticator (go run ./examples/middleware).

With github.com/tpyle/auth/v2 (run from the authrbac directory):

Documentation

API reference: pkg.go.dev/github.com/tpyle/rbac

License

MIT

Documentation

Overview

Package rbac adds role-based access control to services that authenticate users with signed tokens such as JWTs. It works with any authentication library: rbac does not issue or verify tokens, it only computes the claims to embed in them and reads those claims back. github.com/tpyle/rbac/authrbac connects it to github.com/tpyle/auth/v2.

A Policy maps roles to permission grants. Roles can inherit other roles, and grants can use wildcards: "*" matches one segment ("posts:*") and "**" any number ("posts:**"). A role can also except permissions from its grants (see RoleDefinition.Except). Policies are built in code with NewPolicy or loaded from JSON with ParsePolicyJSON (YAML is supported by the rbacyaml subpackage).

An Enforcer connects the policy to tokens:

Roles can also be granted on individual resources or groups of resources through a ScopedRoleStore: "editor of projects/42", or with a wildcard scope, "auditor of projects/*/docs". Enforcer.CanOn, Enforcer.HasRoleOn and the middleware Enforcer.RequireAllPermissionsOn and its siblings check them, combined with the user's global grants. A ScopeResolver decides which scopes apply to a resource; the default, PathScopes, makes a binding cover everything beneath its scope. Scoped bindings are never embedded in tokens. With Config.ScopedRolesOnly, a service ignores global roles and relies on bindings alone.

Embedding claims trades freshness for speed: a change to a user's roles or to the policy reaches embedded claims only when the access token is refreshed.

Index

Examples

Constants

View Source
const (
	// PermissionSeparator separates the segments of a permission, as in
	// "posts:comments:delete".
	PermissionSeparator = ":"
	// Wildcard, used as a whole segment of a grant or scope pattern, matches
	// exactly one segment.
	Wildcard = "*"
	// MultiWildcard, used as a whole segment of a grant or scope pattern,
	// matches zero or more segments.
	MultiWildcard = "**"
)

Permission syntax.

View Source
const DefaultLoadTimeout = 30 * time.Second

DefaultLoadTimeout is how long a CachedPolicyProvider waits for its loader unless WithLoadTimeout says otherwise.

View Source
const ScopeSeparator = "/"

ScopeSeparator separates the segments of a scope, as in "projects/42/docs/7". Scope patterns use the same Wildcard and MultiWildcard segments as permission grants.

Variables

View Source
var (
	// ErrNoClaims means no [Claims] were found: the [ClaimsSource] found
	// none in the context, usually because the request was not
	// authenticated, or nil claims were passed to a check.
	ErrNoClaims = errors.New("rbac: no claims in context")

	// ErrForbidden is passed to the [ErrorHandler] when an authenticated
	// request lacks the required roles or permissions.
	ErrForbidden = errors.New("rbac: forbidden")

	// ErrMalformedClaim means a token carries a roles or permissions claim
	// that is not an array of valid names. The token is not trusted to fall
	// back to the [RoleStore], because the claim being present means the
	// issuer intended it to be authoritative.
	ErrMalformedClaim = errors.New("rbac: malformed claim")

	// ErrInvalidPermission means a permission string is malformed (see
	// [ValidatePermission] and [ValidateGrant]).
	ErrInvalidPermission = errors.New("rbac: invalid permission")

	// ErrInvalidRole means a role name is empty or contains whitespace.
	ErrInvalidRole = errors.New("rbac: invalid role")

	// ErrInvalidScope means a resource or scope is malformed (see
	// [ValidateScope] and [ValidateScopePattern]). The default
	// [ErrorHandler] answers it with 400, since it usually means a request
	// named a resource that cannot exist.
	ErrInvalidScope = errors.New("rbac: invalid scope")

	// ErrUnknownRole means a role definition inherits from a role that is
	// not defined in the policy.
	ErrUnknownRole = errors.New("rbac: unknown role")

	// ErrRoleCycle means role inheritance contains a cycle.
	ErrRoleCycle = errors.New("rbac: role inheritance cycle")

	// ErrInvalidPolicy means a policy file could not be decoded.
	ErrInvalidPolicy = errors.New("rbac: invalid policy")

	// ErrNotConfigured means a check needs a [RoleStore], [ScopedRoleStore]
	// or [Policy] that was not supplied to [New], for example because a
	// token lacked the claims that would have made the lookup unnecessary.
	ErrNotConfigured = errors.New("rbac: not configured")

	// ErrNilPolicy means a [PolicyProvider] returned no policy and no error.
	// Checks fail rather than treating the missing policy as granting
	// nothing, which would deny every request without explanation.
	ErrNilPolicy = errors.New("rbac: policy provider returned a nil policy")
)

Errors returned by the package. They may be wrapped, so compare them with errors.Is.

Functions

func ContextWithClaims

func ContextWithClaims(ctx context.Context, c *Claims) context.Context

ContextWithClaims returns a copy of ctx carrying c, for ClaimsFromContext to find. Authentication middleware calls it after verifying a token, and tests can call it directly. Enforcers read it only when built with WithClaimsSource(ClaimsFromContext).

func Grants

func Grants(grant, perm string) bool

Grants reports whether grant permits the concrete permission perm. Both should be valid (see ValidateGrant and ValidatePermission); input with an empty segment, including the empty string, is never granted. Other invalid input is not detected.

A Wildcard segment matches exactly one segment and a MultiWildcard segment matches zero or more, in any position:

Grants("posts:*", "posts:read")              // true
Grants("posts:*", "posts:comments:delete")   // false
Grants("posts:**", "posts:comments:delete")  // true
Grants("posts:**", "posts")                  // true
Grants("posts:*:read", "posts:drafts:read")  // true
Grants("**:read", "posts:drafts:read")       // true
Grants("**", "anything:at:all")              // true

grant may also carry exceptions, in the form Policy.Permissions returns and permission claims hold: the grant followed by space-separated patterns, each prefixed with "-". It then permits what the grant matches and no exception does:

Grants("files:* -files:delete", "files:read")    // true
Grants("files:* -files:delete", "files:delete")  // false

A grant whose exceptions are malformed permits nothing.

Example
package main

import (
	"fmt"

	"github.com/tpyle/rbac"
)

func main() {
	fmt.Println(rbac.Grants("posts:*", "posts:delete"))
	fmt.Println(rbac.Grants("posts:*", "posts:comments:delete"))
	fmt.Println(rbac.Grants("posts:**", "posts:comments:delete"))
	fmt.Println(rbac.Grants("**:read", "posts:drafts:read"))
	fmt.Println(rbac.Grants("posts:*", "posts"))
}
Output:
true
false
true
true
false

func MatchScope

func MatchScope(pattern, scope string) bool

MatchScope reports whether the scope pattern matches the concrete scope. It follows the same rules as Grants, with ScopeSeparator between segments:

MatchScope("projects/*/docs", "projects/42/docs")                 // true
MatchScope("projects/*", "projects/42/docs")                      // false
MatchScope("projects/**/comments", "projects/comments")           // true
MatchScope("projects/**/comments", "projects/42/docs/7/comments") // true

A pattern matches a scope, not the scope's descendants. Bindings still cover descendants because PathScopes lists a resource's ancestors as scopes too: a binding on "projects/*/docs" applies to "projects/42/docs/7" because it matches the ancestor "projects/42/docs".

Example
package main

import (
	"fmt"

	"github.com/tpyle/rbac"
)

func main() {
	fmt.Println(rbac.MatchScope("projects/*/docs", "projects/42/docs"))
	fmt.Println(rbac.MatchScope("projects/*", "projects/42/docs"))
	fmt.Println(rbac.MatchScope("projects/**/comments", "projects/42/docs/7/comments"))
}
Output:
true
false
true

func ValidateGrant

func ValidateGrant(grant string) error

ValidateGrant reports whether grant is a valid permission grant: like a concrete permission (see ValidatePermission), except that any segment may be exactly Wildcard or MultiWildcard. Two MultiWildcard segments in a row are rejected as redundant. Grants are what roles hold and what tokens carry.

func ValidatePermission

func ValidatePermission(perm string) error

ValidatePermission reports whether perm is a valid concrete permission: one or more non-empty segments separated by PermissionSeparator, with no whitespace and no wildcards. Permissions passed to checks such as Enforcer.Can must be concrete.

func ValidateScope

func ValidateScope(s string) error

ValidateScope reports whether s is a valid concrete scope: one or more non-empty segments separated by ScopeSeparator, with no whitespace, no wildcards and no "." or ".." segments. Resources passed to scoped checks such as Enforcer.CanOn, and scopes returned by a ScopeResolver, must be concrete. Errors wrap ErrInvalidScope.

Dot segments are rejected because a resource such as "projects/42/../43" has "projects/42" as an ancestor for PathScopes while an application resolving the path would act on project 43.

func ValidateScopePattern

func ValidateScopePattern(s string) error

ValidateScopePattern reports whether s is a valid scope pattern, as used in a Binding: like a concrete scope (see ValidateScope), except that any segment may be exactly Wildcard or MultiWildcard. Two MultiWildcard segments in a row are rejected as redundant.

Types

type Binding

type Binding struct {
	Scope string `json:"scope" yaml:"scope"`
	Role  string `json:"role" yaml:"role"`
}

Binding grants Role to a subject on every scope matching Scope, a scope pattern (see ValidateScopePattern).

type CacheOption

type CacheOption func(*CachedPolicyProvider)

CacheOption configures a CachedPolicyProvider.

func WithLoadTimeout

func WithLoadTimeout(d time.Duration) CacheOption

WithLoadTimeout bounds each load: the loader's context is cancelled after d, and the load fails with context.DeadlineExceeded even if the loader ignores its context, so a hung database cannot block the cache. Zero or less disables the timeout. The default is DefaultLoadTimeout.

func WithStaleGrace

func WithStaleGrace(d time.Duration) CacheOption

WithStaleGrace lets the provider keep serving the last loaded policy for up to d after its TTL expires. During that time callers get the old policy at once while a reload runs in the background, and a failed reload leaves the old policy in use, so a brief database outage does not fail every check. Once the grace period is over, callers wait for a load as usual. CachedPolicyProvider.Invalidate discards the policy immediately, grace or not. The default is zero: an expired policy is never used.

type CachedPolicyProvider

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

CachedPolicyProvider wraps a slow policy loader, such as a database query, and reuses its result for a fixed time. It is safe for concurrent use.

When the cache is stale, one load runs and every caller that arrives while it is in progress shares its result, including a failure. At most one load runs at a time: callers arriving after CachedPolicyProvider.Invalidate while an earlier load is still running share a single follow-up load, which starts when the earlier one finishes.

The load is not cancelled when the caller that started it goes away. Instead it is bounded by a timeout (DefaultLoadTimeout, or see WithLoadTimeout); each caller still stops waiting when its own context is done. A panic in the loader is returned to the waiting callers as an error.

func NewCachedPolicyProvider

func NewCachedPolicyProvider(load func(ctx context.Context) (*Policy, error), ttl time.Duration, opts ...CacheOption) *CachedPolicyProvider

NewCachedPolicyProvider returns a provider that calls load at most once per ttl. A failed load, or one that returns a nil policy, is not cached: its error is returned to the callers sharing it and the next call retries, so a check never succeeds against a policy that could not be loaded (unless WithStaleGrace allows the previous one). A ttl of zero or less disables caching, and load is then called on every use, concurrently, with the caller's context.

func (*CachedPolicyProvider) Invalidate

func (c *CachedPolicyProvider) Invalidate()

Invalidate discards the cached policy, so the next call reloads it. Call it after changing the stored policy. Callers already waiting on a load in progress still receive its result, but it is not cached; later callers wait for a new load that starts once it finishes.

func (*CachedPolicyProvider) Policy

func (c *CachedPolicyProvider) Policy(ctx context.Context) (*Policy, error)

Policy implements PolicyProvider.

type Claims

type Claims struct {
	// Subject identifies the user (the JWT "sub" claim). It is the key
	// passed to the [RoleStore] and [ScopedRoleStore].
	Subject string
	// Values holds the token's other claims by name. The enforcer reads
	// only [Config.RolesClaim] and [Config.PermissionsClaim], each of which
	// must be a []string or, as decoded from JSON, a []any of strings.
	// A nil map is a token with no such claims.
	Values map[string]any
}

Claims is the part of a verified access token that an Enforcer reads. rbac does not verify tokens: whatever authenticates requests fills Claims from a token it has already verified, and hands it to the enforcer either in the request context (see ContextWithClaims and WithClaimsSource) or directly, as with Enforcer.ClaimsCanAll.

func ClaimsFromContext

func ClaimsFromContext(ctx context.Context) (*Claims, bool)

ClaimsFromContext returns the claims stored by ContextWithClaims, if any. It is a ClaimsSource for enforcers whose authentication middleware uses ContextWithClaims.

type ClaimsProviderFunc

type ClaimsProviderFunc = func(ctx context.Context, subject string) (map[string]any, error)

ClaimsProviderFunc computes extra claims to embed in subject's access token, as Enforcer.ClaimsProvider does. It is an alias rather than a defined type, so a value of it can be passed wherever a library expects its own function type of the same shape, such as auth.ClaimsProvider in github.com/tpyle/auth/v2, and the other way round.

type ClaimsSource

type ClaimsSource func(ctx context.Context) (*Claims, bool)

ClaimsSource returns the verified claims of the user authenticated in ctx, and false if there is none. Context-based checks such as Enforcer.Can and the middleware use it to find the caller; see WithClaimsSource.

type Config

type Config struct {
	// EmbedRoles adds the user's effective roles (assigned plus inherited)
	// to access tokens, so role checks need no database lookup.
	EmbedRoles bool `mapstructure:"embed_roles"`
	// EmbedPermissions adds the user's effective permission grants to
	// access tokens, so permission checks need neither a database lookup
	// nor the policy. Tokens grow with the number of grants.
	EmbedPermissions bool `mapstructure:"embed_permissions"`
	// RolesClaim is the JWT claim holding roles.
	RolesClaim string `mapstructure:"roles_claim"`
	// PermissionsClaim is the JWT claim holding permission grants.
	PermissionsClaim string `mapstructure:"permissions_claim"`
	// ScopedRolesOnly disables global roles, so that only roles bound in
	// the [ScopedRoleStore] grant anything. Scoped checks then ignore the
	// token's claims and the [RoleStore], and global checks such as
	// [Enforcer.Can] and [Enforcer.SubjectCan] fail with
	// [ErrNotConfigured]. [Enforcer.EffectiveRoles] and
	// [Enforcer.Permissions] still report the RoleStore's contents if one
	// is configured. It requires a
	// [ScopedRoleStore] and cannot be combined with EmbedRoles or
	// EmbedPermissions.
	ScopedRolesOnly bool `mapstructure:"scoped_roles_only"`
}

Config holds the plain-data settings of an Enforcer. Start from DefaultConfig, override fields, and pass it to New with WithConfig.

The embed settings are a contract between the service issuing tokens and every service checking them, so all of them should use the same values. An enforcer only reads the claims it is configured to embed: a token carrying a disabled claim is treated as not having it.

The mapstructure tags let Config be filled by configuration libraries such as Viper without this package depending on them.

func DefaultConfig

func DefaultConfig() Config

DefaultConfig returns the configuration used when no options are given: roles are embedded under "roles", permissions are not embedded (they are resolved from the roles through the policy), and the permissions claim is named "perms".

func (Config) Validate

func (c Config) Validate() error

Validate reports whether c can be used. Claim names must be non-empty, distinct and not one of the claims registered by RFC 7519 ("iss", "sub", "aud", "exp", "nbf", "iat" and "jti"), and ScopedRolesOnly excludes embedding. New also rejects names reserved with WithReservedClaims.

type Enforcer

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

Enforcer issues RBAC claims and answers role and permission checks. It is created with New and is safe for concurrent use.

Checks prefer the claims in the access token and fall back to the RoleStore when the token does not carry them. Permission checks resolve, in order:

  1. the permissions claim, if embedding permissions is enabled and the token has it;
  2. the roles claim through the Policy, if embedding roles is enabled and the token has it;
  3. the subject's roles from the RoleStore through the Policy.

Role checks use the roles claim if enabled and present, and otherwise the RoleStore. Either way, roles are expanded through the policy if one is configured, so inheritance added since the token was issued is seen by role checks just as it is by permission checks. If the token lists a requested role directly, the policy is not loaded, so such checks succeed even when the PolicyProvider is failing.

Inheritance removed since a token was issued still applies to it, for role and permission checks alike, until the token is refreshed: the token lists the roles that were inherited when it was issued, and each of them keeps its own grants.

Checks that find the user in a context, such as Enforcer.Can and Enforcer.CanOn, need a ClaimsSource (see WithClaimsSource); without one they fail with ErrNotConfigured once their arguments are validated.

A claim counts as present if its key exists, so an empty array means "no roles" rather than "look it up". A present but malformed claim is an error wrapping ErrMalformedClaim.

Scoped checks

Enforcer.CanOn, Enforcer.HasRoleOn and related methods ask about a specific resource. A user's grants on a resource are the union of:

Global grants are consulted first, and the resolver and store are only called for what they leave unanswered, so users with broad global roles are checked from the token alone. Scoped bindings are never embedded in tokens. Bound roles are expanded through the policy: a binding of "editor" also grants what "editor" inherits.

If no RoleStore is configured and the token carries none of the enabled claims, the user has no global grants in scoped checks (where global checks report ErrNotConfigured), so a service can rely on bindings alone. With Config.ScopedRolesOnly, global grants are never consulted: only bindings count, and global checks fail with ErrNotConfigured.

Scoped checks need a ScopedRoleStore, and scoped permission checks a policy; without them they fail with ErrNotConfigured even for users whose global grants would answer.

func New

func New(opts ...Option) (*Enforcer, error)

New returns an Enforcer. Without options it embeds roles (see DefaultConfig) and needs a policy. It fails with ErrNotConfigured if no check of some kind could ever be answered: a policy is required unless permissions are embedded, and a RoleStore is required unless roles or permissions are embedded.

Other gaps are allowed, because a service may never make the checks affected. Those checks fail with ErrNotConfigured when called, and middleware for them panics when built. For example, with only permissions embedded and no RoleStore, permission checks work but role checks do not, since the token carries no roles. The service that issues tokens always needs a RoleStore (and a policy, to embed permissions).

func (*Enforcer) Can

func (e *Enforcer) Can(ctx context.Context, perm string) (bool, error)

Can reports whether the user authenticated in ctx holds the concrete permission perm. It returns ErrInvalidPermission if perm is not concrete and ErrNoClaims if ctx has no claims.

Example
package main

import (
	"context"
	"fmt"

	"github.com/tpyle/rbac"
)

func main() {
	policy, _ := rbac.NewPolicy(map[string]rbac.RoleDefinition{
		"editor": {Permissions: []string{"posts:read", "posts:write"}},
	})
	e, _ := rbac.New(rbac.WithPolicy(policy), rbac.WithClaimsSource(rbac.ClaimsFromContext))

	// Normally authentication middleware puts the verified claims in the
	// context.
	ctx := rbac.ContextWithClaims(context.Background(), &rbac.Claims{
		Subject: "alice",
		Values:  map[string]any{"roles": []any{"editor"}},
	})
	canWrite, _ := e.Can(ctx, "posts:write")
	canDelete, _ := e.Can(ctx, "posts:delete")
	isEditor, _ := e.HasRole(ctx, "editor")
	fmt.Println(canWrite, canDelete, isEditor)
}
Output:
true false true

func (*Enforcer) CanAll

func (e *Enforcer) CanAll(ctx context.Context, perms ...string) (bool, error)

CanAll reports whether the user authenticated in ctx holds every one of perms. Arguments are validated before the context is consulted: it returns ErrInvalidPermission if perms is empty or contains a permission that is not concrete, then ErrNotConfigured if the enforcer has neither a RoleStore nor embedded claims, then ErrNoClaims if ctx has no claims.

func (*Enforcer) CanAllOn

func (e *Enforcer) CanAllOn(ctx context.Context, resource string, perms ...string) (bool, error)

CanAllOn reports whether the user authenticated in ctx holds every one of perms on resource. Each permission may come from a different source: one from a global role and another from a binding. Arguments are validated before the context is consulted, as for Enforcer.CanOn.

func (*Enforcer) CanAny

func (e *Enforcer) CanAny(ctx context.Context, perms ...string) (bool, error)

CanAny reports whether the user authenticated in ctx holds at least one of perms. Errors are as for Enforcer.CanAll.

func (*Enforcer) CanAnyOn

func (e *Enforcer) CanAnyOn(ctx context.Context, resource string, perms ...string) (bool, error)

CanAnyOn reports whether the user authenticated in ctx holds at least one of perms on resource. Errors are as for Enforcer.CanAllOn.

func (*Enforcer) CanOn

func (e *Enforcer) CanOn(ctx context.Context, resource, perm string) (bool, error)

CanOn reports whether the user authenticated in ctx holds the concrete permission perm on resource. It returns ErrInvalidPermission if perm is not concrete, ErrNotConfigured if no ScopedRoleStore or policy is configured, ErrInvalidScope if resource is not a concrete scope and ErrNoClaims if ctx has no claims, in that order.

Example
package main

import (
	"context"
	"errors"
	"fmt"
	"log"

	"github.com/tpyle/rbac"
)

func main() {
	policy, _ := rbac.NewPolicy(map[string]rbac.RoleDefinition{
		"viewer": {Permissions: []string{"docs:read"}},
		"editor": {Inherits: []string{"viewer"}, Permissions: []string{"docs:write"}},
	})
	// In production this is a database table of (subject, scope, role).
	bindings := rbac.NewMemoryScopedRoleStore()
	if err := errors.Join(
		bindings.Bind("alice", "projects/42", "editor"),       // one project and everything in it
		bindings.Bind("alice", "projects/*/docs/7", "viewer"), // doc 7 of every project
	); err != nil {
		log.Fatal(err)
	}

	e, _ := rbac.New(rbac.WithPolicy(policy), rbac.WithScopedRoleStore(bindings), rbac.WithClaimsSource(rbac.ClaimsFromContext))

	// alice has no global roles in her token.
	ctx := rbac.ContextWithClaims(context.Background(), &rbac.Claims{
		Subject: "alice",
		Values:  map[string]any{"roles": []any{}},
	})
	for _, check := range []struct{ resource, perm string }{
		{"projects/42/docs/1", "docs:write"},
		{"projects/43/docs/1", "docs:read"},
		{"projects/43/docs/7", "docs:read"},
		{"projects/43/docs/7", "docs:write"},
	} {
		ok, _ := e.CanOn(ctx, check.resource, check.perm)
		fmt.Println(check.resource, check.perm, ok)
	}
}
Output:
projects/42/docs/1 docs:write true
projects/43/docs/1 docs:read false
projects/43/docs/7 docs:read true
projects/43/docs/7 docs:write false

func (*Enforcer) ClaimsCanAll

func (e *Enforcer) ClaimsCanAll(ctx context.Context, c *Claims, perms ...string) (bool, error)

ClaimsCanAll is like Enforcer.CanAll for explicitly supplied claims.

func (*Enforcer) ClaimsCanAllOn

func (e *Enforcer) ClaimsCanAllOn(ctx context.Context, c *Claims, resource string, perms ...string) (bool, error)

ClaimsCanAllOn is like Enforcer.CanAllOn for explicitly supplied claims.

func (*Enforcer) ClaimsCanAny

func (e *Enforcer) ClaimsCanAny(ctx context.Context, c *Claims, perms ...string) (bool, error)

ClaimsCanAny is like Enforcer.CanAny for explicitly supplied claims.

func (*Enforcer) ClaimsCanAnyOn

func (e *Enforcer) ClaimsCanAnyOn(ctx context.Context, c *Claims, resource string, perms ...string) (bool, error)

ClaimsCanAnyOn is like Enforcer.CanAnyOn for explicitly supplied claims.

func (*Enforcer) ClaimsHaveAnyRole

func (e *Enforcer) ClaimsHaveAnyRole(ctx context.Context, c *Claims, roles ...string) (bool, error)

ClaimsHaveAnyRole is like Enforcer.HasAnyRole for explicitly supplied claims, such as those of a token verified outside a request.

func (*Enforcer) ClaimsHaveAnyRoleOn

func (e *Enforcer) ClaimsHaveAnyRoleOn(ctx context.Context, c *Claims, resource string, roles ...string) (bool, error)

ClaimsHaveAnyRoleOn is like Enforcer.HasAnyRoleOn for explicitly supplied claims, such as those of a token verified outside a request.

func (*Enforcer) ClaimsProvider

func (e *Enforcer) ClaimsProvider(base ClaimsProviderFunc) ClaimsProviderFunc

ClaimsProvider returns a function that computes the configured RBAC claims for subject, for the token issuer to add to access tokens. Call it on every login and refresh, so embedded claims are at most one access-token lifetime out of date. See ClaimsProviderFunc for passing it to an authentication library.

Claims from base, if not nil, are included too. base must not set the names of the claims rbac embeds; the name of a disabled claim is free for base to use. With nothing embedded, only base's claims are returned.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/tpyle/rbac"
)

func main() {
	ctx := context.Background()
	policy, _ := rbac.ParsePolicyJSON([]byte(`{"roles": {
		"viewer": {"permissions": ["posts:read"]},
		"editor": {"inherits": ["viewer"], "permissions": ["posts:write"]}
	}}`))
	roles := rbac.NewMemoryRoleStore()
	roles.SetRoles("alice", "editor")

	e, err := rbac.New(
		rbac.WithRoleStore(roles),
		rbac.WithPolicy(policy),
		rbac.WithEmbedRoles(true),
		rbac.WithEmbedPermissions(true),
	)
	if err != nil {
		log.Fatal(err)
	}
	// The token issuer calls this on every login and refresh.
	claims, _ := e.ClaimsProvider(nil)(ctx, "alice")
	fmt.Println(claims["roles"], claims["perms"])
}
Output:
[editor viewer] [posts:read posts:write]

func (*Enforcer) Config

func (e *Enforcer) Config() Config

Config returns the enforcer's plain-data settings.

func (*Enforcer) EffectiveRoles

func (e *Enforcer) EffectiveRoles(ctx context.Context, subject string) ([]string, error)

EffectiveRoles returns subject's roles from the RoleStore, plus the roles they inherit if a policy is configured. It ignores tokens, and reports the store's contents even with Config.ScopedRolesOnly.

func (*Enforcer) HasAnyRole

func (e *Enforcer) HasAnyRole(ctx context.Context, roles ...string) (bool, error)

HasAnyRole reports whether the user authenticated in ctx has at least one of roles. Arguments are validated before the context is consulted: it returns ErrInvalidRole if roles is empty or contains an invalid name, then ErrNotConfigured if the enforcer has neither a RoleStore nor embedded roles, then ErrNoClaims if ctx has no claims.

func (*Enforcer) HasAnyRoleOn

func (e *Enforcer) HasAnyRoleOn(ctx context.Context, resource string, roles ...string) (bool, error)

HasAnyRoleOn reports whether the user authenticated in ctx has at least one of roles on resource. Arguments are validated before the context is consulted, as for Enforcer.HasRoleOn.

func (*Enforcer) HasRole

func (e *Enforcer) HasRole(ctx context.Context, role string) (bool, error)

HasRole reports whether the user authenticated in ctx (see WithClaimsSource) has role, directly or through inheritance. It returns ErrInvalidRole if role is invalid and ErrNoClaims if ctx has no claims.

func (*Enforcer) HasRoleOn

func (e *Enforcer) HasRoleOn(ctx context.Context, resource, role string) (bool, error)

HasRoleOn reports whether the user authenticated in ctx has role on resource, globally or through a binding. It returns ErrInvalidRole if role is invalid, ErrNotConfigured if no ScopedRoleStore is configured, ErrInvalidScope if resource is not a concrete scope and ErrNoClaims if ctx has no claims, in that order.

func (*Enforcer) Permissions

func (e *Enforcer) Permissions(ctx context.Context, subject string) ([]string, error)

Permissions returns the grants subject holds through the roles in the RoleStore and the policy. It ignores tokens.

func (*Enforcer) RequireAllPermissions

func (e *Enforcer) RequireAllPermissions(perms ...string) Middleware

RequireAllPermissions returns middleware that only admits requests whose user holds every one of perms (see Enforcer.CanAll). It panics if perms is empty or contains a permission that is not concrete, or if the enforcer has neither a RoleStore nor embedded claims to answer it. See Enforcer.RequireAnyRole for where to mount it.

Example
package main

import (
	"net/http"

	"github.com/tpyle/rbac"
)

func main() {
	var e *rbac.Enforcer // from rbac.New, with rbac.WithClaimsSource(rbac.ClaimsFromContext)
	// authenticate is your authentication middleware: it verifies the
	// token and stores its claims with rbac.ContextWithClaims.
	var authenticate func(http.Handler) http.Handler
	if e == nil || authenticate == nil {
		return
	}
	mux := http.NewServeMux()
	mux.Handle("DELETE /posts/{id}", e.RequireAllPermissions("posts:delete")(http.HandlerFunc(
		func(w http.ResponseWriter, r *http.Request) { /* ... */ },
	)))
	// The token is verified first; rbac then checks access.
	_ = http.ListenAndServe(":8080", authenticate(mux))
}

func (*Enforcer) RequireAllPermissionsOn

func (e *Enforcer) RequireAllPermissionsOn(resource ResourceFunc, perms ...string) Middleware

RequireAllPermissionsOn returns middleware that only admits requests whose user holds every one of perms on the resource named by resource (see Enforcer.CanAllOn). It panics if no ScopedRoleStore or policy is configured, if resource is nil, or if perms is empty or contains a permission that is not concrete. See Enforcer.RequireAnyRole for where to mount it.

Example
package main

import (
	"net/http"

	"github.com/tpyle/rbac"
)

func main() {
	var e *rbac.Enforcer                             // from rbac.New, with rbac.WithScopedRoleStore
	var authenticate func(http.Handler) http.Handler // verifies tokens, as above
	if e == nil || authenticate == nil {
		return
	}
	mux := http.NewServeMux()
	doc := rbac.PathResource("projects/{project}/docs/{doc}")
	mux.Handle("PUT /projects/{project}/docs/{doc}", e.RequireAllPermissionsOn(doc, "docs:write")(http.HandlerFunc(
		func(w http.ResponseWriter, r *http.Request) { /* ... */ },
	)))
	_ = http.ListenAndServe(":8080", authenticate(mux))
}

func (*Enforcer) RequireAnyPermission

func (e *Enforcer) RequireAnyPermission(perms ...string) Middleware

RequireAnyPermission returns middleware that only admits requests whose user holds at least one of perms (see Enforcer.CanAny). It panics as Enforcer.RequireAllPermissions does. See Enforcer.RequireAnyRole for where to mount it.

func (*Enforcer) RequireAnyPermissionOn

func (e *Enforcer) RequireAnyPermissionOn(resource ResourceFunc, perms ...string) Middleware

RequireAnyPermissionOn returns middleware that only admits requests whose user holds at least one of perms on the resource named by resource (see Enforcer.CanAnyOn). It panics as Enforcer.RequireAllPermissionsOn does.

func (*Enforcer) RequireAnyRole

func (e *Enforcer) RequireAnyRole(roles ...string) Middleware

RequireAnyRole returns middleware that only admits requests whose user has at least one of roles (see Enforcer.HasAnyRole). It panics if roles is empty or contains an invalid name, or if the enforcer has neither a RoleStore nor embedded roles to answer it, since those are programming errors.

Mount it inside the authentication middleware that verifies the token and puts its claims in the request context, where the enforcer's ClaimsSource finds them. Rejected requests go to the ErrorHandler.

func (*Enforcer) RequireAnyRoleOn

func (e *Enforcer) RequireAnyRoleOn(resource ResourceFunc, roles ...string) Middleware

RequireAnyRoleOn returns middleware that only admits requests whose user has at least one of roles on the resource named by resource (see Enforcer.HasAnyRoleOn). It panics if no ScopedRoleStore is configured, if resource is nil, or if roles is empty or contains an invalid name. See Enforcer.RequireAnyRole for where to mount it.

func (*Enforcer) SubjectCan

func (e *Enforcer) SubjectCan(ctx context.Context, subject, perm string) (bool, error)

SubjectCan reports whether subject holds the concrete permission perm according to the RoleStore and policy, ignoring tokens. Like other global checks, it returns ErrNotConfigured with Config.ScopedRolesOnly.

func (*Enforcer) SubjectCanOn

func (e *Enforcer) SubjectCanOn(ctx context.Context, subject, resource, perm string) (bool, error)

SubjectCanOn reports whether subject holds the concrete permission perm on resource according to the RoleStore, ScopedRoleStore and policy, ignoring tokens. With Config.ScopedRolesOnly, the RoleStore is not consulted.

func (*Enforcer) SubjectHasRole

func (e *Enforcer) SubjectHasRole(ctx context.Context, subject, role string) (bool, error)

SubjectHasRole reports whether subject has role according to the RoleStore and policy, ignoring tokens. Like other global checks, it returns ErrNotConfigured with Config.ScopedRolesOnly.

func (*Enforcer) SubjectHasRoleOn

func (e *Enforcer) SubjectHasRoleOn(ctx context.Context, subject, resource, role string) (bool, error)

SubjectHasRoleOn reports whether subject has role on resource according to the RoleStore, ScopedRoleStore and policy, ignoring tokens. With Config.ScopedRolesOnly, the RoleStore is not consulted.

type ErrorHandler

type ErrorHandler func(w http.ResponseWriter, r *http.Request, err error)

ErrorHandler writes the response when middleware rejects a request. err wraps ErrNoClaims when the request is not authenticated, ErrForbidden when it lacks the required access, ErrInvalidScope when scoped middleware finds no valid resource in the request, and anything else for internal failures such as an unreachable RoleStore.

type MemoryRoleStore

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

MemoryRoleStore is an in-memory RoleStore. It is intended for tests, examples and prototypes.

func NewMemoryRoleStore

func NewMemoryRoleStore() *MemoryRoleStore

NewMemoryRoleStore returns an empty MemoryRoleStore.

func (*MemoryRoleStore) AddRoles

func (s *MemoryRoleStore) AddRoles(subject string, roles ...string)

AddRoles assigns additional roles to subject.

func (*MemoryRoleStore) RemoveRoles

func (s *MemoryRoleStore) RemoveRoles(subject string, roles ...string)

RemoveRoles unassigns roles from subject.

func (*MemoryRoleStore) Roles

func (s *MemoryRoleStore) Roles(_ context.Context, subject string) ([]string, error)

Roles implements RoleStore. It returns a copy.

func (*MemoryRoleStore) SetRoles

func (s *MemoryRoleStore) SetRoles(subject string, roles ...string)

SetRoles replaces the roles assigned to subject. With no roles, the subject is removed.

type MemoryScopedRoleStore

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

MemoryScopedRoleStore is an in-memory ScopedRoleStore. It is intended for tests, examples and prototypes.

func NewMemoryScopedRoleStore

func NewMemoryScopedRoleStore() *MemoryScopedRoleStore

NewMemoryScopedRoleStore returns an empty MemoryScopedRoleStore.

func (*MemoryScopedRoleStore) Bind

func (s *MemoryScopedRoleStore) Bind(subject, scope string, roles ...string) error

Bind grants roles to subject on every scope matching the scope pattern scope. It returns ErrInvalidScope if scope is not a valid pattern (see ValidateScopePattern) and ErrInvalidRole if a role name is invalid, in which case nothing is bound.

func (*MemoryScopedRoleStore) Bindings

func (s *MemoryScopedRoleStore) Bindings(_ context.Context, subject string, scopes []string) ([]Binding, error)

Bindings implements ScopedRoleStore. Like a database implementation, it returns the bindings on any of scopes plus every binding whose scope contains a wildcard, sorted by scope and role.

func (*MemoryScopedRoleStore) Unbind

func (s *MemoryScopedRoleStore) Unbind(subject, scope string, roles ...string)

Unbind removes roles bound to subject on the scope pattern scope. With no roles, every role bound on scope is removed.

type Middleware

type Middleware func(http.Handler) http.Handler

Middleware wraps an http.Handler. Every Require method panics when building middleware if the enforcer has no ClaimsSource (see WithClaimsSource), since it could never find a user.

type Option

type Option func(*settings)

Option configures an Enforcer.

func WithClaimsSource

func WithClaimsSource(src ClaimsSource) Option

WithClaimsSource sets where context-based checks such as Enforcer.Can and the middleware find the claims of the authenticated user. Pass ClaimsFromContext if your authentication middleware stores claims with ContextWithClaims, or a function that reads and converts the claims your authentication library stores. There is no default, so that a service that forgets to set one fails loudly rather than rejecting every request as unauthenticated: without a source (or with a nil src), those checks fail with ErrNotConfigured and the middleware panics when built. Checks on explicit claims, such as Enforcer.ClaimsCanAll, and on subjects, such as Enforcer.SubjectCan, don't need one.

Example
package main

import (
	"context"
	"fmt"

	"github.com/tpyle/rbac"
)

// verifiedToken stands in for the claims type of an authentication library.
type verifiedToken struct {
	Sub    string
	Claims map[string]any
}

type tokenKey struct{}

func main() {
	policy, _ := rbac.NewPolicy(map[string]rbac.RoleDefinition{
		"editor": {Permissions: []string{"posts:write"}},
	})
	// Read the claims where the authentication library left them.
	source := func(ctx context.Context) (*rbac.Claims, bool) {
		t, ok := ctx.Value(tokenKey{}).(*verifiedToken)
		if !ok {
			return nil, false
		}
		return &rbac.Claims{Subject: t.Sub, Values: t.Claims}, true
	}
	e, _ := rbac.New(rbac.WithPolicy(policy), rbac.WithClaimsSource(source))

	ctx := context.WithValue(context.Background(), tokenKey{}, &verifiedToken{
		Sub:    "alice",
		Claims: map[string]any{"roles": []any{"editor"}},
	})
	ok, _ := e.Can(ctx, "posts:write")
	fmt.Println(ok)
}
Output:
true

func WithConfig

func WithConfig(cfg Config) Option

WithConfig replaces all plain-data settings with cfg. Options applied after it override individual fields.

func WithEmbedPermissions

func WithEmbedPermissions(embed bool) Option

WithEmbedPermissions sets Config.EmbedPermissions.

func WithEmbedRoles

func WithEmbedRoles(embed bool) Option

WithEmbedRoles sets Config.EmbedRoles.

func WithErrorHandler

func WithErrorHandler(h ErrorHandler) Option

WithErrorHandler replaces the default middleware error responses (a JSON 401, 403 or 500).

func WithLogger

func WithLogger(l *slog.Logger) Option

WithLogger sets the logger for internal errors in middleware. The default is slog.Default.

func WithOptions

func WithOptions(opts ...Option) Option

WithOptions applies opts in order, so that a set of options can be passed around as one, for example by a package integrating rbac with an authentication library.

func WithPermissionsClaim

func WithPermissionsClaim(name string) Option

WithPermissionsClaim sets Config.PermissionsClaim.

func WithPolicy

func WithPolicy(p *Policy) Option

WithPolicy sets a fixed role-to-permission policy. It is shorthand for WithPolicyProvider(StaticPolicy(p)); a nil p removes the policy.

func WithPolicyProvider

func WithPolicyProvider(p PolicyProvider) Option

WithPolicyProvider sets where the role-to-permission policy is read from, for policies that can change at runtime (see NewCachedPolicyProvider).

func WithReservedClaims

func WithReservedClaims(reserved func(name string) bool) Option

WithReservedClaims makes New reject a Config.RolesClaim or Config.PermissionsClaim for which reserved returns true, in addition to the registered claims Config.Validate rejects. Use it with the token issuer's own list of claims it manages, so that a clash is reported when the enforcer is built rather than when the first token is issued. A nil reserved reserves nothing more.

func WithRoleStore

func WithRoleStore(store RoleStore) Option

WithRoleStore sets where users' role assignments are read from. It is required to issue claims and to check tokens that lack them.

func WithRolesClaim

func WithRolesClaim(name string) Option

WithRolesClaim sets Config.RolesClaim.

func WithScopeResolver

func WithScopeResolver(r ScopeResolver) Option

WithScopeResolver sets how scoped checks find the scopes that apply to a resource. The default, also used when r is nil, is PathScopes.

func WithScopedRoleStore

func WithScopedRoleStore(store ScopedRoleStore) Option

WithScopedRoleStore sets where users' roles on individual resources and groups of resources are read from. It is required for scoped checks such as Enforcer.CanOn that the user's global grants do not answer.

func WithScopedRolesOnly

func WithScopedRolesOnly() Option

WithScopedRolesOnly sets Config.ScopedRolesOnly and turns off embedding, which it excludes.

Example
package main

import (
	"context"
	"errors"
	"fmt"
	"log"

	"github.com/tpyle/rbac"
)

func main() {
	policy, _ := rbac.NewPolicy(map[string]rbac.RoleDefinition{
		"admin": {Permissions: []string{"**"}},
	})
	bindings := rbac.NewMemoryScopedRoleStore()
	if err := bindings.Bind("alice", "projects/42", "admin"); err != nil {
		log.Fatal(err)
	}
	e, _ := rbac.New(
		rbac.WithScopedRolesOnly(),
		rbac.WithPolicy(policy),
		rbac.WithScopedRoleStore(bindings),
		rbac.WithClaimsSource(rbac.ClaimsFromContext),
	)

	// The token claims alice is a global admin, but this service ignores
	// global roles.
	ctx := rbac.ContextWithClaims(context.Background(), &rbac.Claims{
		Subject: "alice",
		Values:  map[string]any{"roles": []any{"admin"}},
	})
	here, _ := e.CanOn(ctx, "projects/42", "projects:delete")
	elsewhere, _ := e.CanOn(ctx, "projects/43", "projects:delete")
	_, err := e.Can(ctx, "projects:delete")
	fmt.Println(here, elsewhere, errors.Is(err, rbac.ErrNotConfigured))
}
Output:
true false true

type Policy

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

Policy maps roles to permissions. It is created with NewPolicy (or loaded with ParsePolicyJSON), is immutable, and is safe for concurrent use. A nil *Policy defines no roles.

func LoadPolicyJSON

func LoadPolicyJSON(r io.Reader) (*Policy, error)

LoadPolicyJSON is like ParsePolicyJSON but reads from r.

func NewPolicy

func NewPolicy(defs map[string]RoleDefinition) (*Policy, error)

NewPolicy validates defs and returns a policy. Role names must be non-empty without whitespace, grants and exceptions must be valid (see ValidateGrant), inherited roles must be defined (ErrUnknownRole) and inheritance must be acyclic (ErrRoleCycle). defs is copied.

Example
package main

import (
	"fmt"
	"log"

	"github.com/tpyle/rbac"
)

func main() {
	policy, err := rbac.NewPolicy(map[string]rbac.RoleDefinition{
		"viewer": {Permissions: []string{"posts:read"}},
		"editor": {Inherits: []string{"viewer"}, Permissions: []string{"posts:*"}},
	})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(policy.EffectiveRoles("editor"))
	fmt.Println(policy.Permissions("editor"))
	ok, _ := policy.Can([]string{"editor"}, "posts:delete")
	fmt.Println(ok)
}
Output:
[editor viewer]
[posts:* posts:read]
true

func ParsePolicyJSON

func ParsePolicyJSON(data []byte) (*Policy, error)

ParsePolicyJSON decodes a PolicyFile from JSON and builds a Policy. Unknown fields, trailing data and a missing roles key are rejected with ErrInvalidPolicy, so mistakes such as "permisions" or an empty file do not silently remove permissions.

func (*Policy) Can

func (p *Policy) Can(roles []string, perm string) (bool, error)

Can reports whether any of roles grants the concrete permission perm. It returns an error wrapping ErrInvalidPermission if perm is not concrete.

func (*Policy) Definitions

func (p *Policy) Definitions() map[string]RoleDefinition

Definitions returns a copy of the role definitions the policy was built from.

func (*Policy) EffectiveRoles

func (p *Policy) EffectiveRoles(assigned ...string) []string

EffectiveRoles returns assigned plus every role they inherit, sorted and without duplicates. Roles the policy does not define are kept as they are, so a role can be checked with Enforcer.HasRole without granting any permissions.

func (*Policy) HasRole

func (p *Policy) HasRole(name string) bool

HasRole reports whether the policy defines the role name.

func (*Policy) Permissions

func (p *Policy) Permissions(roles ...string) []string

Permissions returns the grants held by the given roles, directly or through inheritance, sorted and without duplicates. Undefined roles grant nothing. A grant narrowed by a role's exceptions carries them, as in "files:* -files:delete" (see Grants).

func (*Policy) Roles

func (p *Policy) Roles() []string

Roles returns the names of the roles the policy defines, sorted.

type PolicyFile

type PolicyFile struct {
	Roles map[string]RoleDefinition `json:"roles" yaml:"roles"`
}

PolicyFile is the document format of a policy file:

{
  "roles": {
    "viewer": {"permissions": ["posts:read"]},
    "editor": {"inherits": ["viewer"], "permissions": ["posts:*"], "except": ["posts:delete"]}
  }
}

The YAML loader in the rbacyaml subpackage uses the same structure.

func (PolicyFile) Policy

func (f PolicyFile) Policy() (*Policy, error)

Policy validates the file and builds a Policy from it. The roles key is required, so an empty or truncated file is rejected with ErrInvalidPolicy instead of producing a policy that grants nothing; a policy that really has no roles must say so with an empty roles object.

type PolicyProvider

type PolicyProvider interface {
	Policy(ctx context.Context) (*Policy, error)
}

PolicyProvider supplies the current Policy. Use StaticPolicy for a policy fixed at startup, or NewCachedPolicyProvider for one loaded from a database. It is called on every check that needs the policy, so it should be fast.

func StaticPolicy

func StaticPolicy(p *Policy) PolicyProvider

StaticPolicy returns a PolicyProvider that always returns p.

type PolicyProviderFunc

type PolicyProviderFunc func(ctx context.Context) (*Policy, error)

PolicyProviderFunc adapts a function to a PolicyProvider.

func (PolicyProviderFunc) Policy

func (f PolicyProviderFunc) Policy(ctx context.Context) (*Policy, error)

Policy implements PolicyProvider.

type ResourceFunc

type ResourceFunc func(r *http.Request) (string, error)

ResourceFunc extracts the resource a request acts on, for scoped middleware such as Enforcer.RequireAllPermissionsOn. An error wrapping ErrInvalidScope is answered with 400 by the default ErrorHandler; other errors with 500. See PathResource for the common case.

func PathResource

func PathResource(pattern string) ResourceFunc

PathResource returns a ResourceFunc that builds the resource from pattern, replacing each "{name}" with the request's path value name (see http.Request.PathValue). It pairs with http.ServeMux patterns:

mux.Handle("PUT /projects/{project}/docs/{doc}",
	e.RequireAllPermissionsOn(rbac.PathResource("projects/{project}/docs/{doc}"), "docs:write")(h))

Path values are unescaped, so a request for "/projects/42%2Fdocs/docs/1" has the project value "42/docs". A value for "{name}" containing ScopeSeparator is therefore rejected, since it would change which scopes are the resource's ancestors. Write "{name...}" to allow several segments, as for a ServeMux "{name...}" wildcard.

Each placeholder must be a whole segment of pattern, and a "{name...}" placeholder must end it, so that different requests can't build the same resource ("{org}{team}" would make org 4, team 27 and org 42, team 7 the same). Placeholder names follow ServeMux's rules.

A request whose path value is empty, contains a separator where not allowed, or yields an invalid scope (see ValidateScope) is rejected with ErrInvalidScope. PathResource panics if pattern has unbalanced braces, breaks the placeholder rules above, or would not be a valid scope with its placeholders filled in, since that is a programming error.

type RoleDefinition

type RoleDefinition struct {
	// Permissions are the grants the role holds directly. They may contain
	// wildcards (see [ValidateGrant]).
	Permissions []string `json:"permissions,omitempty" yaml:"permissions,omitempty"`
	// Inherits names roles whose roles and permissions this role also has.
	// For example an "admin" role inheriting "editor" is also an "editor".
	Inherits []string `json:"inherits,omitempty" yaml:"inherits,omitempty"`
	// Except lists grants carved out of Permissions: the role holds what
	// Permissions match and no exception does, so ["files:*"] except
	// ["files:delete"] grants every files permission but delete. They may
	// contain wildcards.
	//
	// Exceptions narrow only the role's own Permissions. Permissions from
	// inherited roles are unaffected, since a role inheriting "viewer" is a
	// "viewer"; to withhold a permission a parent grants, don't inherit it.
	// Likewise a user holds a permission if any of their roles grants it.
	Except []string `json:"except,omitempty" yaml:"except,omitempty"`
}

RoleDefinition describes one role of a Policy.

Example (Except)
package main

import (
	"fmt"

	"github.com/tpyle/rbac"
)

func main() {
	policy, _ := rbac.NewPolicy(map[string]rbac.RoleDefinition{
		// Every files permission except delete.
		"editor": {Permissions: []string{"files:*"}, Except: []string{"files:delete"}},
		// An editor who may also delete.
		"owner": {Inherits: []string{"editor"}, Permissions: []string{"files:delete"}},
	})
	for _, role := range []string{"editor", "owner"} {
		canWrite, _ := policy.Can([]string{role}, "files:write")
		canDelete, _ := policy.Can([]string{role}, "files:delete")
		fmt.Println(role, canWrite, canDelete, policy.Permissions(role))
	}
}
Output:
editor true false [files:* -files:delete]
owner true true [files:* -files:delete files:delete]

type RoleStore

type RoleStore interface {
	// Roles returns the roles directly assigned to subject (inherited roles
	// are added from the [Policy]). An unknown subject has no roles and
	// should return an empty slice and a nil error.
	Roles(ctx context.Context, subject string) ([]string, error)
}

RoleStore is the source of truth for which roles each user is assigned, typically a database table. It is used to build token claims and, when a token lacks them, to answer checks directly.

type RoleStoreFunc

type RoleStoreFunc func(ctx context.Context, subject string) ([]string, error)

RoleStoreFunc adapts a function to a RoleStore.

func (RoleStoreFunc) Roles

func (f RoleStoreFunc) Roles(ctx context.Context, subject string) ([]string, error)

Roles implements RoleStore.

type ScopeResolver

type ScopeResolver interface {
	Scopes(ctx context.Context, resource string) ([]string, error)
}

ScopeResolver lists the scopes whose bindings apply to a resource: the resource itself and every group it belongs to, such as a parent folder, a project or a tag. The default is PathScopes.

Returned scopes must be concrete (see ValidateScope). An error wrapping ErrInvalidScope tells middleware the request named a bad resource (400); other errors are internal (500).

func PathScopes

func PathScopes() ScopeResolver

PathScopes returns a ScopeResolver for hierarchical resource names. It returns the resource followed by each of its ancestors, so that a binding on a scope applies to everything beneath it:

"projects/42/docs/7" -> ["projects/42/docs/7", "projects/42/docs", "projects/42", "projects"]

Resources that are not concrete scopes are rejected with ErrInvalidScope.

Example
package main

import (
	"context"
	"fmt"

	"github.com/tpyle/rbac"
)

func main() {
	scopes, _ := rbac.PathScopes().Scopes(context.Background(), "projects/42/docs/7")
	fmt.Println(scopes)
}
Output:
[projects/42/docs/7 projects/42/docs projects/42 projects]

type ScopeResolverFunc

type ScopeResolverFunc func(ctx context.Context, resource string) ([]string, error)

ScopeResolverFunc adapts a function to a ScopeResolver.

func (ScopeResolverFunc) Scopes

func (f ScopeResolverFunc) Scopes(ctx context.Context, resource string) ([]string, error)

Scopes implements ScopeResolver.

type ScopedRoleStore

type ScopedRoleStore interface {
	// Bindings returns subject's bindings that may apply to scopes. It must
	// return at least every binding of subject whose scope is one of scopes
	// or contains a wildcard. It may return more: every result is matched
	// against scopes with [MatchScope], so returning all of subject's
	// bindings is a correct, if slower, implementation. An unknown subject
	// has no bindings and should return an empty slice and a nil error.
	Bindings(ctx context.Context, subject string, scopes []string) ([]Binding, error)
}

ScopedRoleStore is the source of truth for the roles users hold on individual resources or groups of resources, typically a database table of (subject, scope, role) rows. Scoped bindings are never embedded in tokens, so scoped checks that global grants do not answer call it every time.

type ScopedRoleStoreFunc

type ScopedRoleStoreFunc func(ctx context.Context, subject string, scopes []string) ([]Binding, error)

ScopedRoleStoreFunc adapts a function to a ScopedRoleStore.

func (ScopedRoleStoreFunc) Bindings

func (f ScopedRoleStoreFunc) Bindings(ctx context.Context, subject string, scopes []string) ([]Binding, error)

Bindings implements ScopedRoleStore.

Directories

Path Synopsis
authrbac module
examples
basic command
Command basic walks through the rbac library without HTTP or an authentication library: it loads a policy, computes the claims a token issuer would embed, round-trips them through JSON as a JWT payload would be, and checks roles and permissions against the decoded claims.
Command basic walks through the rbac library without HTTP or an authentication library: it loads a policy, computes the claims a token issuer would embed, round-trips them through JSON as a JWT payload would be, and checks roles and permissions against the decoded claims.
middleware command
Command middleware shows rbac's HTTP middleware behind an authentication layer of your own.
Command middleware shows rbac's HTTP middleware behind an authentication layer of your own.
Package rbacyaml loads rbac.Policy values from YAML.
Package rbacyaml loads rbac.Policy values from YAML.

Jump to

Keyboard shortcuts

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