Per-record access grants — set-based authorization for lists and named policies.
Chirp's flatuser.permissionsgate answers route-level questions; this module
adds resource-level grants stored in anaccess_grantstable and wired through
the existing named-policy seam (access_policy()) and
accessible_to().
Usage::
from chirp.data import Database, Query, migrate
from chirp.security.access_grants import (
access_grants_ddl,
access_policy,
check_access,
create_grant,
register_access_policy,
require_access,
)
# One-time migration — dialect-aware DDL (sqlite | postgresql).
# Copy access_grants_ddl("sqlite") or access_grants_ddl("postgresql") into
# migrations/NNN_access_grants.sql, then:
await migrate(db, "migrations/")
# Or apply directly: await db.execute_script(access_grants_ddl("postgresql"))
app.register_permission("documents.grant.read")
register_access_policy(app, "owns:document", "document", param="document_id", perm="read")
# List — one query, no N+1
docs = await (
Query(Doc, "documents")
.accessible_to(user, "read", resource_type="document")
.fetch(db)
)
# Detail route — AuthSpec(policy="owns:document") or imperative guard
if not await check_access(db, user, "document", doc_id, "write"):
...
security.access_grants
| Name | Type | Default | Description |
|---|---|---|---|
type
|
|
— | |
qualified_name
|
|
— | |
element_type
|
|
— | |
description
|
|
— | |
source_file
|
|
— | |
line_number
|
|
— | |
is_autodoc
|
|
— | |
autodoc_element
|
|
— | |
_autodoc_template
|
|
— | |
_autodoc_url_path
|
|
— | |
_autodoc_page_type
|
|
— | |
title
|
|
— | |
doc_content_hash
|
|
— |
Symbols on this page
Return portableaccess_grantsCREATE TABLE / INDEX SQL for dialect.
dialect is"sqlite" or "postgresql" ("postgres"accepted).
SQLite usesINTEGER PRIMARY KEY…
One row fromaccess_grants.
Raised when a user attempts to grant access beyond their feature permissions.
Return form field errors when user may not grant permission on resource_type.
A user may grantread when they hold grant.reador
{resource_type}.grant.read…
Build a correlatedEXISTSWHERE fragment for set-based grant filtering.
Return whether user holds perm on the given resource via grants.
RaiseHTTPError(403)and emit a security event when access is denied.
Insert a grant after validating sharing escalation bounds.
RaisesSharingEscalationErrorwhen granter lacks the feature
permission to create a grant at permissionlevel (fail…
Build a named-policy callable forAuthSpec(policy=...) / @requires.
Readsrequest.path_params[param]as the resource id and checks grants.
When owner_paramis set, a matching…
Registeraccess_policy(...) under name via app.register_policy.
access_grants_ddl
function
def access_grants_ddl(dialect: str = 'sqlite') -> str
Return portableaccess_grantsCREATE TABLE / INDEX SQL for dialect.
dialect is"sqlite" or "postgresql" ("postgres"accepted).
SQLite usesINTEGER PRIMARY KEY(implicit autoincrement; no
AUTOINCREMENT keyword). PostgreSQL uses SERIAL PRIMARY KEY.
Both forms are compatible withcreate_grant(), which reads the
inserted row viaINSERT ... RETURNING rather than SQLite rowid.
Copy the returned SQL into a numbered migration, or apply it with
execute_script().
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
dialect
|
str
|
'sqlite'
|
AccessGrant
class
One row fromaccess_grants.
SharingEscalationError
class
Raised when a user attempts to grant access beyond their feature permissions.
sharing_escalation_errors
function
access_exists_clause
function
def access_exists_clause(*, table: str, resource_column: str, resource_type: str, perm: str, user_id: str, group_ids: tuple[str, ...], grants_table: str = GRANTS_TABLE) -> tuple[str, tuple[object, ...]]
Build a correlatedEXISTSWHERE fragment for set-based grant filtering.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
table
|
str
|
— | |
resource_column
|
str
|
— | |
resource_type
|
str
|
— | |
perm
|
str
|
— | |
user_id
|
str
|
— | |
group_ids
|
tuple[str, ...]
|
— | |
grants_table
|
str
|
GRANTS_TABLE
|
check_access
function
async
async def check_access(db: Database, user: Any, resource_type: str, resource_id: str, perm: str, *, group_ids: frozenset[str] | None = None, grants_table: str = GRANTS_TABLE) -> bool
Return whether user holds perm on the given resource via grants.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
db
|
Database
|
— | |
user
|
Any
|
— | |
resource_type
|
str
|
— | |
resource_id
|
str
|
— | |
perm
|
str
|
— | |
group_ids
|
frozenset[str] | None
|
None
|
|
grants_table
|
str
|
GRANTS_TABLE
|
require_access
function
async
async def require_access(db: Database, user: Any, resource_type: str, resource_id: str, perm: str, *, request: Any | None = None, group_ids: frozenset[str] | None = None) -> None
RaiseHTTPError(403)and emit a security event when access is denied.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
db
|
Database
|
— | |
user
|
Any
|
— | |
resource_type
|
str
|
— | |
resource_id
|
str
|
— | |
perm
|
str
|
— | |
request
|
Any | None
|
None
|
|
group_ids
|
frozenset[str] | None
|
None
|
create_grant
function
async
async def create_grant(db: Database, *, granter: Any, resource_type: str, resource_id: str, principal_type: PrincipalType, principal_id: str, permission: GrantPermission) -> AccessGrant
Insert a grant after validating sharing escalation bounds.
RaisesSharingEscalationErrorwhen granter lacks the feature
permission to create a grant at permission level (fail loud, not silent strip).
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
db
|
Database
|
— | |
granter
|
Any
|
— | |
resource_type
|
str
|
— | |
resource_id
|
str
|
— | |
principal_type
|
PrincipalType
|
— | |
principal_id
|
str
|
— | |
permission
|
GrantPermission
|
— |
access_policy
function
def access_policy(resource_type: str, *, param: str, perm: str = PERM_READ, owner_param: str | None = None, grants_table: str = GRANTS_TABLE) -> PolicyCallable
Build a named-policy callable forAuthSpec(policy=...) / @requires.
Readsrequest.path_params[param]as the resource id and checks grants.
When owner_param is set, a matching path param equal touser.idallows
access without a grant row (explicit owner bypass at the call site).
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
resource_type
|
str
|
— | |
param
|
str
|
— | |
perm
|
str
|
PERM_READ
|
|
owner_param
|
str | None
|
None
|
|
grants_table
|
str
|
GRANTS_TABLE
|
register_access_policy
function
def register_access_policy(app: Any, name: str, resource_type: str, *, param: str, perm: str = PERM_READ, owner_param: str | None = None) -> None
Registeraccess_policy(...) under name via app.register_policy.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
app
|
Any
|
— | |
name
|
str
|
— | |
resource_type
|
str
|
— | |
param
|
str
|
— | |
perm
|
str
|
PERM_READ
|
|
owner_param
|
str | None
|
None
|
View source · /home/runner/work/chirp/chirp/site/../src/chirp/security/access_grants.py:1