access

package
v0.38.1 Latest Latest
Warning

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

Go to latest
Published: Jul 21, 2026 License: MIT Imports: 13 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Can

func Can(ctx context.Context, permission Permission) bool

Can reports whether the request context carries the given permission. It reads the RolePolicy and roles installed via WithPolicy / WithRoles (by access.Middleware or battery/auth). Returns false when no policy is present — the secure-by-default answer for an un-wired request. This is the seam the CRUD layer uses to enforce EntityConfig.Access.

func CanResource added in v0.30.0

func CanResource(ctx context.Context, capability Permission, resource Ref) bool

CanResource is the resource-aware capability check. It consults a Decider installed in ctx (via WithDecider / DeciderMiddleware) BEFORE the role policy:

  • DecisionAllow → true (role policy not consulted)
  • DecisionDeny → false (role policy not consulted)
  • DecisionAbstain → falls through to Can

With no decider in ctx the answer is exactly Can(ctx, capability) — the fail-closed semantics of the coarse role policy are unchanged, so existing RBAC-only wiring is byte-identical. A nil context fails closed to false.

Can itself is untouched: there is no wildcard/segment logic in the hot path. The resource-aware path is a separate entrypoint the CRUD layer opts into.

func DeciderMiddleware added in v0.30.0

func DeciderMiddleware(d Decider) func(http.Handler) http.Handler

DeciderMiddleware installs a Decider into the request context so downstream CanResource calls (auto-CRUD permission gates, RequirePermission-style handlers) consult it. Mount it alongside access.Middleware:

app.Use(access.Middleware(policy, roles.Resolve))
app.Use(access.DeciderMiddleware(teamMaintainerDecider))

This shape — a sibling constructor returning the same func(http.Handler) http.Handler type — matches Middleware's. The package uses positional constructors (Middleware(policy, roles)), not an options pattern, so a DeciderMiddleware(d) wrapper is the consistent way to add the decider without changing Middleware's signature or introducing an options idiom for a single field.

func GetRoles added in v0.3.2

func GetRoles(ctx context.Context) []string

GetRoles reads back the roles installed via WithRoles (by access.Middleware or battery/auth). It is the reader half of the role-context seam — without it, role context is one-way (you can put roles in but not read them out), which blocks role-based UI branching (e.g. "show the admin nav only when the caller holds 'admin'").

Returns nil when ctx is nil or carries no roles — never panics. A nil context is treated as an anonymous request.

func Middleware

func Middleware(policy *RolePolicy, roles func(ctx context.Context) []string) func(http.Handler) http.Handler

Middleware installs the RBAC policy and the request's roles into the context so downstream RequirePermission middleware and auto-CRUD permission gates (EntityConfig.Access) can resolve permissions. roles maps a request context to the caller's roles — typically by reading the authenticated user; pass nil to install only the policy (roles resolved elsewhere). Mount this once, app-wide or on a route group, ahead of any permission-gated routes.

func RequirePermission

func RequirePermission(permission Permission) func(http.Handler) http.Handler

RequirePermission returns HTTP middleware that checks if the current user has the specified permission. Returns 403 if denied.

func ScopeMatch added in v0.31.0

func ScopeMatch(granted []Permission, required Permission) bool

ScopeMatch reports whether any granted permission satisfies the required permission using the resource:verb wildcard grammar shared by token scopes and module capability grants:

  • exact match: "posts:read" grants "posts:read"
  • resource wildcard: "posts:*" grants any "posts:<verb>"
  • verb wildcard: "*:read" grants "<any-resource>:read"
  • grant-all: "*:*" grants everything

Empty grants deny everything (secure-by-default), and a required value that does not parse as "resource:verb" is never satisfied.

ScopeMatch is PURE: it is a function of its two arguments only. It does NOT consult the capability registry and does NOT expand resource wildcards the way RolePolicy.Grant does at grant time — "teams:*" matches literally here, against whatever the caller passes as required. Grant-time expansion (teams:* → teams:read, teams:write, …) is a separate concern that lives in RolePolicy; matching and expanding are deliberately not entangled.

This is the matcher the module-grant side and battery/auth's token scopes delegate to, so the resource:verb algebra has exactly one home. access.Can (the RBAC hot path) is untouched: it performs exact-string-or-global-"*" matching and must stay that way — widening it would silently change live RBAC for every caller.

func ValidScope added in v0.31.0

func ValidScope(s string) bool

ValidScope reports whether s is a well-formed "resource:verb" scope under the same closed vocabulary ScopeMatch and the token issuer use: each half is one or more of [a-z0-9_*-], separated by exactly one ':'. Both halves may be "*" so the wildcard scopes ScopeMatch documents ("posts:*", "*:read", "*:*") are mintable. Use this at install/mint time to reject malformed scope strings before they reach the matcher.

