Model exposure
Which models become API endpoints, the built-in block-list (auth_user, session, the permissions/tasks/admin tables), and the include_only / exclude / expose controls.
umbral-rest walks the model registry and mounts /api/<table>/ for every registered model: exposure is opt-out, not opt-in. Add RestPlugin::default() and every model you've registered gets List + Retrieve + Create + Update + Delete, except a small set of framework-internal tables that are blocked by default. Writes are gated by the default ReadOnly permission, so an unconfigured resource is read-only until you opt in (see Permissions).
App::builder() .model::<Article>() .model::<Order>() .plugin(umbral_rest::RestPlugin::default()) .build()?;// → /api/article/ and /api/order/ are live. auth_user / session are NOT.The default block-list
A set of framework-internal tables is refused even though every other model is served. The list (DEFAULT_BLOCKED_TABLES in plugins/umbral-rest/src/lib.rs):
| Table | Why it's blocked |
|---|---|
auth_user | Serving it would leak password hashes and let PATCH /api/auth_user/1 flip is_staff / is_superuser. |
session | Serving it would leak session identifiers: anyone reading the table could hijack a live session. |
umbral_migrations | The migration ledger. Internal bookkeeping, never a public resource. |
permissions_permission, permissions_contenttype, permissions_group, permissions_usergroup, permissions_userpermission | The authorization tables from umbral-permissions. Exposing them would let a client read or rewrite who-can-do-what. |
task_row | The umbral-tasks queue rows - internal job state, not a public resource. |
admin_audit_log | The admin's audit trail. Internal bookkeeping. |
A request to a blocked table 404s; the route is never mounted. This is the backstop that keeps the credential store (and the framework's own bookkeeping tables) off the API even though the plugin auto-exposes everything else.
Beyond the table block-list, the field password_hash is always stripped from every response, on every table, and can never be re-exposed (not even with .expose(...)). It's a hard, un-overridable denylist entry - the last line of defence if a custom credential model slips past the table block-list.
Controlling what's exposed
Four builder methods, applied in a strict precedence order.
include_only([...])
Allowlist. ONLY the named tables are served; every other model, blocked or not, is denied. The tightest control.
exclude([...])
Blocklist. Adds your own tables to the default block-list. Use for a business model you don't want on the API.
expose([...])
Opt INTO a default-blocked table (auth_user / session). The escape hatch: you're saying 'I know what I'm doing.'
ResourceConfig::hide(col)
Strips a column from responses and write bodies. Pair with expose to serve auth_user without the password_hash column.
// Tighten to a fixed allowlist; nothing else is reachable.RestPlugin::new().include_only(["article", "order"]); // Block one of your own models that would otherwise be served.RestPlugin::default().exclude(["internal_audit_log"]); // Deliberately serve the user list, without the password hash.RestPlugin::default() .expose(["auth_user"]) .resource(ResourceConfig::new("auth_user").hide("password_hash"));hide (and transform / computed) also applies to a table when it appears NESTED under an ?include='d relation. For example ?include=created_by hydrates auth_user under created_by, and the password_hash hidden on auth_user is stripped from that nested object too, at every hop.Registering many resources at once
REST customization is best defined per app - next to the models it describes - rather than piled into main.rs. Each plugin exports a Vec<ResourceConfig> and the app registers them in one .resources(...) call instead of a .resource(...) per model:
// plugins/blog/src/lib.rs - the blog app's REST surface, next to its modelspub fn rest_resources() -> Vec<umbral_rest::ResourceConfig> { vec![ ResourceConfig::new("post").hide("draft_notes"), ResourceConfig::new("comment"), ]} // main.rs - one call per app, no per-model boilerplateRestPlugin::default() .resources(blog::rest_resources()) .resources(shop::rest_resources());.resources(iter) accepts any IntoIterator<Item = ResourceConfig> (a Vec, an array, …) and is exactly equivalent to calling .resource(...) once per item - additive, same precedence.
Precedence
allow(table) decides exposure top-down. The first matching rule wins:
include_only: if set, only listed tables pass; everything else is denied outright.expose: a default-blocked table you've opted into (still subject toexclude).- default block-list: the framework-internal tables above (
auth_user,session,umbral_migrations, thepermissions_*tables,task_row,admin_audit_log) are denied. exclude: your own additions are denied.- otherwise served.
So an auth_user that's on expose but also on exclude stays blocked: an explicit "no" beats an explicit "yes".
The block-list matches by table name, not by type. umbral-rest discovers models dynamically and never names the AuthUser Rust type (it can't depend on umbral-auth). If you swap in a custom user / credential model under a different table name, it is not auto-protected: its name isn't auth_user, so it's served like any other model. Add it yourself with .exclude(["your_user_table"]).
Auto-exposure means a business model (customer, payment, invoice) is readable the moment you add RestPlugin::default(): NoAuthentication plus the default ReadOnly permission means anyone can list and retrieve it, though writes return 403 until you opt in. The block-list only protects the framework's own internal tables, not yours. To require auth even for reads, set a stricter permission class (e.g. IsAuthenticated or IsStaff) per resource, and reach for include_only when you want exposure to be an explicit allowlist rather than opt-out.
The API root
GET /api/ (the base path you mounted at) returns a browsable index of everything the API exposes:
{ "resources": { "post": { "path": "/api/post/", "detail": "/api/post/{id}" } }, "endpoints": [ { "group": "oauth", "name": "google.login", "method": "GET", "path": "/oauth/google/login", "url": "https://api.example.com/oauth/google/login", "label": "Sign in with Google" } ]}resources lists every model that passes the exposure rules above, each with its collection + detail path. endpoints aggregates what other plugins advertise via the framework's Plugin::api_endpoints() seam (OAuth's login/connect links, for instance), each with an absolute url joined from the incoming request's origin. REST lists them without depending on those plugins' crates. (When REST is mounted at the bare root, the index is skipped so it doesn't shadow your app's home route.)
See also
- Permissions: gate who can perform each action once a table is exposed.
- Authentication: resolve the caller's identity.
- The plugin contract and the full exposure rules live in
arch.mdanddocs/specs/.