Admin-machine surface and auto-admin for Intent API products.
Project description
Intent API Admin
Console-issued admin tokens and zero-boilerplate admin surfaces for any Intent API product.
intent-api-admin plugs into intent-api to add a dedicated admin-machine surface, Ed25519/JWKS-verified admin tokens, an automatic CRUD admin generator from your SQLAlchemy models, role-aware role gates, audit correlation, and a schema document the console can render directly.
Install
pip install intent-api-admin
Requires intent-api>=0.5.0 and Python 3.10+.
Three-step integration
from intent_api import IntentRouter
from intent_api_admin import (
configure_admin_auth, auto_admin, admin_machine_auth,
)
configure_admin_auth(
issuer="https://console.example.com",
audience="myapp",
base=Base,
)
router = IntentRouter()
router.register("User", UserService())
auto_admin(router, expose=["User", "Team"])
app.include_router(router.build(get_user=get_clerk_user, get_db=get_db))
app.include_router(router.build_admin_machine(get_user=admin_machine_auth(), get_db=get_db))
That is the entire integration. Console operators now get full read/write CRUD on every model in expose=[...], and your handlers are unchanged.
Impersonation (Clerk actor tokens)
Impersonation consumes Clerk's native actor tokens, minted per-product via the product's own Clerk SDK. The console never holds product Clerk credentials and never mints Clerk tokens itself.
The visual indicator is a UX hint, not a guarantee. Always confirm impersonation state via the console's session list before assuming the absence of an indicator means you're not impersonating. The security boundary is server-side.
Two-step integration
Step 1 — Mint. Add a User.create_clerk_actor_token custom action that calls clerk_sdk.actor_tokens.create(...). The full reference adapter is in docs/CLERK_ACTOR_TOKEN_ADAPTER.md.
Step 2 — Enforce. Call enforce_actor_writes at the top of any write handler, OR install actor_writes_middleware once in your runtime.
# Per-handler
from intent_api_admin import enforce_actor_writes
@custom_action(admin_only=True)
async def ban(self, *, db, user, context, id, payload, session_claims=None):
enforce_actor_writes(session_claims or {}, action="custom",
is_read_only_custom_action=False)
...
# Or middleware (one-time install)
from intent_api_admin import actor_writes_middleware
runtime = build_runtime(middlewares=[actor_writes_middleware])
Audit context — one line in get_user
bind_impersonation_context populates acting_admin_id and impersonation_session_id as structlog contextvars so every log line in the handler inherits them. One line in your auth dependency:
from intent_api_admin import bind_impersonation_context
async def get_current_user(...):
auth_state = clerk_sdk.authenticate_request(...)
bind_impersonation_context(auth_state.payload) # one-liner
context.clerk_claims = auth_state.payload
...
CSP — frame-ancestors 'none'
The console always launches impersonation in a new tab via window.open(url, "_blank", "noopener,noreferrer"). Set CSP frame-ancestors 'none' on every product response so this architecture self-enforces — even if a future change tries to embed your product, browsers will refuse.
@app.middleware("http")
async def add_csp_headers(request, call_next):
response = await call_next(request)
response.headers["Content-Security-Policy"] = "frame-ancestors 'none'"
return response
See docs/PRODUCT_ADOPTION.md for the full per-product checklist (~60 LOC) and SECURITY.md for the V1 threat model.
Roles
Three roles, ordered by rank:
| Role | Rank | Default for |
|---|---|---|
owner |
3 | delete on auto-admin services |
admin |
2 | update, default for @custom_action(admin_only=True) |
support |
1 | list, read |
A higher-ranked role satisfies any lower-ranked requirement. So an owner token can perform any action that requires support, admin, or owner.
Heuristic defaults
auto_admin() reads each SQLAlchemy model and picks sensible defaults you almost never need to override.
list_fields— primary key +name/email/title/slug/status/created_atif present, then non-text columns up to 6 total.search_fields— string columns withindex=Trueorunique=Trueandlength<=255.default_sort—created_at descif present; otherwise<primary_key> desc.- Auto-redaction patterns — any column whose name starts with
password,secret,api_key,token, or_, or ends with_hash/_digest. Redacted fields are stripped from responses and refused on update.
Override patterns
auto_admin(
router,
expose=["User", "Team", "Order"],
redact_fields={"User": ["last_login_ip"]}, # add to auto-redaction
deny_edit_fields={"User": ["email"]}, # read-only in admin
deny_delete=["Order"], # disallow delete entirely
overrides={ # surgical schema override
"User": {"default_sort": "email asc",
"list_fields": ["id", "email", "name", "plan", "is_active"]},
},
)
Custom admin actions
Use the augmented @custom_action from intent_api_admin for admin-only commands. They are dispatched on the admin-machine surface only — invisible to standard, MCP, machine, and the legacy admin route.
from intent_api_admin import custom_action
class UserService(IntentService):
@custom_action(admin_only=True, min_role="admin")
async def ban(self, *, db, user, context, id, payload):
...
@custom_action(admin_only=True, min_role="support", read_only=True)
async def view_audit(self, *, db, user, context, id, payload):
...
read_only=True is honored by enforce_actor_writes (see above) so the action remains callable under a read-only Clerk actor session.
Common errors and fixes
| Code | When | Fix |
|---|---|---|
ADMIN_AUTH_NOT_CONFIGURED |
auto_admin() called before configure_admin_auth() |
Call configure_admin_auth(...) first. |
CONFIGURATION_ORDER_ERROR |
configure_admin_auth() called again after auto_admin() ran |
Reset only in tests; in prod call exactly once at startup. |
MODEL_NOT_FOUND |
expose=["X"] but X isn't on Base.registry |
Make sure the model is imported before auto_admin(). |
WRONG_SURFACE |
Admin token on /api/intent |
Use /api/admin-machine-intent for admin tokens. |
IMPERSONATION_WRITE_DENIED |
A write ran under a read-only Clerk actor session | Mint the actor token with allow_writes=true, or mark the action read_only=True. |
INSUFFICIENT_ROLE |
Token role rank is below the action's min_role |
Issue a token with the required role. |
FIELD_REDACTED |
Update payload references a redacted field | Drop the field from payload, or remove from redact_fields. |
FIELD_READ_ONLY |
Update payload references a deny_edit_fields field |
Drop the field, or remove the deny entry. |
DELETE_DENIED |
Delete on a deny_delete model |
Remove the model from deny_delete=[...]. |
UNSUPPORTED_FILTER_OP |
List filter uses an op the auto-admin filter parser doesn't support | Use one of eq, ne, lt, lte, gt, gte, contains, in, is_null. |
See TOKEN_CONTRACT.md for the full token spec.
Reference test suite
tests/wbs_reference/ is an exhaustive contract suite that mirrors the WBS success criteria S1–S52. Copy it into your repo to validate every adoption.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file intent_api_admin-0.2.0.tar.gz.
File metadata
- Download URL: intent_api_admin-0.2.0.tar.gz
- Upload date:
- Size: 52.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0a7127fd34d8fc77a8b75ec53c84705fe5cf3ef1e0ac23ed5e01fbb090faf257
|
|
| MD5 |
b475be3661463d58a1c29f71198b5c65
|
|
| BLAKE2b-256 |
9631ccee81f19867479c07016400ad969dbe5a65d5f9315d6497fff466c82e35
|
File details
Details for the file intent_api_admin-0.2.0-py3-none-any.whl.
File metadata
- Download URL: intent_api_admin-0.2.0-py3-none-any.whl
- Upload date:
- Size: 37.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e20dd46a87681c1340a95de5c49b222c6af728070562be4325e0147aebd150b6
|
|
| MD5 |
0676d171c41f5040484495517dae25e4
|
|
| BLAKE2b-256 |
a64655c0fb6178669167445728f48c476e8b93427f8f38f326794bd5378ba3bf
|