Role-based access control over HTTP. auth answers one question — may user X
do Y — so your services don't reinvent roles and permissions.
It is authorization, not authentication: it does not log anyone in, store
passwords, or issue tokens. It trusts that the caller already knows who the
user is, and decides what they may do. Model:
user → (member of) → role → (holds) → permission.
Your client key is any UUID4 — it is also your private, isolated namespace. Generate one, keep it secret, and reuse it for every call. A role must exist before you add members or permissions to it.
KEY=$(python3 -c "import uuid; print(uuid.uuid4())")
BASE=https://auth.rodmena.app
curl -X POST -H "Authorization: Bearer $KEY" $BASE/api/role/engineers
curl -X POST -H "Authorization: Bearer $KEY" $BASE/api/permission/engineers/deploy
curl -X POST -H "Authorization: Bearer $KEY" $BASE/api/apikeys/user/alice
curl -X POST -H "Authorization: Bearer $KEY" $BASE/api/membership/alice/engineers
curl -H "Authorization: Bearer $KEY" $BASE/api/has_permission/alice/deploy
# -> {"success": true, "data": {"has_permission": true}, ...}Why the apikeys call is in there. Since 3.0.0 a namespace created from a
brand-new key is strict: a user must hold an API key in your namespace
before it can be given a role. Omit that line and the membership call answers
409 {"reason": "user_not_key_backed", "result": false} — a permanent refusal,
so retrying never helps. You need not keep the returned secret if you only want
the identity to exist.
If your users can never hold auth API keys (you authenticate them yourself and your "users" are opaque ids), opt the namespace out once and skip that step forever:
curl -X PUT -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"strict_users": false}' $BASE/api/settingsThe opt-out is audited, per-tenant, and supported indefinitely. Namespaces created before 3.0.0 were grandfathered onto it, which is why existing integrations saw no change at upgrade.
With the Python client (pip install auth):
from auth import Client
with Client(api_key=KEY, service_url="https://auth.rodmena.app") as c:
c.create_role("engineers")
c.add_permission("engineers", "deploy")
c.create_api_key("alice") # strict default since 3.0.0
c.add_membership("alice", "engineers")
c.user_has_permission("alice", "deploy") # -> {... "has_permission": true}- Writes can return HTTP 200 with
{"result": false}(e.g. adding to a missing role). Check theresult/datafield, not just the status code. - Two response shapes — bare
{"result": ...}and wrapped{"success", "data", ...}. The API reference says which per endpoint. - Errors below 2xx are HTML, not JSON. Branch on the status code first.
- Python client: transport failures raise
AuthTransportError(3.0.0). The 2.x answer-shaped error dict is gone — an outage can never be misread as a denial. Catch the exception and map it to your unavailable/503 path, never to a permission denial. The oldraise_on_errorconstructor argument is a deprecated no-op, so 2.x code keeps constructing. - Reuse one key. A new key is a new empty namespace, not an error. Keep the
key out of source control, logs, and URLs — it is the only thing protecting
your data. Rotate it with
POST /api/keys/rotateif it leaks. - Per-user API keys (2.4.0):
/api/apikeys/user/<user>(create/list),/api/apikeys/user/<user>/<key_id>(revoke),/api/apikeys/validate. auth mintsrak_...secrets for your users, shows each exactly once, stores only a hash, and validates them inside your namespace — an identity UI fronts the lifecycle, backends validate then use the RBAC checks. Client methods:create_api_key,list_api_keys,revoke_api_key,validate_api_key,check_api_key_permission(validate + permission in one round trip), plusget_settings/set_strict_users. All of these also exist on the in-processAuthorizationwrapper for embedded consumers. - Strict user identity is the default for NEW tenants (3.0.0). A tenant
namespace created after 3.0.0 requires key-backed users for authorization
decisions (
user_not_key_backedanswers; key-less membership grants answer 409) — create the user's API key first, then grant roles. Every tenant that existed before 3.0.0 was grandfathered with an explicitstrict_users: falserow (nothing changed for them at upgrade), and the audited per-tenant opt-out (PUT /api/settings {"strict_users": false}) survives indefinitely for validated machine-subject architectures. Details: docs/DEPRECATIONS.md.
| Doc | What's in it |
|---|---|
Live API reference — /docs · /llms.txt |
Every endpoint and exact response shape, served by the app (agent-friendly). |
| docs/ARCHITECTURE.md | Design, components, request lifecycle, data model, permission-check and key-rotation flows, diagrams. |
| SECURITY.md | Security model (tenant isolation, encryption, audit, rotation), threat notes, reporting. |
| MIGRATIONS.md | Schema creation vs migrations, upgrade/rollback runbook. |
| CONTRIBUTING.md | Local setup, tests (sqlite + postgres), lint/type-check, CI. |
| docs/ | Full Sphinx docs (concepts, configuration, encryption, deployment, REST & Python usage). |
Use it for RBAC: named roles, permissions, group membership, and boolean "can user X do Y" gates for a service, CLI, or workflow engine.
Not for authentication (login/passwords/sessions/OAuth/JWT), fine-grained / attribute-based rules (owner-of-this-record, time-of-day, row-level tenancy — reach for an ABAC/policy engine), or air-gapped hot loops where a network hop per check is too costly (cache, or use the library in-process).
python3.11 -m venv .venv && . .venv/bin/activate
pip install -e ".[dev,ratelimit,migrations]"
make check # ruff + mypy
make test # sqlite suite
make test-postgres # postgres integration (Docker), encryption onSee CONTRIBUTING.md for the full workflow. Licensed under the MIT License.