Listing resources (ReBAC)
list_resources() answers the reverse question of check(): instead of “may this subject act on this
one resource?”, it asks “which resources can this subject reach under a given relation?”. This is a
ReBAC (relationship-based) lookup, served by POST {base}/decisions/list-resources.
Motivation
To render a list view — “the warehouses I can view”, “the projects I can edit” — you do not want to call
check() once per candidate resource. list_resources() lets the server enumerate the reachable set in
a single round-trip.
The call
use laravel_iam::Subject;
let warehouses = iam
.list_resources(Subject::user("usr_123"), "viewer")
.await?;
for r in &warehouses {
println!("{}:{}", r.kind, r.id); // e.g. "warehouse:wh_milan"
}
The signature is:
pub async fn list_resources(
&self,
subject: Subject,
relation: impl AsRef<str>,
) -> Result<Vec<Resource>, IamError>
relation accepts anything string-like (&str, String, …). The request body is exactly
{ "subject": { "type", "id" }, "relation": "<relation>" }.
The response
Each entry is a Resource — { kind, id }, where kind serializes as type on the
wire. The parser is permissive about envelope shape: it accepts either
{ "resources": [ { "type": "warehouse", "id": "wh_milan" } ] }
or a bare top-level array:
[ { "type": "warehouse", "id": "wh_milan" } ]
Fail-closed, but not “deny”
list_resources() is fail-closed in the sense that any failure yields an Err rather than a partial
or empty list — you never silently get a truncated set that looks like “no access”:
match iam.list_resources(Subject::user("usr_123"), "viewer").await {
Ok(resources) => render(resources),
Err(e) => {
// Do NOT treat this as "empty list" — it's an error. Surface or retry.
tracing::warn!("list_resources failed: {e}");
show_error_state();
}
}
Unlike check() — where an error collapses to deny — an error from list_resources() is not the
same as “the subject can reach nothing”. Distinguish Ok(vec![]) (genuinely empty) from Err(..)
(could not determine the set). Treating an error as an empty list can hide data the user is entitled to,
or mask an outage.
Worked example: building a scoped menu
use laravel_iam::{IamClient, Subject, Resource};
async fn editable_projects(iam: &IamClient, user_id: &str) -> Result<Vec<Resource>, ()> {
iam.list_resources(Subject::user(user_id), "editor")
.await
.map_err(|e| {
tracing::error!("could not list editable projects: {e}");
})
}
Status and error handling
The same status mapping as check() applies before parsing:
| Server status | Result |
|---|---|
2xx |
parsed into Vec<Resource> |
401 / 403 |
IamError::Unauthorized |
| other non-2xx | IamError::Http |
| unparseable / wrong-shaped body | IamError::Malformed |
Gotchas
Err≠ empty. See above — the most common mistake is collapsing errors into “no results”.- Relation names are server-defined.
viewer,editor,owner, … must match the relations the
server’s ReBAC model defines for that resource type. - Large sets. The server returns the full reachable set; for very large tenancies prefer a scoped
check()on a specific resource where you can.
See also: Checking decisions, The wire contract.