func WithDecider added in v0.30.0

func WithDecider(ctx context.Context, d Decider) context.Context

WithDecider stores a Decider in the context, mirroring the WithPolicy / WithRoles pair. Downstream CanResource calls will consult it before the role policy. Installing a decider is strictly additive: with none in ctx, CanResource answers exactly what Can answers.

func WithPolicy

func WithPolicy(ctx context.Context, policy *RolePolicy) context.Context

WithPolicy stores a RolePolicy in the context.

func WithRoles

func WithRoles(ctx context.Context, roles []string) context.Context

WithRoles stores user roles in the context.

Types

type CachedResolver added in v0.30.0

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

CachedResolver caches resolved roles by the authenticated user's ID. Contexts without a user exposing GetID() are resolved without caching so anonymous callers can never share an entry.

func NewCachedResolver added in v0.30.0

func NewCachedResolver(resolve func(context.Context) []string, opts ...CachedResolverOption) *CachedResolver

NewCachedResolver wraps a role resolver with per-user TTL caching. It derives the cache key from the authenticated user installed in core/handler context, the same identity seam used by battery/auth and access.Middleware resolvers.

func (*CachedResolver) Invalidate added in v0.30.0

func (r *CachedResolver) Invalidate(userID string)

Invalidate removes one user's cached roles. An in-flight result is not cached; calls waiting on it observe the invalidation and resolve again.

func (*CachedResolver) InvalidateAll added in v0.30.0

func (r *CachedResolver) InvalidateAll()

InvalidateAll removes every cached role resolution. In-flight results are not cached, and calls waiting on them resolve again.

func (*CachedResolver) Resolve added in v0.30.0

func (r *CachedResolver) Resolve(ctx context.Context) []string

Resolve returns the current user's roles. Concurrent misses for the same user share one resolver call. Returned roles are defensive copies.

type CachedResolverOption added in v0.30.0

type CachedResolverOption func(*CachedResolver)

CachedResolverOption configures a CachedResolver.

func WithTTL added in v0.30.0

func WithTTL(ttl time.Duration) CachedResolverOption

WithTTL sets the duration resolved roles remain cached. A zero or negative duration retains single-flight behavior but expires the result immediately.

type Decider added in v0.30.0

type Decider func(ctx context.Context, roles []string, capability Permission, resource Ref) Decision

Decider is consulted before the role policy when a resource-aware check (CanResource) runs. roles is the caller's resolved roles — the same slice access.Middleware / WithRoles install. The decider receives the capability and the Ref under check so it can express rules the coarse role policy cannot ("a team maintainer may edit their team's projects").

Return DecisionAbstain to fall through to Can. A nil/missing decider also falls through, so wiring a decider is strictly opt-in.

func GetDecider added in v0.30.0

func GetDecider(ctx context.Context) Decider

GetDecider reads back the Decider installed via WithDecider (by DeciderMiddleware or an explicit WithDecider call). It is the reader half of the decider-context seam. Returns nil when ctx is nil or carries no decider — never panics.

type Decision added in v0.30.0

type Decision int

Decision is the verdict a Decider returns for one resource-scoped capability check. The zero value is DecisionAbstain so a decider that forgets to return falls through to the role policy rather than silently allowing or denying.

const (
	// DecisionAbstain defers the check to the role policy (Can). Use it when
	// the decider has no opinion for this resource — e.g. the resource type is
	// not one it governs, or the caller holds a role it doesn't model.
	DecisionAbstain Decision = iota
	// DecisionAllow permits the check; the role policy is not consulted.
	DecisionAllow
	// DecisionDeny refuses the check even when the role policy would allow.
	DecisionDeny
)

type GrantStore added in v0.20.0

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

GrantStore persists role→permission grants to a database table so RBAC edits survive restarts. It wraps a live *RolePolicy: Grant/Revoke write the DB row AND mutate the in-memory policy in one call, keeping the two in sync. The policy's own RWMutex covers concurrent Can checks, so a Grant/Revoke call is "atomic enough" — a reader may see the state before or after the change, never a torn map.

The store holds a reference to the live *RolePolicy (store-holds-policy shape). Bind the policy at construction with NewGrantStore(db, policy), then call LoadInto once at boot to hydrate the policy from persisted rows. Subsequent Grant/Revoke calls mutate both layers.

All role and permission VALUES are passed as $n bound parameters — never interpolated into SQL. The table name is validated via query.SafeIdent at construction time and quoted via query.QuoteIdent in every statement.

Both SQLite (mattn/go-sqlite3) and PostgreSQL (lib/pq) accept $N placeholders and ON CONFLICT DO NOTHING, so the same SQL works on both.

func NewGrantStore added in v0.20.0

