Documentation
¶
Index ¶
- func Can(ctx context.Context, permission Permission) bool
- func GetRoles(ctx context.Context) []string
- func Middleware(policy *RolePolicy, roles func(ctx context.Context) []string) func(http.Handler) http.Handler
- func RequirePermission(permission Permission) func(http.Handler) http.Handler
- func WithPolicy(ctx context.Context, policy *RolePolicy) context.Context
- func WithRoles(ctx context.Context, roles []string) context.Context
- type GrantStore
- func (s *GrantStore) EnsureSchema(ctx context.Context) error
- func (s *GrantStore) Grant(ctx context.Context, role string, perms ...Permission) error
- func (s *GrantStore) LoadInto(ctx context.Context, policy *RolePolicy) error
- func (s *GrantStore) Policy() *RolePolicy
- func (s *GrantStore) Revoke(ctx context.Context, role string, perms ...Permission) error
- type GrantStoreOption
- type Permission
- type Policy
- type RolePolicy
- func (rp *RolePolicy) Can(ctx context.Context, permission Permission) bool
- func (rp *RolePolicy) Grant(role string, permissions ...Permission)
- func (rp *RolePolicy) PermissionsOf(role string) []Permission
- func (rp *RolePolicy) Revoke(role string, permissions ...Permission)
- func (rp *RolePolicy) Roles() []string
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 GetRoles ¶ added in v0.3.2
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 WithPolicy ¶
func WithPolicy(ctx context.Context, policy *RolePolicy) context.Context
WithPolicy stores a RolePolicy in the context.
Types ¶
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 persists (role, permission) rows to the database (INSERT ... ON CONFLICT DO NOTHING) and then calls policy.Grant on the live policy, keeping the DB and the in-memory policy in sync. Idempotent: granting an already-held permission is a no-op in both layers.
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.
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 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 (*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) Grant ¶
func (rp *RolePolicy) Grant(role string, permissions ...Permission)
Grant adds permissions to a role.
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) 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.