rbac

package
v0.1.16 Latest Latest
Warning

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

Go to latest
Published: Aug 30, 2026 License: Apache-2.0 Imports: 10 Imported by: 0

Documentation

Index

Constants

View Source
const PublicRoleName = "$public"

PublicRoleName is the reserved role the schema's `rbac.public` block compiles into (ADR-026, PUBLIC-SURFACE-S1). An anonymous request (no Authorization header, on an app whose schema declares the block) is evaluated AS this role through the one existing evaluator — same deny-by-default, same conditions, same field allowlists, on every surface. The name is not declarable in rbac.roles (schema.PublicRoleName mirrors it; a cross-pin test keeps the two constants identical), so the anonymous surface can only come from the block.

View Source
const TransactionRoute = "transaction"

TransactionRoute is the reserved single segment of the atomic multi-resource transaction endpoint (G4): POST /api/transaction. It is NOT a resource — the handler authorizes EACH operation in the batch against its own resource (so a restricted role may transact over the resources it is allowed). The middleware therefore passes this path through (no single-resource RBAC check here); per-op RBAC is enforced in the handler. A schema resource may not be named "transaction" (reserved at load), so this never shadows a real resource's policy.

Variables

This section is empty.

Functions

func IsIdentityVar added in v0.1.10

func IsIdentityVar(val string) bool

IsIdentityVar reports whether a condition `val` names the caller's identity (the two variables the grammar accepts — any other `$…` is a load error, ENG-20). The single predicate the evaluator and the write rule share.

func RBACMiddleware

func RBACMiddleware(policyJSON []byte) func(http.Handler) http.Handler

RBACMiddleware returns a chi-compatible middleware that enforces the given policy JSON. It reads X-User-Role and X-User-ID from request headers, maps the HTTP method to a CRUD action, and injects the EvalResult into context. Requests to paths outside /api/ are passed through without enforcement.

func RBACMiddlewareWithPublic

func RBACMiddlewareWithPublic(policyJSON []byte, isPublic func(method, path string) bool) func(http.Handler) http.Handler

RBACMiddlewareWithPublic is RBACMiddleware plus an exact-match pass-through for the custom routes explicitly registered as Public (appximo.Route {Public: true}): an anonymous request has no role, so path-based enforcement would deny-by-default every public route. The pass-through injects NO EvalResult — inside the handler, the Ctx's RBAC-aware helpers (Query/Insert/ Update) still evaluate the (empty) role themselves and fail closed; only deliberate anonymous logic (UnsafeTx, CreateUser) proceeds. Everything not matched by isPublic keeps full enforcement.

Types

type Condition

type Condition struct {
	Field string `json:"field"`
	Op    string `json:"op"`  // "eq", "neq", "in", etc.
	Val   string `json:"val"` // may be "$user_id", "$external_client_id", or a literal
}

Condition is a predicate evaluated at request time for row-level filtering.

type EvalContext

type EvalContext struct {
	Role             string
	UserID           string
	ExternalClientID string
}

EvalContext carries the identity of the caller for a single request.

func EvalContextFromRequest

func EvalContextFromRequest(r *http.Request) EvalContext

EvalContextFromRequest resolves the caller identity for a request: the JWT claims injected by JWTMiddleware when present, else the X-User-* headers (so integration tests can run without a full JWT stack). This is the SINGLE identity-resolution used by both the route RBAC middleware AND the relation/subresource read checks (codegen.makeRelationRBAC), so every authorization path scopes by the same principal.

type EvalResult

type EvalResult struct {
	Allowed       bool
	AllowedFields []string        // non-nil means restrict to these fields
	Condition     *WhereCondition // non-nil means append this WHERE clause
}

EvalResult is the outcome of a policy evaluation.

func EvalResultFromCtx

func EvalResultFromCtx(ctx context.Context) *EvalResult

EvalResultFromCtx retrieves the EvalResult injected by RBACMiddleware. Returns nil if the middleware was not applied or the path was not enforced.

type Policy

type Policy struct {
	Roles map[string]RolePolicy `json:"roles"`
}

Policy holds all role definitions for a tenant.

func (*Policy) Allows

func (p *Policy) Allows(role, resource, action string) bool

Allows reports whether role may perform action on resource. Conditions are not evaluated here — use Evaluate for that.

func (*Policy) DenyDetail

func (p *Policy) DenyDetail(role, resource, action string) string

DenyDetail returns the operator-facing explanation of a deny: whether the caller's role is not declared by any schema role at all, or is declared but lacks the grant. SERVER LOG ONLY — deliberately asymmetric (ENG-27):

  • The RESPONSE stays the byte-identical `403 {"error":"forbidden"}` for both cases. Distinguishing them in the body would turn every endpoint into an enumeration oracle over the schema's role namespace (an attacker minting tokens could probe which role names exist).
  • The LOG gains the distinction, because that is where the operator looks and the attacker cannot. Before this, a token carrying a typo'd or forged role produced a deny indistinguishable from a legitimate one anywhere — not the engine log, not the access log, not the trace.

Echoing the role name in the log leaks nothing to the caller (they hold the JWT; its claims are base64, not encrypted). Same family as SEC-5: a defence must not leak through its own error channel.