func NewGrantStore(db *sql.DB, policy *RolePolicy, opts ...GrantStoreOption) *GrantStore

NewGrantStore creates a GrantStore bound to the given policy. The policy reference is retained — Grant/Revoke mutate it directly so concurrent Can checks see the change without a reload. Call LoadInto once at boot to hydrate the policy from persisted rows.

A nil policy is allowed only if you intend to call LoadInto with a policy before any Grant/Revoke; Grant/Revoke on a store with a nil policy return an error.

func (*GrantStore) EnsureSchema added in v0.20.0

func (s *GrantStore) EnsureSchema(ctx context.Context) error

EnsureSchema creates the grants table if it does not already exist. Idempotent (CREATE TABLE IF NOT EXISTS). The column types (TEXT) are portable across SQLite and PostgreSQL. The (role, permission) pair has a UNIQUE constraint so INSERT ... ON CONFLICT DO NOTHING is a no-op for duplicates.

func (*GrantStore) Grant added in v0.20.0

func (s *GrantStore) Grant(ctx context.Context, role string, perms ...Permission) error

Grant validates and expands permissions, persists the resulting (role, permission) rows to the database (INSERT ... ON CONFLICT DO NOTHING), and then updates the live policy. Idempotent: granting an already-held permission is a no-op in both layers. In strict capability mode, validation happens before any database write.

Role and permission are bound as $n parameters — never interpolated.

func (*GrantStore) LoadInto added in v0.20.0

func (s *GrantStore) LoadInto(ctx context.Context, policy *RolePolicy) error

LoadInto reads all persisted grant rows and calls policy.Grant for each, hydrating the live *RolePolicy from the database. The policy is also retained as the store's active policy (overwriting any previously bound one) so subsequent Grant/Revoke calls mutate it. Call once at boot, after constructing the policy and after EnsureSchema.

If the store was constructed with a policy and policy is nil, the store's existing policy is used.

func (*GrantStore) Policy added in v0.20.0

func (s *GrantStore) Policy() *RolePolicy

Policy returns the live *RolePolicy the store mutates. May be nil if LoadInto has not yet been called and no policy was passed to NewGrantStore.

func (*GrantStore) Revoke added in v0.20.0

func (s *GrantStore) Revoke(ctx context.Context, role string, perms ...Permission) error

Revoke deletes (role, permission) rows from the database and then calls policy.Revoke on the live policy. Idempotent: revoking a permission the role doesn't hold is a no-op in both layers.

Role and permission are bound as $n parameters — never interpolated.

func (*GrantStore) SetFanout added in v0.36.0

func (s *GrantStore) SetFanout(f fanout.Fanout) (stop func(), err error)

SetFanout attaches a cross-replica fanout so grant/revoke propagate to other replicas and remote grant/revoke re-read authoritative state into the local policy. Mirrors the wiring shape of [island.Manager.SetFanout] and [ModuleManager.subscribeFanout]: store the backend, mint a node id, subscribe to accessFanoutTopic, and return the unsubscribe func as stop. Call once at boot; the returned stop is registered by the framework as an OnStop drainer. A nil fanout makes SetFanout a no-op returning a no-op stop, so callers can unconditionally wire it regardless of topology.

type GrantStoreOption added in v0.20.0

type GrantStoreOption func(*GrantStore)

GrantStoreOption configures a GrantStore.

func WithGrantTable added in v0.20.0

func WithGrantTable(name string) GrantStoreOption

WithGrantTable overrides the default table name ("access_grants"). The name is validated via query.SafeIdent — an unsafe identifier panics at construction time, not at query time.

type Permission

type Permission string

Permission represents an action permission string (e.g. "posts:read", "posts:write").

const Wildcard Permission = "*"

Wildcard is the superuser permission: a role granted "*" passes every permission check. Grant it deliberately and only to fully-trusted, separately-gated surfaces (e.g. the admin back-office, which has its own Authorize gate) — never to an end-user role.

func GetPermissions

func GetPermissions(ctx context.Context) []Permission

GetPermissions extracts the user's permissions from context by looking up the user's roles against the RolePolicy.

Returns nil if ctx is nil, missing a policy, or missing roles — never panics. A nil context is treated as an anonymous request rather than allowed to crash the handler.

type Policy

type Policy interface {
	Can(ctx context.Context, permission Permission) bool
}

Policy determines whether the subject in ctx holds a permission.

type Ref added in v0.30.0

type Ref struct {
	Type string
	ID   string
}

Ref identifies the resource a capability check is about. Type is the entity name (e.g. "projects"); ID is the record id, or "" for a collection-level check (List, Create, a batch, or the SSE feed). The zero Ref carries no resource information — a decider that ignores empty Refs is effectively opting out of resource-awareness for that call.

