Skip to content

Resource and Grant

A resource is anything an agent wants to access — an API endpoint, an MCP tool, a data service, or any addressable target. A grant is the binding that permits a specific application to access a resource on behalf of a specific user. Together, resources and grants define the access surface that policies evaluate.

A resource is registered in a zone with these attributes:

FieldTypeDescription
idUUIDInternal identifier used in grants and delegation constraints
zone_idstringZone this resource belongs to
namestringHuman-readable label
identifierstringLogical name used in policy rules and mandate target claims (e.g. resource://payments)
upstream_urlstring | nullThe actual URL the Gateway proxies to, if routing through Gateway
prefixbooleanIf true, upstream_url is a URL prefix; path is appended from the request
scopesstring[]Full set of scopes available for this resource
credential_provider_idstring | nullProvider to use for credential substitution, if any

The identifier is the string that appears in policy rules (input.resource.identifier) and in mandate target claims. It is a stable logical name, not an internal UUID.

The scopes array defines all scopes the resource can grant. A grant can assign any subset of these scopes to an application. A policy can further restrict which scopes are included in the mandate.

A grant binds an application and an optional user identity to a resource and a set of scopes:

FieldTypeDescription
idUUIDInternal identifier
zone_idstringZone this grant belongs to
application_idstringThe application being granted access
user_idstringOpaque user identifier (not validated against any directory)
resource_idstringThe resource being granted
scopesstring[]Subset of the resource’s scopes being granted
statusstringGrant lifecycle status

The scopes on a grant must be a subset of the resource’s scopes. The grant does not itself authorize access — it establishes that the principal has been given permission. The STS policy still evaluates whether to actually issue a mandate. A grant is necessary but not sufficient.

The user_id is an opaque string. Caracal does not maintain a user directory or authenticate users. The user_id is stored for audit purposes and is available to policies via actor_claims.

Resource defines scopes: ["read", "write", "transfer"]
Grant allows a subset: ["read", "write"]
Exchange requests a subset: ["read"]
Policy decides on the request: allow / deny
Mandate carries granted scope: "read"

At each stage, scopes can only be narrowed, never widened. An exchange request cannot ask for scopes the grant does not include. The mandate’s scope claim contains only what the policy allowed.

A resource can be associated with a credential provider via credential_provider_id. When this is set, the STS retrieves the relevant provider credential (an OAuth token or API key stored encrypted under the zone DEK) and includes it in the upstreams directive of the token response.

The Gateway reads the upstreams directive and substitutes the provider credential as the Authorization header sent to the upstream. The agent never holds the provider credential — it only presents its mandate to the Gateway, and the Gateway handles the provider authentication.

Provider kind values:

KindDescription
oauth2OAuth 2.0 access token
oidcOpenID Connect token
apikeyStatic API key
workloadMachine-to-machine workload identity

Provider secrets are encrypted before storage. Any field whose name matches the pattern /(secret|password|token|api[_-]?key|private[_-]?key|credential|passphrase)/i is sealed under the zone KEK and never returned in API responses.

When a request arrives at the Gateway, the Gateway:

  1. Reads the X-Caracal-Resource header from the request to identify the target resource.
  2. Looks up the resource binding in its binding store (populated from the database) to find the upstream URL.
  3. Verifies the resource identifier appears in the mandate’s target claim.
  4. If the resource has a credential provider, reads the upstreams directive from the mandate to determine the credential substitution mode.
  5. Forwards the request to the upstream with the appropriate Authorization header.

Without a credential_provider_id, the Gateway forwards the mandate as the Authorization: Bearer header to the upstream. The upstream is responsible for verifying the mandate against the zone JWKS.

The identifier field is the string that policies inspect. Use logical names that remain stable even if the underlying upstream URL changes:

# Allow read access to the payments resource
result := {
"decision": "allow",
"evaluation_status": "complete",
"determining_policies": ["payments-read-policy"],
"diagnostics": [],
} if {
input.resource.identifier == "resource://payments"
"read" in input.context.requested_scopes
}

The input.resource.id (UUID) is also available, but using identifier is more stable and readable.