API Reference¶
Core¶
policy
¶
Policy engine — registration and lookup of authorization policies.
policy(resource_type, action, *, predicate=None, registry=None, query_only=False)
¶
Decorator that registers a policy function for (model, action).
The decorated function receives an actor and returns a
ColumnElement[bool] filter expression.
When predicate is provided, the predicate's __call__ is
registered as the policy function instead of the decorated function body.
The decorated function is still used for its name and docstring.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
resource_type
|
type
|
The SQLAlchemy model class. |
required |
action
|
str
|
The action string (e.g., "read", "update"). |
required |
predicate
|
Predicate | None
|
Optional composable predicate to use as the policy function. |
None
|
registry
|
PolicyRegistry | None
|
Optional custom registry. Defaults to the global registry. |
None
|
query_only
|
bool
|
If |
False
|
Returns:
| Type | Description |
|---|---|
Callable[[F], F]
|
A decorator that registers the function and returns it unchanged. |
Example::
@policy(Post, "read")
def post_read(actor: User) -> ColumnElement[bool]:
return or_(
Post.is_published == True,
Post.author_id == actor.id,
)
@policy(Post, "update", predicate=is_author)
def post_update(actor: User) -> ColumnElement[bool]:
...
@policy(Post, "read", query_only=True)
def complex_read(actor: User) -> ColumnElement[bool]:
return func.lower(Post.category) == "public"
Source code in src/sqla_authz/policy/_decorator.py
authorize_query(stmt, *, actor, action, registry=None)
¶
Apply authorization filters to a SQLAlchemy SELECT statement.
Looks up registered policies for the statement's entities and the given action, evaluates them with the actor, and applies the resulting WHERE clauses.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
stmt
|
Select[Any]
|
A SQLAlchemy 2.0 Select statement. |
required |
actor
|
ActorLike
|
The user/principal. Must satisfy ActorLike protocol. |
required |
action
|
str
|
The action being performed (e.g., "read", "update"). |
required |
registry
|
PolicyRegistry | None
|
Optional custom registry. Defaults to the global registry. |
None
|
Returns:
| Type | Description |
|---|---|
Select[Any]
|
A new Select with authorization filters applied. |
Example::
stmt = select(Post).where(Post.category == "tech")
stmt = authorize_query(stmt, actor=current_user, action="read")
# SQL: SELECT ... WHERE category = 'tech'
# AND (is_published OR author_id = :id)
Source code in src/sqla_authz/compiler/_query.py
can(actor, action, resource, *, registry=None, session=None)
¶
Check if actor can perform action on a specific resource instance.
Returns True if the policy filter matches the resource, False
otherwise. The real application database is never touched — the
filter expression is evaluated in-memory by walking the SQLAlchemy
ColumnElement AST.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
actor
|
ActorLike
|
The user/principal performing the action. |
required |
action
|
str
|
The action string (e.g., |
required |
resource
|
DeclarativeBase
|
A mapped SQLAlchemy model instance. |
required |
registry
|
PolicyRegistry | None
|
Optional custom registry. Defaults to the global registry. |
None
|
session
|
Session | None
|
Optional session (reserved for future use). |
None
|
Returns:
| Type | Description |
|---|---|
bool
|
|
Raises:
| Type | Description |
|---|---|
QueryOnlyPolicyError
|
If any matching policy is marked
|
Example::
post = session.get(Post, 1)
if can(current_user, "read", post):
return post
Source code in src/sqla_authz/_checks.py
can_create(actor, resource, *, registry=None, session=None)
¶
Check whether actor can create resource in its current state.
This is a convenience wrapper around can(..., action="create") for
pending or transient ORM instances. Populate the object first, then call
can_create() before flushing or committing.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
actor
|
ActorLike
|
The user/principal attempting the create. |
required |
resource
|
DeclarativeBase
|
The pending mapped SQLAlchemy model instance. |
required |
registry
|
PolicyRegistry | None
|
Optional custom registry. Defaults to the global registry. |
None
|
session
|
Session | None
|
Optional session (reserved for future use). |
None
|
Returns:
| Type | Description |
|---|---|
bool
|
|
Source code in src/sqla_authz/_checks.py
authorize(actor, action, resource, *, registry=None, message=None, session=None)
¶
Assert that actor is authorized to perform action on resource.
Raises :class:~sqla_authz.exceptions.AuthorizationDenied when access
is denied. Returns None on success.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
actor
|
ActorLike
|
The user/principal performing the action. |
required |
action
|
str
|
The action string (e.g., |
required |
resource
|
DeclarativeBase
|
A mapped SQLAlchemy model instance. |
required |
registry
|
PolicyRegistry | None
|
Optional custom registry. Defaults to the global registry. |
None
|
message
|
str | None
|
Optional custom error message for the exception. |
None
|
session
|
Session | None
|
Optional session (reserved for future use). |
None
|
Raises:
| Type | Description |
|---|---|
AuthorizationDenied
|
If the actor is not authorized. |
QueryOnlyPolicyError
|
If any matching policy is marked
|
Example::
authorize(current_user, "update", post) # raises if denied
Source code in src/sqla_authz/_checks.py
authorize_create(actor, resource, *, registry=None, message=None, session=None)
¶
Assert that actor can create resource in its current state.
This is a convenience wrapper around authorize(..., action="create")
for pending or transient ORM instances.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
actor
|
ActorLike
|
The user/principal attempting the create. |
required |
resource
|
DeclarativeBase
|
The pending mapped SQLAlchemy model instance. |
required |
registry
|
PolicyRegistry | None
|
Optional custom registry. Defaults to the global registry. |
None
|
message
|
str | None
|
Optional custom error message for the exception. |
None
|
session
|
Session | None
|
Optional session (reserved for future use). |
None
|
Source code in src/sqla_authz/_checks.py
configure(*, on_missing_policy=None, default_action=None, log_policy_decisions=None, on_unloaded_relationship=None, strict_mode=None, on_unprotected_get=None, on_text_query=None, on_skip_authz=None, audit_bypasses=None, intercept_creates=None, intercept_updates=None, intercept_deletes=None, on_write_denied=None, on_unknown_action=None)
¶
Update the global configuration by merging overrides.
Only non-None values are applied. Returns the new global config.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
on_missing_policy
|
OnMissingPolicy | None
|
Set to |
None
|
default_action
|
str | None
|
Set the default action string. |
None
|
log_policy_decisions
|
bool | None
|
Enable/disable audit logging of policy decisions. |
None
|
on_unloaded_relationship
|
OnUnloadedRelationship | None
|
Set to |
None
|
strict_mode
|
bool | None
|
Enable strict mode (applies convenience defaults). |
None
|
on_unprotected_get
|
OnBypassAction | None
|
Set to |
None
|
on_text_query
|
OnBypassAction | None
|
Set to |
None
|
on_skip_authz
|
OnSkipAuthz | None
|
Set to |
None
|
audit_bypasses
|
bool | None
|
Enable/disable bypass audit logging. |
None
|
intercept_creates
|
bool | None
|
Enable interception of ORM create/insert flushes. |
None
|
intercept_updates
|
bool | None
|
Enable interception of UPDATE statements. |
None
|
intercept_deletes
|
bool | None
|
Enable interception of DELETE statements. |
None
|
on_write_denied
|
OnWriteDenied | None
|
Set to |
None
|
on_unknown_action
|
OnUnknownAction | None
|
Set to |
None
|
Returns:
| Type | Description |
|---|---|
AuthzConfig
|
The updated global |
Example::
configure(on_missing_policy="raise")
# Now missing policies raise NoPolicyError instead of denying
Source code in src/sqla_authz/config/_config.py
Actions¶
actions
¶
Action constants and factory for sqla-authz.
Provides well-known action constants for use with @policy,
authorize_query, can, and other APIs. Using constants
instead of bare strings gives IDE autocomplete and prevents typos.
Example::
from sqla_authz.actions import READ, UPDATE, action
PUBLISH = action("publish")
@policy(Post, READ)
def post_read(actor: User) -> ColumnElement[bool]:
return Post.is_published == True
@policy(Post, PUBLISH)
def post_publish(actor: User) -> ColumnElement[bool]:
return Post.author_id == actor.id
READ = 'read'
module-attribute
¶
Built-in action constant for read operations.
UPDATE = 'update'
module-attribute
¶
Built-in action constant for update operations.
DELETE = 'delete'
module-attribute
¶
Built-in action constant for delete operations.
CREATE = 'create'
module-attribute
¶
Built-in action constant for create operations.
action(name)
¶
Create a validated action name for use in policies and queries.
Validates that the name follows conventions: non-empty, lowercase,
alphabetic (underscores allowed). This catches common mistakes like
action("Read Posts") or action("") at definition time.
Custom actions created with this factory work identically to the built-in constants — they're plain strings.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The action name. Must be lowercase alphabetic with
optional underscores (e.g., |
required |
Returns:
| Type | Description |
|---|---|
str
|
The validated action name string. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the name doesn't follow naming conventions. |
Example::
APPROVE = action("approve")
SOFT_DELETE = action("soft_delete")
PUBLISH = action("publish")
@policy(Article, PUBLISH)
def article_publish(actor: User) -> ColumnElement[bool]:
return Article.author_id == actor.id
Source code in src/sqla_authz/actions.py
Types¶
ActorLike
¶
Bases: Protocol
Structural type for authorization actors.
Any object with an id attribute satisfies this protocol.
Works with SQLAlchemy models, dataclasses, Pydantic models,
named tuples — no inheritance required.
Example::
@dataclass
class User:
id: int
name: str
user = User(id=1, name="Alice")
assert isinstance(user, ActorLike)
PolicyRegistry()
¶
Registry that maps (model, action) pairs to policy functions.
All public methods are thread-safe via an internal lock.
Example::
registry = PolicyRegistry()
registry.register(Post, "read", my_policy_fn, name="p", description="")
policies = registry.lookup(Post, "read")
Source code in src/sqla_authz/policy/_registry.py
register(resource_type, action, fn, *, name, description, validate_signature=True, query_only=False)
¶
Register a policy function for a (model, action) pair.
Multiple policies can be registered for the same key; they will be OR'd together at evaluation time.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
resource_type
|
type
|
The SQLAlchemy model class. |
required |
action
|
str
|
The action string (e.g., |
required |
fn
|
Callable[..., ColumnElement[bool]]
|
A callable that takes an actor and returns a
|
required |
name
|
str
|
Human-readable name for the policy (used in logging). |
required |
description
|
str
|
Description of the policy (typically the docstring). |
required |
validate_signature
|
bool
|
If |
True
|
query_only
|
bool
|
If |
False
|
Returns:
| Type | Description |
|---|---|
None
|
None |
Example::
registry = PolicyRegistry()
registry.register(
Post, "read",
lambda actor: Post.is_published == True,
name="published_only",
description="Allow reading published posts",
)
Source code in src/sqla_authz/policy/_registry.py
lookup(resource_type, action)
¶
Look up all policies for a (model, action) pair.
Returns a copy of the internal list so callers cannot mutate the registry state.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
resource_type
|
type
|
The SQLAlchemy model class to look up. |
required |
action
|
str
|
The action string to look up. |
required |
Returns:
| Type | Description |
|---|---|
list[PolicyRegistration]
|
A list of |
list[PolicyRegistration]
|
no policies are registered for the given key. |
Example::
policies = registry.lookup(Post, "read")
for p in policies:
print(p.name)
Source code in src/sqla_authz/policy/_registry.py
has_policy(resource_type, action)
¶
Check whether at least one policy exists for (model, action).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
resource_type
|
type
|
The SQLAlchemy model class. |
required |
action
|
str
|
The action string. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
|
Example::
if not registry.has_policy(Post, "delete"):
print("No delete policy for Post")
Source code in src/sqla_authz/policy/_registry.py
registered_entities(action)
¶
Return all entity types that have policies registered for action.
Useful for applying loader criteria to relationship loads that are not part of the main query.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
action
|
str
|
The action string to filter by. |
required |
Returns:
| Type | Description |
|---|---|
set[type]
|
A set of model classes with registered policies for the action. |
Example::
entities = registry.registered_entities("read")
# e.g., {Post, User}
Source code in src/sqla_authz/policy/_registry.py
register_scope(scope_reg)
¶
Register a cross-cutting scope filter.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
scope_reg
|
ScopeRegistration
|
A |
required |
Example::
from sqla_authz.policy._scope import ScopeRegistration
reg = ScopeRegistration(
applies_to=(Post, Comment),
fn=my_scope_fn,
name="tenant",
description="Tenant isolation",
actions=None,
)
registry.register_scope(reg)
Source code in src/sqla_authz/policy/_registry.py
lookup_scopes(resource_type, action=None)
¶
Look up all scopes that apply to a model and optional action.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
resource_type
|
type
|
The SQLAlchemy model class. |
required |
action
|
str | None
|
Optional action string to filter by. Scopes with
no |
None
|
Returns:
| Type | Description |
|---|---|
list[ScopeRegistration]
|
A list of matching |
Example::
scopes = registry.lookup_scopes(Post, "read")
Source code in src/sqla_authz/policy/_registry.py
has_scopes(resource_type)
¶
Check whether at least one scope exists for a model.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
resource_type
|
type
|
The SQLAlchemy model class. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
|
Example::
if registry.has_scopes(Post):
print("Post has scope coverage")
Source code in src/sqla_authz/policy/_registry.py
known_actions()
¶
Return all action strings that have registered policies.
Thread-safe. Useful for introspection and validation.
Returns:
| Type | Description |
|---|---|
set[str]
|
A set of action strings. |
Example::
actions = registry.known_actions()
# e.g., {"read", "update", "delete"}
Source code in src/sqla_authz/policy/_registry.py
known_actions_for(resource_type)
¶
Return all action strings registered for a specific model.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
resource_type
|
type
|
The SQLAlchemy model class. |
required |
Returns:
| Type | Description |
|---|---|
set[str]
|
A set of action strings registered for that model. |
Example::
actions = registry.known_actions_for(Post)
# e.g., {"read", "update"}
Source code in src/sqla_authz/policy/_registry.py
clear()
¶
Remove all registered policies and scopes.
Primarily useful in test teardown to reset the registry state between tests.
Returns:
| Type | Description |
|---|---|
None
|
None |
Example::
registry.clear()
assert registry.lookup(Post, "read") == []
Source code in src/sqla_authz/policy/_registry.py
Configuration¶
AuthzConfig(on_missing_policy='deny', default_action='read', log_policy_decisions=False, on_unloaded_relationship='deny', strict_mode=False, on_unprotected_get='ignore', on_text_query='ignore', on_skip_authz='ignore', audit_bypasses=False, intercept_creates=False, intercept_updates=False, intercept_deletes=False, on_write_denied='raise', on_unknown_action='ignore')
dataclass
¶
Layered configuration with merge semantics (global -> session -> query).
Attributes:
| Name | Type | Description |
|---|---|---|
on_missing_policy |
OnMissingPolicy
|
Behavior when no policy is registered.
|
default_action |
str
|
The default action string used when none is specified. |
Example::
config = AuthzConfig(on_missing_policy="raise")
merged = config.merge(default_action="update")
merge(*, on_missing_policy=None, default_action=None, log_policy_decisions=None, on_unloaded_relationship=None, strict_mode=None, on_unprotected_get=None, on_text_query=None, on_skip_authz=None, audit_bypasses=None, intercept_creates=None, intercept_updates=None, intercept_deletes=None, on_write_denied=None, on_unknown_action=None)
¶
Return a new config with non-None overrides applied.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
on_missing_policy
|
OnMissingPolicy | None
|
Override for on_missing_policy (ignored if None). |
None
|
default_action
|
str | None
|
Override for default_action (ignored if None). |
None
|
log_policy_decisions
|
bool | None
|
Override for log_policy_decisions (ignored if None). |
None
|
on_unloaded_relationship
|
OnUnloadedRelationship | None
|
Override for on_unloaded_relationship (ignored if None). |
None
|
strict_mode
|
bool | None
|
Override for strict_mode (ignored if None). |
None
|
on_unprotected_get
|
OnBypassAction | None
|
Override for on_unprotected_get (ignored if None). |
None
|
on_text_query
|
OnBypassAction | None
|
Override for on_text_query (ignored if None). |
None
|
on_skip_authz
|
OnSkipAuthz | None
|
Override for on_skip_authz (ignored if None). |
None
|
audit_bypasses
|
bool | None
|
Override for audit_bypasses (ignored if None). |
None
|
intercept_creates
|
bool | None
|
Override for intercept_creates (ignored if None). |
None
|
intercept_updates
|
bool | None
|
Override for intercept_updates (ignored if None). |
None
|
intercept_deletes
|
bool | None
|
Override for intercept_deletes (ignored if None). |
None
|
on_write_denied
|
OnWriteDenied | None
|
Override for on_write_denied (ignored if None). |
None
|
on_unknown_action
|
OnUnknownAction | None
|
Override for on_unknown_action (ignored if None). |
None
|
Returns:
| Type | Description |
|---|---|
AuthzConfig
|
A new |
Example::
base = AuthzConfig()
session_cfg = base.merge(on_missing_policy="raise")
query_cfg = session_cfg.merge(default_action="update")
Source code in src/sqla_authz/config/_config.py
118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 | |
Predicates¶
Predicate(fn, *, name='')
¶
A composable authorization predicate.
Wraps a callable that takes an actor and returns a
ColumnElement[bool]. Supports & (AND), | (OR),
and ~ (NOT) composition.
Example::
is_published = Predicate(lambda actor: Post.is_published == True)
is_author = Predicate(lambda actor: Post.author_id == actor.id)
combined = is_published | is_author
expr = combined(current_user) # ColumnElement[bool]
Source code in src/sqla_authz/policy/_predicate.py
name
property
¶
The human-readable name of this predicate.
predicate(fn)
¶
Decorator/factory that creates a Predicate from a callable.
Example::
@predicate
def is_published(actor: User) -> ColumnElement[bool]:
return Post.is_published == True
# Or as a factory:
is_author = predicate(lambda actor: Post.author_id == actor.id)
Source code in src/sqla_authz/policy/_predicate.py
always_allow = Predicate(_always_allow, name='always_allow')
module-attribute
¶
always_deny = Predicate(_always_deny, name='always_deny')
module-attribute
¶
Scopes¶
scope(applies_to, *, actions=None, registry=None)
¶
Decorator that registers a cross-cutting scope filter.
Scopes are AND'd with OR'd policy results for matching models. They enforce invariants like tenant isolation that must not be accidentally bypassed by adding a new policy.
The decorated function receives (actor, Model) where Model
is the class currently being filtered, and returns a
ColumnElement[bool].
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
applies_to
|
Sequence[type]
|
List of SQLAlchemy model classes this scope covers. |
required |
actions
|
Sequence[str] | None
|
Optional list of actions to restrict this scope to.
If |
None
|
registry
|
PolicyRegistry | None
|
Optional custom registry. Defaults to the global registry. |
None
|
Returns:
| Type | Description |
|---|---|
Callable[[F], F]
|
A decorator that registers the function and returns it unchanged. |
Example::
@scope(applies_to=[Post, Comment, Document])
def tenant_scope(actor: User, Model: type) -> ColumnElement[bool]:
return Model.org_id == actor.org_id
@scope(applies_to=[Post, Comment], actions=["read"])
def soft_delete(actor: User, Model: type) -> ColumnElement[bool]:
return Model.deleted_at.is_(None)
Source code in src/sqla_authz/policy/_scope.py
ScopeRegistration(applies_to, fn, name, description, actions)
dataclass
¶
A registered scope function with its metadata.
Attributes:
| Name | Type | Description |
|---|---|---|
applies_to |
tuple[type, ...]
|
The model classes this scope applies to. |
fn |
Callable[..., ColumnElement[bool]]
|
The scope function |
name |
str
|
The scope function name (for debugging/logging). |
description |
str
|
Human-readable description (from docstring). |
actions |
tuple[str, ...] | None
|
Actions this scope is restricted to, or |
verify_scopes(base, *, field=None, when=None, registry=None)
¶
Verify that all matching models have scope coverage.
Scans all concrete subclasses of base and checks that each matching model has at least one registered scope.
Exactly one of field or when must be provided.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
base
|
type[DeclarativeBase]
|
The SQLAlchemy |
required |
field
|
str | None
|
Column name to match on. Models with this column are expected to have a scope. |
None
|
when
|
Callable[[type], bool] | None
|
Predicate |
None
|
registry
|
PolicyRegistry | None
|
Optional custom registry. Defaults to the global registry. |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If neither or both of field and when are provided. |
UnscopedModelError
|
If any matching models lack scope coverage. |
Example::
from sqla_authz import verify_scopes
verify_scopes(Base, field="org_id")
Source code in src/sqla_authz/_verify.py
Session¶
authorized_sessionmaker(bind, *, actor_provider, action='read', registry=None, config=None, **kwargs)
¶
Create a sessionmaker with automatic authorization interception.
Convenience factory that creates a sessionmaker and installs
authorization interceptors in one step.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
bind
|
Any
|
The engine or connection to bind to. |
required |
actor_provider
|
Callable[[], ActorLike]
|
A callable returning the current actor. |
required |
action
|
str
|
Default action string. |
'read'
|
registry
|
PolicyRegistry | None
|
Policy registry. Defaults to the global registry. |
None
|
config
|
AuthzConfig | None
|
Configuration. Defaults to the global config. |
None
|
**kwargs
|
Any
|
Additional keyword arguments passed to |
{}
|
Returns:
| Type | Description |
|---|---|
sessionmaker[Session]
|
A configured |
Example::
AuthorizedSession = authorized_sessionmaker(
bind=engine,
actor_provider=get_current_user,
action="read",
)
with AuthorizedSession() as session:
posts = session.execute(select(Post)).scalars().all()
# Only authorized posts are returned
Source code in src/sqla_authz/session/_interceptor.py
install_interceptor(session_factory, *, actor_provider, action='read', registry=None, config=None)
¶
Install a do_orm_execute event listener on a session factory.
The listener intercepts SELECT queries and, when enabled via config, CREATE/UPDATE/DELETE writes as well.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
session_factory
|
sessionmaker[Session]
|
A SQLAlchemy |
required |
actor_provider
|
Callable[[], ActorLike]
|
A callable returning the current actor. Called once per query execution. |
required |
action
|
str
|
Default action string. Can be overridden per-query
via |
'read'
|
registry
|
PolicyRegistry | None
|
Policy registry to use. Defaults to the global registry. |
None
|
config
|
AuthzConfig | None
|
Configuration to use. Defaults to the global config. |
None
|
Example::
factory = sessionmaker(bind=engine)
install_interceptor(
factory,
actor_provider=get_current_user,
action="read",
)
with factory() as session:
# All SELECT queries are automatically authorized
posts = session.execute(select(Post)).scalars().all()
Source code in src/sqla_authz/session/_interceptor.py
AuthorizationContext(actor, action, config)
dataclass
¶
Carries actor, action, and config through the authorization pipeline.
Attributes:
| Name | Type | Description |
|---|---|---|
actor |
ActorLike
|
The current user/principal satisfying |
action |
str
|
The action being performed (e.g., |
config |
AuthzConfig
|
The resolved configuration for this authorization check. |
Example::
ctx = AuthorizationContext(
actor=current_user,
action="read",
config=AuthzConfig(),
)
Exceptions¶
exceptions
¶
Exception hierarchy for sqla-authz.
AuthzError
¶
Bases: Exception
Base exception for all sqla-authz errors.
AuthorizationDenied(*, actor, action, resource_type, message=None)
¶
Bases: AuthzError
Actor is not authorized to perform the requested action.
Attributes:
| Name | Type | Description |
|---|---|---|
actor |
The actor that was denied. |
|
action |
The action that was attempted. |
|
resource_type |
The type of resource involved. |
Example::
try:
authorize(user, "delete", post)
except AuthorizationDenied as exc:
print(f"{exc.actor} cannot {exc.action} {exc.resource_type}")
Source code in src/sqla_authz/exceptions.py
NoPolicyError(*, resource_type, action)
¶
Bases: AuthzError
No policy registered for (resource_type, action).
Raised when configured to error on missing policies instead of the default deny-by-default (WHERE FALSE) behavior.
Attributes:
| Name | Type | Description |
|---|---|---|
resource_type |
The resource type with no policy. |
|
action |
The action with no policy. |
Example::
configure(on_missing_policy="raise")
# Now missing policies raise instead of silently denying
Source code in src/sqla_authz/exceptions.py
PolicyCompilationError
¶
Bases: AuthzError
Policy returned an invalid expression.
Raised when a policy function returns something other than
a SQLAlchemy ColumnElement[bool].
UnknownActionError(*, action, known_actions, suggestion=None)
¶
Bases: AuthzError
Action string has no registered policies for any model.
Raised when on_unknown_action="raise" and the action string
doesn't match any registered policy. This typically indicates a
typo in the action name.
Attributes:
| Name | Type | Description |
|---|---|---|
action |
The unrecognized action string. |
|
known_actions |
List of valid action strings. |
|
suggestion |
Closest matching action, if any. |
Example::
configure(on_unknown_action="raise")
# authorize_query(..., action="raed") now raises:
# UnknownActionError: Action 'raed' has no registered policies.
# Did you mean 'read'?
# Known actions: ['create', 'delete', 'read', 'update']
Source code in src/sqla_authz/exceptions.py
UnscopedModelError(*, models, field=None)
¶
Bases: AuthzError
One or more models lack required scope coverage.
Raised by verify_scopes() when models matching the check
criteria have no registered scopes.
Attributes:
| Name | Type | Description |
|---|---|---|
models |
The model classes that lack scope coverage. |
|
field |
The field name used for matching (if any). |
Source code in src/sqla_authz/exceptions.py
WriteDeniedError(*, actor, action, resource_type, message=None)
¶
Bases: AuthzError
Write operation denied by authorization policy.
Raised when a write operation targets data that the actor is not authorized to create, update, or delete.
Attributes:
| Name | Type | Description |
|---|---|---|
actor |
The actor that was denied. |
|
action |
The action that was attempted. |
|
resource_type |
The type of resource involved. |
Source code in src/sqla_authz/exceptions.py
FastAPI Integration¶
AuthzDep(model, action, *, id_param=None, pk_column='id', registry=None)
¶
FastAPI dependency for authorized queries.
Returns a Depends() instance that resolves authorized model
instances by applying registered policies. When id_param is
None, returns a list of all authorized instances (collection
endpoint). When id_param is set, fetches a single instance by
primary key from the named path parameter, returning 404 if not
found or not authorized.
Use directly as a default parameter value in route signatures.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
type
|
The SQLAlchemy model class to query. |
required |
action
|
str
|
The authorization action (e.g., |
required |
id_param
|
str | None
|
Path parameter name for single-item lookups. |
None
|
pk_column
|
str
|
Model attribute name for the primary key column.
Defaults to |
'id'
|
registry
|
PolicyRegistry | None
|
Optional per-dependency registry override. |
None
|
Returns:
| Type | Description |
|---|---|
Any
|
A FastAPI |
Example::
@app.get("/posts")
async def list_posts(
posts: list[Post] = AuthzDep(Post, "read"),
) -> list[dict]:
return [{"id": p.id, "title": p.title} for p in posts]
@app.get("/posts/{post_id}")
async def get_post(
post: Post = AuthzDep(Post, "read", id_param="post_id"),
) -> dict:
return {"id": post.id, "title": post.title}
# Model with non-'id' PK:
@app.get("/documents/{doc_uuid}")
async def get_document(
doc: Document = AuthzDep(
Document, "read", id_param="doc_uuid", pk_column="uuid"
),
) -> dict:
return {"uuid": doc.uuid}
Source code in src/sqla_authz/integrations/fastapi/_dependencies.py
get_actor(request)
¶
Sentinel dependency — override via app.dependency_overrides[get_actor].
Raises NotImplementedError when called directly. Override this
dependency in your FastAPI app to provide the current actor.
Example::
from sqla_authz.integrations.fastapi import get_actor
app.dependency_overrides[get_actor] = my_get_current_user
Source code in src/sqla_authz/integrations/fastapi/_dependencies.py
get_session(request)
¶
Sentinel dependency — override via app.dependency_overrides[get_session].
Raises NotImplementedError when called directly. Override this
dependency in your FastAPI app to provide the current SQLAlchemy session.
Example::
from sqla_authz.integrations.fastapi import get_session
app.dependency_overrides[get_session] = my_get_db_session
Source code in src/sqla_authz/integrations/fastapi/_dependencies.py
install_error_handlers(app)
¶
Install exception handlers for sqla-authz errors on a FastAPI app.
Converts authorization exceptions into proper HTTP responses:
AuthorizationDenied-> 403 ForbiddenNoPolicyError-> 500 Internal Server Error
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
app
|
FastAPI
|
The FastAPI application instance. |
required |
Example::
from fastapi import FastAPI
from sqla_authz.integrations.fastapi import install_error_handlers
app = FastAPI()
install_error_handlers(app)
Source code in src/sqla_authz/integrations/fastapi/_errors.py
Testing¶
testing
¶
sqla-authz testing utilities — MockActor, assertions, and fixtures.
Provides test helpers for verifying authorization policies:
- MockActor / factories: Lightweight actors for tests.
- Assertion helpers:
assert_authorized,assert_denied,assert_query_contains. - Fixtures:
authz_registry,authz_config,authz_context.
Example::
from sqla_authz.testing import MockActor, assert_authorized
from sqlalchemy import select
def test_admin_reads_all(session, sample_data):
assert_authorized(session, select(Post), MockActor(id=1, role="admin"), "read")
MockActor(id, role='viewer', org_id=None)
dataclass
¶
Test actor that satisfies the ActorLike protocol.
A lightweight, immutable dataclass for use in tests. Provides
the id property required by ActorLike, plus optional
role and org_id attributes commonly used in policies.
Example::
actor = MockActor(id=1, role="admin", org_id=5)
assert isinstance(actor, ActorLike)
make_admin(id=1)
¶
Create an admin MockActor.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
id
|
int | str
|
The actor's identifier. Defaults to |
1
|
Returns:
| Type | Description |
|---|---|
MockActor
|
A |
Example::
admin = make_admin()
assert admin.role == "admin"
Source code in src/sqla_authz/testing/_actors.py
make_user(id=1, role='viewer', org_id=None)
¶
Create a regular user MockActor.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
id
|
int | str
|
The actor's identifier. Defaults to |
1
|
role
|
str
|
The actor's role. Defaults to |
'viewer'
|
org_id
|
int | None
|
Optional organization ID. |
None
|
Returns:
| Type | Description |
|---|---|
MockActor
|
A |
Example::
user = make_user(id=5, role="editor", org_id=3)
assert user.role == "editor"
Source code in src/sqla_authz/testing/_actors.py
make_anonymous()
¶
Create an anonymous MockActor with id=0.
Returns:
| Type | Description |
|---|---|
MockActor
|
A |
Example::
anon = make_anonymous()
assert anon.id == 0
assert anon.role == "anonymous"
Source code in src/sqla_authz/testing/_actors.py
assert_authorized(session, stmt, actor, action, *, expected_count=None, registry=None)
¶
Assert that a query returns results after authorization.
Applies authorize_query and executes the statement. Fails with
AssertionError if zero rows are returned. Optionally checks
that the exact number of rows matches expected_count.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
session
|
Session
|
A SQLAlchemy |
required |
stmt
|
Select[Any]
|
A |
required |
actor
|
ActorLike
|
The actor to authorize as. |
required |
action
|
str
|
The action string (e.g., |
required |
expected_count
|
int | None
|
If given, assert exactly this many rows. |
None
|
registry
|
PolicyRegistry | None
|
Optional custom registry. |
None
|
Example::
assert_authorized(session, select(Post), admin, "read", expected_count=3)
Source code in src/sqla_authz/testing/_assertions.py
assert_denied(session, stmt, actor, action, *, registry=None)
¶
Assert that a query returns zero results after authorization.
The inverse of assert_authorized — verifies deny-by-default
or explicit denial.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
session
|
Session
|
A SQLAlchemy |
required |
stmt
|
Select[Any]
|
A |
required |
actor
|
ActorLike
|
The actor to authorize as. |
required |
action
|
str
|
The action string. |
required |
registry
|
PolicyRegistry | None
|
Optional custom registry. |
None
|
Example::
assert_denied(session, select(Post), anonymous_user, "delete")
Source code in src/sqla_authz/testing/_assertions.py
assert_query_contains(stmt, actor, action, *, text, registry=None)
¶
Assert that compiled SQL of an authorized query contains the given text.
Useful for structural tests that verify filter expressions are applied without requiring a database connection.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
stmt
|
Select[Any]
|
A |
required |
actor
|
ActorLike
|
The actor to authorize as. |
required |
action
|
str
|
The action string. |
required |
text
|
str
|
The text to search for in the compiled SQL. |
required |
registry
|
PolicyRegistry | None
|
Optional custom registry. |
None
|
Example::
assert_query_contains(
select(Post), admin, "read",
text="is_published", registry=registry,
)
Source code in src/sqla_authz/testing/_assertions.py
authz_registry()
¶
Provide a fresh, isolated PolicyRegistry for each test.
The registry is created empty and is not shared with the global default registry.
Example::
def test_my_policy(authz_registry):
authz_registry.register(Post, "read", my_fn, name="p", description="")
assert authz_registry.has_policy(Post, "read")
Source code in src/sqla_authz/testing/_fixtures.py
authz_config()
¶
Provide a default authorization config for testing.
Returns a simple dict with default settings. Will be updated to
return an AuthzConfig instance when the config module is ready.
Example::
def test_with_config(authz_config):
assert authz_config["on_missing_policy"] == "deny"
Source code in src/sqla_authz/testing/_fixtures.py
authz_context()
¶
Provide a default AuthorizationContext for tests.