Documentation
¶
Overview ¶
Package authz provides authorization for the Nucleus framework using Casbin. It wraps Casbin's enforcer with sensible RBAC defaults and provides Chi middleware for route-level access control.
Index ¶
- Constants
- func BootstrapAllowList() []struct{ ... }
- type ActionResolver
- type AuthzOptions
- type CSVMigrationReport
- type Denial
- type DenialHandler
- type Enforcer
- func (e *Enforcer) AddPolicy(sub, obj, act string) error
- func (e *Enforcer) AddRole(user, role string) error
- func (e *Enforcer) AllowAll(role string) error
- func (e *Enforcer) AllowResource(role, resource string, actions ...string) error
- func (e *Enforcer) Can(sub, obj, act string) bool
- func (e *Enforcer) Deny(sub, obj, act string) error
- func (e *Enforcer) GetAllRoles() ([]string, error)
- func (e *Enforcer) GetGroupingPolicy() ([][]string, error)
- func (e *Enforcer) GetPolicy() ([][]string, error)
- func (e *Enforcer) GetRoles(user string) []string
- func (e *Enforcer) Middleware() func(http.Handler) http.Handler
- func (e *Enforcer) MiddlewareWithOptions(opts AuthzOptions) func(http.Handler) http.Handler
- func (e *Enforcer) RemovePolicy(sub, obj, act string) error
- func (e *Enforcer) RemoveRole(user, role string) error
- func (e *Enforcer) RequireRole(roles ...string) func(http.Handler) http.Handler
- func (e *Enforcer) RequireRoleWithOptions(opts AuthzOptions, roles ...string) func(http.Handler) http.Handler
- func (e *Enforcer) SeedBootstrapAllowList() error
- func (e *Enforcer) SeedBootstrapAllowListExcluding(skip ...string) error
- func (e *Enforcer) SetupAdminPolicies(prefix string, modelNames ...string) error
- func (e *Enforcer) SetupModelPolicies(resourcePrefix string, modelNames ...string) error
- type SubjectResolver
Constants ¶
const BootstrapSubject = "anonymous"
BootstrapSubject is the subject used by SeedBootstrapAllowList and by the default-deny middleware in pkg/app when no JWT claims are present on a request. Operators can grant or deny access for unauthenticated callers by writing policies against this subject; framework-owned routes registered by SeedBootstrapAllowList are the canonical example.
Variables ¶
This section is empty.
Functions ¶
func BootstrapAllowList ¶
func BootstrapAllowList() []struct{ Object, Action string }
BootstrapAllowList returns the routes the framework registers under BootstrapSubject before any user policy file loads. These are paths the framework itself owns and that must respond without authorization — Kubernetes probes, Prometheus scrapes, the login flow, and the static assets that the runtime mounts. Operators cannot override this list via config; removing an entry requires a code change.
Returned as ((object, action) tuples); the subject is implicit (BootstrapSubject) and the action is "*" because these routes serve every HTTP method that the underlying handler accepts.
Types ¶
type ActionResolver ¶
ActionResolver derives the policy action (the act in Enforcer.Can) from a request. Supply one via AuthzOptions to express actions the HTTP method alone cannot — e.g. a pure-HTML SSR form POSTs to both update and delete routes, so a resolver maps a POST whose path ends in /delete to "delete" rather than the default "create". When nil, Middleware maps the HTTP method (GET→read, POST→create, PUT/PATCH→update, DELETE→delete).
type AuthzOptions ¶
type AuthzOptions struct {
// OnDeny, when non-nil, is invoked instead of writing the default JSON
// error envelope whenever a request is rejected (whether unauthenticated
// or forbidden). SSR applications set this; JSON APIs leave it nil.
OnDeny DenialHandler
// ResolveSubject, when non-nil, overrides the policy subject Middleware
// checks (default: claims.UserID). RequireRole ignores it — it matches the
// claim's role directly, not through the policy store.
ResolveSubject SubjectResolver
// ResolveAction, when non-nil, overrides the policy action Middleware checks
// (default: the HTTP-method mapping). RequireRole ignores it.
ResolveAction ActionResolver
}
AuthzOptions configures how Middleware/RequireRole answer a denial and, for Middleware, how the policy subject and action are derived. The zero value preserves the default behaviour (a JSON error envelope, subject = claims.UserID, action = HTTP method), so existing callers are unaffected.
type CSVMigrationReport ¶
type CSVMigrationReport struct {
// Path is the input file that was migrated.
Path string
// PolicyLinesUpgraded counts `p, sub, obj, act` rows that received an
// `eft` column. Lines already in 4-column form are not counted.
PolicyLinesUpgraded int
// PolicyLinesAlreadyMigrated counts `p, sub, obj, act, eft` rows that
// were left untouched because they already had the effect column.
PolicyLinesAlreadyMigrated int
// GroupingLinesPreserved counts `g, ...` rows; grouping policies have
// no eft column under the default model and are never rewritten.
GroupingLinesPreserved int
// BlankOrCommentLines counts blank lines and `#`-prefixed comments
// preserved verbatim.
BlankOrCommentLines int
// Changed reports whether the file on disk was rewritten. False means
// every policy line was already in the post-migration shape.
Changed bool
}
CSVMigrationReport summarizes what MigrateCSVPolicyFile did.
func MigrateCSVPolicyFile ¶
func MigrateCSVPolicyFile(path, defaultEffect string) (CSVMigrationReport, error)
MigrateCSVPolicyFile rewrites a Casbin RBAC CSV policy file in place so every `p` row carries an `eft` column compatible with the deny-override model introduced alongside ADR-004.
Behaviour, line by line:
- blank lines and `#` comments are preserved verbatim
- `g, user, role` (and any non-`p` ptype) is preserved verbatim
- `p, sub, obj, act` (exactly four fields) is rewritten to `p, sub, obj, act, <defaultEffect>`
- `p, sub, obj, act, eft` (five or more fields) is preserved verbatim
The function is idempotent: running it twice produces the same file as running it once, and it does not rewrite the file if no upgrade is needed (Changed=false in that case).
defaultEffect must be either `allow` or `deny`. Empty string is treated as `allow` because every existing 3-column policy was conceptually an allow rule under the legacy single-effect Casbin model.
The rewrite is atomic: the new content is written to a sibling temp file in the same directory and renamed over the original.
type Denial ¶
type Denial struct {
// Status is the HTTP status the default renderer would use:
// http.StatusUnauthorized (401) when no identity was present, or
// http.StatusForbidden (403) when the request was authenticated but not
// permitted.
Status int
// Authenticated reports whether the request carried identity (JWT claims).
// When false the visitor is anonymous — a login redirect is usually the
// right response; when true they are signed in but lack the role or
// permission — render a 403.
Authenticated bool
// Reason is the human-readable explanation (e.g. "insufficient role").
Reason string
}
Denial describes an authorization rejection handed to a DenialHandler. It lets an application choose how to answer — redirect an anonymous visitor to a login page, or render a styled 403 for a signed-in user who lacks the required role/permission — instead of the default JSON error envelope. This is what makes the authz middlewares usable from a server-rendered (SSR) UI, not only a JSON API.
type DenialHandler ¶
type DenialHandler func(w http.ResponseWriter, r *http.Request, d Denial)
DenialHandler answers an authorization denial. Supply one via AuthzOptions to render a page or issue a redirect; when nil, the middleware writes the default JSON error envelope. The handler owns the response from this point — it must write a status and body and must not call the next handler. If it returns without writing a status, net/http sends an empty 200 OK, which a client reads as success; either way the protected handler is NOT called. The signature deliberately omits next — do not capture the wrapped handler in a closure and call it from OnDeny, which would grant access.
type Enforcer ¶
type Enforcer struct {
// contains filtered or unexported fields
}
Enforcer wraps a Casbin enforcer with logging and convenience methods.
The underlying *casbin.Enforcer is held in an unexported field rather than embedded so that Casbin's concrete type and its full method set do not leak onto this stable public surface (ADR-015, F-4). Every Casbin capability Nucleus exposes is forwarded by an explicit method below.
func New ¶
New creates an Enforcer with the default RBAC model. If policyPath is provided, policies are loaded from that CSV file. If policyPath is empty, no policies are loaded (add them programmatically).
func NewFromModel ¶
NewFromModel creates an Enforcer with a custom Casbin model string.
func (*Enforcer) AddPolicy ¶
AddPolicy adds an allow policy (subject, object, action). The eft column is auto-stamped to `allow`. To add a deny rule, use Deny.
func (*Enforcer) AllowResource ¶
AllowResource grants a role access to a specific resource pattern with actions.
func (*Enforcer) Deny ¶
Deny adds an explicit deny policy (subject, object, action). Deny rules override every matching allow rule under the model's deny-override effect formula, so this is the primitive for "block this user even though their role normally has access".
func (*Enforcer) GetAllRoles ¶
GetAllRoles returns every role referenced by a grouping policy.
func (*Enforcer) GetGroupingPolicy ¶
GetGroupingPolicy returns all role-assignment (grouping) rules as (user, role) string tuples.
func (*Enforcer) GetPolicy ¶
GetPolicy returns all permission policy rules as (subject, object, action, eft) string tuples. It forwards Casbin's policy store without exposing any Casbin type, so callers (e.g. the admin RBAC inspector) can read the live ruleset.
func (*Enforcer) Middleware ¶
Middleware returns a Chi middleware that checks authorization using the enforcer. It extracts the user identity from the JWT claims in the context (set by auth.JWTManager.Middleware), derives the resource from the URL path, and maps the HTTP method to a CRUD action. Denials are answered with a JSON error envelope; for an SSR-friendly page or redirect, use MiddlewareWithOptions.
func (*Enforcer) MiddlewareWithOptions ¶
MiddlewareWithOptions is Middleware with a configurable denial response and, optionally, custom subject/action derivation. opts.OnDeny replaces the default JSON envelope on every rejection; opts.ResolveSubject and opts.ResolveAction override how the policy subject and action are derived from the request (defaults: claims.UserID and the HTTP-method mapping). The zero AuthzOptions value preserves Middleware's behaviour exactly.
func (*Enforcer) RemovePolicy ¶
RemovePolicy removes a permission policy regardless of its eft. Both allow and deny variants matching (sub, obj, act) are dropped, which matches operator intent — "stop applying this rule" should not require knowing how the rule was originally written.
func (*Enforcer) RemoveRole ¶
RemoveRole removes a role assignment from a user.
func (*Enforcer) RequireRole ¶
RequireRole returns middleware that checks if the authenticated user has one of the specified roles. Denials are answered with a JSON error envelope; for an SSR-friendly page or redirect, use RequireRoleWithOptions.
func (*Enforcer) RequireRoleWithOptions ¶
func (e *Enforcer) RequireRoleWithOptions(opts AuthzOptions, roles ...string) func(http.Handler) http.Handler
RequireRoleWithOptions is RequireRole with a configurable denial response. When opts.OnDeny is set it is invoked on every rejection (instead of the default JSON envelope); otherwise the behaviour is identical to RequireRole.
func (*Enforcer) SeedBootstrapAllowList ¶
SeedBootstrapAllowList programmatically adds the BootstrapAllowList entries to the enforcer under BootstrapSubject. pkg/app calls this during App.New (before mounting the default authz middleware) so the framework's own probe / login routes respond without authorization regardless of whether the operator has loaded a user policy file.
func (*Enforcer) SeedBootstrapAllowListExcluding ¶
SeedBootstrapAllowListExcluding is SeedBootstrapAllowList with an operator-driven subtraction: entries whose Object equals one of skip are not seeded, so those routes fall under the default-deny policy like any user route. pkg/app uses it to honor `metrics_public: false` — the metrics path stays out of the anonymous allow-list and answers only under an explicit policy grant.
func (*Enforcer) SetupAdminPolicies ¶
SetupAdminPolicies configures common admin policies for a set of model names, in the URL shape of the Orbit panel: `<prefix>/api/models/<name>/*` per model, plus `<prefix>/*` read for admins. The "admin" role gets full access to every model and "viewer" read-only. The path layout is the panel's, not this package's (the admin was extracted to Orbit); for any other resource layout use SetupModelPolicies with the full prefix.
func (*Enforcer) SetupModelPolicies ¶
SetupModelPolicies grants "admin" full access and "viewer" read access to `<resourcePrefix>/<name>/*` for each model name, with no assumption about where the models are served.
type SubjectResolver ¶
SubjectResolver derives the policy subject (the sub in Enforcer.Can) from a request and its claims. Supply one via AuthzOptions to check policies keyed by something other than the user id — e.g. an app whose policy table is keyed by role (rather than by user via Casbin grouping rules) returns claims.Role. When nil, Middleware uses claims.UserID.