func (*Policy) Evaluate

func (p *Policy) Evaluate(evalCtx EvalContext, resource, action string) EvalResult

Evaluate determines whether evalCtx.Role may perform action on resource, resolves any dynamic variables in the applicable Condition, and returns the full result. The condition + field allowlist returned are ALWAYS the ones belonging to THIS resource: in the legacy form the role-global ones, in the per-resource form the matched resource's own — so every operation (read/create/update/delete/ aggregate, on REST and GraphQL, which all funnel through this one call) scopes by the correct resource's condition.

func (*Policy) HasPublicSurface added in v0.1.5

func (p *Policy) HasPublicSurface() bool

HasPublicSurface reports whether the policy declares an anonymous surface — the switch that lets the auth middleware admit tokenless /api requests as the public role instead of answering 401.

func (*Policy) RoleCacheable

func (p *Policy) RoleCacheable(role string) bool

RoleCacheable reports whether the response cache may SHARE a role's responses across users. A role is cacheable ONLY if it injects no per-user row condition and no field allowlist anywhere — otherwise caching by role would let one user receive another's rows/fields. Covers BOTH forms: a legacy role with a global condition/allowlist is not cacheable, and a per-resource role is not cacheable if ANY of its resources carries a condition or a field allowlist (fail-safe).

func (*Policy) UnmarshalJSON added in v0.1.5

func (p *Policy) UnmarshalJSON(data []byte) error

UnmarshalJSON folds the schema's `rbac.public` block into the reserved public role, so everything downstream — Evaluate, Allows, RoleCacheable, DenyDetail — sees ONE uniform role map and needs no second code path.

type ResourcePermission

type ResourcePermission struct {
	Actions          []string   `json:"actions"`
	Conditions       *Condition `json:"conditions,omitempty"`
	ConditionActions []string   `json:"condition_actions,omitempty"`
	Fields           []string   `json:"fields,omitempty"`
}

ResourcePermission is one role's grant on ONE resource (G2): the actions allowed, an optional row-level Condition carrying that resource's OWN ownership column, an optional field allowlist, and ConditionActions to scope the condition to a subset of the actions (the "read all, write own" pattern — read unconditional, writes owner-scoped). An empty ConditionActions means the condition applies to ALL granted actions (the safe default — most restrictive).

type RolePolicy

type RolePolicy struct {
	Resources   json.RawMessage `json:"resources,omitempty"`
	Actions     []string        `json:"actions,omitempty"`
	Conditions  *Condition      `json:"conditions,omitempty"`
	FieldsAllow []string        `json:"fields,omitempty"`

	// Permissions is the per-resource form: resource name → its grant. Empty for a
	// legacy role (omitempty keeps the marshalled legacy policy byte-identical).
	Permissions map[string]ResourcePermission `json:"permissions,omitempty"`

	// Routes grants CUSTOM-ROUTE segments (LIBRARY-GAPS-S1) — the virtual resource
	// the middleware derives from a custom endpoint's first /api/ segment. Mirrors
	// schema.RolePolicy.Routes so the schema→rbac.Policy JSON round-trip is lossless.
	// Orthogonal to Resources/Permissions (different namespace: registered endpoints,
	// not tables) and carries NO condition or field allowlist — a virtual segment has
	// no rows. Empty for every pre-S1 role, so the evaluation path is unchanged.
	Routes map[string]RouteGrant `json:"routes,omitempty"`
}

RolePolicy defines what a role can do. Resources is json.RawMessage because it can be the string "*" or a []string.

A role is expressed in ONE of two mutually-exclusive forms (G2):

  • Role-global (legacy): Resources + Actions + (optional) Conditions/FieldsAllow. The single Conditions/FieldsAllow apply to EVERY listed resource. Behaviour is unchanged from before per-resource permissions existed.
  • Per-resource (Permissions): each resource carries its OWN actions, condition and field allowlist — so one role can scope `workspace_id` on one resource and `conversation_id` on another (workspace/participation/owner scoping). When Permissions is non-empty it is the SOLE source of truth: a resource absent from the map is denied (deny-by-default). The two forms never mix (schema validation rejects a role that declares both).

type RouteGrant

type RouteGrant struct {
	Actions []string `json:"actions"`
}

RouteGrant is one role's grant on one custom-route segment: the allowed actions, nothing else. See schema.RouteGrant / ADR-021.

type WhereCondition

type WhereCondition struct {
	Field string
	Op    string
	Value string // dynamic variables already substituted
	// Identity is true when the declared `val` was a principal variable
	// ($user_id / $external_client_id) — i.e. the column carries WHO OWNS the
	// row, not a literal visibility filter such as status = "published". The
	// write path treats an identity-bound column as server-owned on create
	// AND update (codegen.EnforceCreateRBAC / EnforceUpdateRBAC,
	// MOTOR-AUTORIZACION-S1); a literal condition only scopes reads and the
	// WHERE of writes, so a moderator scoped to pending rows may still move
	// one to approved.
	Identity bool
}

WhereCondition is a resolved predicate ready to be appended to SQL.

Jump to

Keyboard shortcuts

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