Permissions¶
SpecStar lets you enforce access control at the resource layer.
The main entry point is a permission checker. It receives a permission context for each operation and decides whether to allow, deny, or defer the decision.
Default behavior¶
If you do not configure a custom checker, the default experience is open and convenient for development.
For production systems, you should explicitly define a permission strategy.
Option 1: Allow everything¶
This is the simplest setup and is useful for local experiments.
from specstar import spec
from specstar.permission import AllowAll
spec.configure(permission_checker=AllowAll())
Option 2: Root-only access¶
If you want a single operator account to manage everything, use RootOnly.
from specstar import spec
from specstar.permission import RootOnly
spec.configure(permission_checker=RootOnly("admin@example.com"))
Only the configured root user will be allowed to perform operations.
Option 3: ACL-based permissions¶
For per-user or per-role rules, use ACLPermissionChecker.
import datetime as dt
from specstar import spec
from specstar.permission import (
ACLPermission,
ACLPermissionChecker,
PermissionResult,
Policy,
ResourceAction,
)
checker = ACLPermissionChecker(policy=Policy.strict)
spec.configure(permission_checker=checker)
Once the checker is installed, you can create permission rules through the checker's own resource manager.
with checker.resource_manager.meta_provide("root", dt.datetime.now()):
checker.resource_manager.create(
ACLPermission(
subject="alice",
object="document",
action=ResourceAction.read | ResourceAction.read_list,
effect=PermissionResult.allow,
)
)
This rule allows alice to read and list document resources.
Important ACL fields¶
subject— who the rule applies toobject— the target resource type or*action— the requested action or action groupeffect— allow or denyorder— lower values are evaluated first
Policy modes¶
Common policy choices include:
Policy.strict— deny wins, and missing rules default to denyPolicy.permissive— allow wins, and missing rules default to allow
Option 4: RBAC-based permissions¶
For role-based systems, use RBACPermissionChecker.
import datetime as dt
from specstar import spec
from specstar.permission import (
PermissionResult,
RBACPermissionChecker,
RBACPermissionEntry,
ResourceAction,
RoleMembership,
)
checker = RBACPermissionChecker()
spec.configure(permission_checker=checker)
with checker.resource_manager.meta_provide("root", dt.datetime.now()):
checker.resource_manager.create(
RoleMembership(subject="alice", group="editor")
)
checker.resource_manager.create(
RBACPermissionEntry(
subject="editor",
object="document",
action=ResourceAction.create | ResourceAction.update,
effect=PermissionResult.allow,
)
)
In this setup:
alicebelongs to theeditorrole- the
editorrole is allowed to create and update documents
Using the admin shortcut¶
If you provide an admin user and do not supply a custom checker, SpecStar can enable an RBAC-style setup for you:
Use this when you want a convenient starting point and plan to build out the rules afterward.
Resource-aware write authorization¶
For write actions (update, modify, patch, delete, and the lifecycle
verbs switch, permanently_delete, restore) a checker can read the
current, already-stored resource — its meta, data, and revision info —
to make data- or owner-based decisions. SpecStar loads that snapshot into
context.current_resource before running the before-phase check.
A checker only pays for what it declares. Use required_resource_parts(action)
(or the @requires_resource_parts(...) marker on a CheckFunc) to opt in to the
slices you actually read — an owner check stays a cheap meta read and never
loads the data blob:
from specstar.permission import (
ActionBasedPermissionChecker,
ResourcePart,
any_authenticated,
owner_self,
requires_resource_parts,
)
from specstar.types import ResourceAction
# Built-in: owner_self reads current_resource.meta.created_by.
# Custom: read an embedded field on the stored data.
@requires_resource_parts(ResourcePart.DATA)
def only_if_public(context):
current = context.current_resource
return (
PermissionResult.allow
if current.data.visibility == "public"
else PermissionResult.deny
)
checker = ActionBasedPermissionChecker.from_dict(
{
ResourceAction.read: any_authenticated, # internal reads writes perform
ResourceAction.update: owner_self, # needs meta → loaded for you
ResourceAction.delete: owner_self,
}
)
Notes:
- The snapshot carries only the declared slices; the rest stay
UNSET. Models with no resource-aware checker pay no extra read. - On a write,
context.datais the incoming (new) value whilecontext.current_resource.datais the stored (old) value — handy for "this field is immutable" rules. - If the resource doesn't exist, the write fails with the usual not-found (404) before the permission verdict is reached.
- Reads/lists are filtered by an access-scope predicate, not
this checker. That same predicate is also a precondition for writes: a
request targeting a resource outside the caller's scope 404s (existence
hidden) before this checker runs, so
access_scope(visibility / 404) and the permission checker (authorization / 403) compose.
What actions can be protected¶
Permission checks are tied to ResourceAction values such as:
creategetupdatepatchdeleterestoresearch_resources- grouped actions like
read,write, orowner
Because actions are represented as flags, you can combine them with the bitwise OR operator.
Practical tips¶
- use
AllowAllonly for demos or isolated internal tools - use
RootOnlyfor emergency admin-only systems - use ACL when you want explicit user-level rules
- use RBAC when you want role inheritance and cleaner long-term management
- prefer a default-deny policy in production