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:
- Enforcer.ClaimsProvider computes the user's roles and/or permissions for the token issuer to embed in access tokens, as chosen by Config.
- Enforcer.HasRole, Enforcer.Can and related methods answer checks for the user authenticated in a context, whose verified Claims the authentication middleware stores there (see ContextWithClaims and WithClaimsSource). They use the token's claims when present and otherwise look the user up in a RoleStore (usually a database), so every combination of embedded claims works.
- Enforcer.RequireAnyRole, Enforcer.RequireAllPermissions and Enforcer.RequireAnyPermission are net/http middleware built on those checks.
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 ¶
- Constants
- Variables
- func ContextWithClaims(ctx context.Context, c *Claims) context.Context
- func Grants(grant, perm string) bool
- func MatchScope(pattern, scope string) bool
- func ValidateGrant(grant string) error
- func ValidatePermission(perm string) error
- func ValidateScope(s string) error
- func ValidateScopePattern(s string) error
- type Binding
- type CacheOption
- type CachedPolicyProvider
- type Claims
- type ClaimsProviderFunc
- type ClaimsSource
- type Config
- type Enforcer
- func (e *Enforcer) Can(ctx context.Context, perm string) (bool, error)
- func (e *Enforcer) CanAll(ctx context.Context, perms ...string) (bool, error)
- func (e *Enforcer) CanAllOn(ctx context.Context, resource string, perms ...string) (bool, error)
- func (e *Enforcer) CanAny(ctx context.Context, perms ...string) (bool, error)
- func (e *Enforcer) CanAnyOn(ctx context.Context, resource string, perms ...string) (bool, error)
- func (e *Enforcer) CanOn(ctx context.Context, resource, perm string) (bool, error)
- func (e *Enforcer) ClaimsCanAll(ctx context.Context, c *Claims, perms ...string) (bool, error)
- func (e *Enforcer) ClaimsCanAllOn(ctx context.Context, c *Claims, resource string, perms ...string) (bool, error)
- func (e *Enforcer) ClaimsCanAny(ctx context.Context, c *Claims, perms ...string) (bool, error)
- func (e *Enforcer) ClaimsCanAnyOn(ctx context.Context, c *Claims, resource string, perms ...string) (bool, error)
- func (e *Enforcer) ClaimsHaveAnyRole(ctx context.Context, c *Claims, roles ...string) (bool, error)
- func (e *Enforcer) ClaimsHaveAnyRoleOn(ctx context.Context, c *Claims, resource string, roles ...string) (bool, error)
- func (e *Enforcer) ClaimsProvider(base ClaimsProviderFunc) ClaimsProviderFunc
- func (e *Enforcer) Config() Config
- func (e *Enforcer) EffectiveRoles(ctx context.Context, subject string) ([]string, error)
- func (e *Enforcer) HasAnyRole(ctx context.Context, roles ...string) (bool, error)
- func (e *Enforcer) HasAnyRoleOn(ctx context.Context, resource string, roles ...string) (bool, error)
- func (e *Enforcer) HasRole(ctx context.Context, role string) (bool, error)
- func (e *Enforcer) HasRoleOn(ctx context.Context, resource, role string) (bool, error)
- func (e *Enforcer) Permissions(ctx context.Context, subject string) ([]string, error)
- func (e *Enforcer) RequireAllPermissions(perms ...string) Middleware
- func (e *Enforcer) RequireAllPermissionsOn(resource ResourceFunc, perms ...string) Middleware
- func (e *Enforcer) RequireAnyPermission(perms ...string) Middleware
- func (e *Enforcer) RequireAnyPermissionOn(resource ResourceFunc, perms ...string) Middleware
- func (e *Enforcer) RequireAnyRole(roles ...string) Middleware
- func (e *Enforcer) RequireAnyRoleOn(resource ResourceFunc, roles ...string) Middleware
- func (e *Enforcer) SubjectCan(ctx context.Context, subject, perm string) (bool, error)
- func (e *Enforcer) SubjectCanOn(ctx context.Context, subject, resource, perm string) (bool, error)
- func (e *Enforcer) SubjectHasRole(ctx context.Context, subject, role string) (bool, error)
- func (e *Enforcer) SubjectHasRoleOn(ctx context.Context, subject, resource, role string) (bool, error)
- type ErrorHandler
- type MemoryRoleStore
- type MemoryScopedRoleStore
- type Middleware
- type Option
- func WithClaimsSource(src ClaimsSource) Option
- func WithConfig(cfg Config) Option
- func WithEmbedPermissions(embed bool) Option
- func WithEmbedRoles(embed bool) Option
- func WithErrorHandler(h ErrorHandler) Option
- func WithLogger(l *slog.Logger) Option
- func WithOptions(opts ...Option) Option
- func WithPermissionsClaim(name string) Option
- func WithPolicy(p *Policy) Option
- func WithPolicyProvider(p PolicyProvider) Option
- func WithReservedClaims(reserved func(name string) bool) Option
- func WithRoleStore(store RoleStore) Option
- func WithRolesClaim(name string) Option
- func WithScopeResolver(r ScopeResolver) Option
- func WithScopedRoleStore(store ScopedRoleStore) Option
- func WithScopedRolesOnly() Option
- type Policy
- func (p *Policy) Can(roles []string, perm string) (bool, error)
- func (p *Policy) Definitions() map[string]RoleDefinition
- func (p *Policy) EffectiveRoles(assigned ...string) []string
- func (p *Policy) HasRole(name string) bool
- func (p *Policy) Permissions(roles ...string) []string
- func (p *Policy) Roles() []string
- type PolicyFile
- type PolicyProvider
- type PolicyProviderFunc
- type ResourceFunc
- type RoleDefinition
- type RoleStore
- type RoleStoreFunc
- type ScopeResolver
- type ScopeResolverFunc
- type ScopedRoleStore
- type ScopedRoleStoreFunc
Examples ¶
Constants ¶
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.
const DefaultLoadTimeout = 30 * time.Second
DefaultLoadTimeout is how long a CachedPolicyProvider waits for its loader unless WithLoadTimeout says otherwise.
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
ClaimsFromContext returns the claims stored by ContextWithClaims, if any. It is a ClaimsSource for enforcers whose authentication middleware uses ContextWithClaims.
type ClaimsProviderFunc ¶
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 ¶
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 ¶
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:
- the permissions claim, if embedding permissions is enabled and the token has it;
- the roles claim through the Policy, if embedding roles is enabled and the token has it;
- 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:
- their global grants, resolved exactly as for Enforcer.Can and Enforcer.HasRole, which apply to every resource; and
- the grants of the roles bound to them in the ScopedRoleStore on any scope the ScopeResolver lists for the resource.
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
CanAny reports whether the user authenticated in ctx holds at least one of perms. Errors are as for Enforcer.CanAll.
func (*Enforcer) CanAnyOn ¶
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 ¶
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 ¶
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 ¶
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 ¶
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) EffectiveRoles ¶
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 ¶
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 ¶
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 ¶
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 ¶
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))
}
Output:
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))
}
Output:
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 ¶
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 ¶
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 ¶
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) 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 ¶
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 ¶
WithConfig replaces all plain-data settings with cfg. Options applied after it override individual fields.
func WithEmbedPermissions ¶
WithEmbedPermissions sets Config.EmbedPermissions.
func WithErrorHandler ¶
func WithErrorHandler(h ErrorHandler) Option
WithErrorHandler replaces the default middleware error responses (a JSON 401, 403 or 500).
func WithLogger ¶
WithLogger sets the logger for internal errors in middleware. The default is slog.Default.
func WithOptions ¶
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 ¶
WithPermissionsClaim sets Config.PermissionsClaim.
func WithPolicy ¶
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 ¶
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 ¶
WithRoleStore sets where users' role assignments are read from. It is required to issue claims and to check tokens that lack them.
func WithRolesClaim ¶
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 ¶
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 ¶
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 ¶
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 ¶
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) Permissions ¶
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).
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 ¶
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 ¶
PolicyProviderFunc adapts a function to a PolicyProvider.
func (PolicyProviderFunc) Policy ¶
func (f PolicyProviderFunc) Policy(ctx context.Context) (*Policy, error)
Policy implements PolicyProvider.
type ResourceFunc ¶
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 ¶
RoleStoreFunc adapts a function to a RoleStore.
type ScopeResolver ¶
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 ¶
ScopeResolverFunc adapts a function to a ScopeResolver.
func (ScopeResolverFunc) Scopes ¶
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.
Source Files
¶
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. |