type RolePolicy

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

RolePolicy implements Policy using role-based permission grants.

Grant and Revoke may be called concurrently with Can / GetPermissions: the underlying role→permissions map is guarded by an RWMutex so reads don't block each other and writes won't trigger Go's concurrent-map fatal.

func NewRolePolicy

func NewRolePolicy() *RolePolicy

NewRolePolicy creates a new empty RolePolicy.

func PolicyFromContext added in v0.31.0

func PolicyFromContext(ctx context.Context) *RolePolicy

PolicyFromContext reads back the *RolePolicy installed via WithPolicy (by access.Middleware or battery/auth). It is the reader half of the policy-context seam — the symmetric pair to GetRoles. The process-module capability broker (framework/processmodule_broker.go, design #37 §5) uses it to snapshot the delegated caller's policy at delegation-mint time so the CrossOwnerRead carve-out can be checked on the reverse path without an app-wide policy reference.

Returns nil when ctx is nil or carries no policy — never panics.

func (*RolePolicy) Can

func (rp *RolePolicy) Can(ctx context.Context, permission Permission) bool

Can checks if the user from ctx has the given permission via any of their roles. A role holding the Wildcard permission ("*") passes any check.

func (*RolePolicy) Capabilities added in v0.30.0

func (rp *RolePolicy) Capabilities() []Permission

Capabilities returns the policy's registered capabilities in sorted order. The returned slice is a defensive copy.

func (*RolePolicy) Grant

func (rp *RolePolicy) Grant(role string, permissions ...Permission) error

Grant adds permissions to a role. Duplicate grants are ignored.

With a non-empty capability registry, resource wildcards such as "teams:*" expand to all registered capabilities with that prefix. Unknown grants warn by default and remain accepted for backward compatibility; a strict policy rejects them and returns an error.

func (*RolePolicy) PermissionsOf added in v0.20.0

func (rp *RolePolicy) PermissionsOf(role string) []Permission

PermissionsOf returns a defensive copy of the permissions granted to the given role. Returns nil when the role has no grants. Callers iterate the returned slice without holding the lock so a concurrent Grant/Revoke can't mutate it under them.

func (*RolePolicy) Register added in v0.30.0

func (rp *RolePolicy) Register(capabilities ...Permission)

Register adds capabilities to the policy's known capability registry. Registration is idempotent and safe to call concurrently with grants and permission checks.

func (*RolePolicy) ReplaceRole added in v0.36.0

func (rp *RolePolicy) ReplaceRole(role string, perms ...Permission) error

ReplaceRole atomically REPLACES the set of permissions held by role with perms (after the same prepare/validate/dedupe pass Grant uses). Unlike Grant, permissions absent from perms are dropped — the role ends up with exactly perms, nothing more. Use to re-sync a role's grants from an authoritative source (e.g. after a cross-replica invalidation re-reads the access_grants table). Honors strictCapabilities exactly like Grant: an unknown capability in perms is rejected and the role is left unchanged.

func (*RolePolicy) Revoke

func (rp *RolePolicy) Revoke(role string, permissions ...Permission)

Revoke removes specific permissions from a role.

func (*RolePolicy) Roles added in v0.20.0

func (rp *RolePolicy) Roles() []string

Roles returns the sorted list of all roles that currently have at least one granted permission. The slice is a defensive copy — callers can iterate it without holding the lock. Intended for admin UIs that need to enumerate the grant matrix; not used on the hot Can path.

func (*RolePolicy) Snapshot added in v0.36.0

func (rp *RolePolicy) Snapshot() map[string][]Permission

Snapshot returns a deep copy of the current role→permissions map. Used by GrantStore to capture the code-defined baseline grants (before DB grants are overlaid) so a cross-replica refresh can merge baseline ∪ DB instead of replacing a role with only its DB rows.

func (*RolePolicy) StrictCapabilities added in v0.30.0

func (rp *RolePolicy) StrictCapabilities() *RolePolicy

StrictCapabilities makes unknown capability grants fail instead of warning. It returns the policy so callers can opt in while constructing it.

type RoleWithOrigin added in v0.30.0

type RoleWithOrigin struct {
	Role   string
	Origin string
}

RoleWithOrigin identifies an effective role and where it came from, such as a direct user assignment or a resolved organization membership.

type UnknownCapabilityError added in v0.30.0

type UnknownCapabilityError struct {
	Grant   Permission
	Nearest Permission
}

UnknownCapabilityError reports a strict-mode grant of a capability that is not in the policy's registry. Handlers can errors.As on it to present the message as a caller mistake (e.g. HTTP 400) rather than a server fault.

func (*UnknownCapabilityError) Error added in v0.30.0

func (e *UnknownCapabilityError) Error() string

Jump to

Keyboard shortcuts

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