Managing policies through the REST API#
Permission Service exposes a REST API under /v1beta/policies/ for managing Cedar policies at runtime. Use it to list, fetch, create, update, and delete the policies the service evaluates.
This API is specific to this service and is not part of the published Omniverse Storage Permission API — that contract covers only the authorization endpoints — so it is documented in full here. Runtime management requires Postgres mode; in config-file mode the store is read-only and every write is answered with 501 Not Implemented.
For the Cedar syntax used in the policy field, see Writing Cedar policies for Permission Service. For how policies are loaded from Helm or YAML at startup, see Deployment and Database configuration.
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.
Overview#
The policy management API is a CRUD surface over Cedar policy records:
Method |
Path |
Purpose |
|---|---|---|
GET |
|
List policies with offset pagination and filters. |
GET |
|
Fetch a single policy by identifier. |
PUT |
|
Add a policy, or update one with the same text. |
PUT |
|
Add or update many policies in one request. |
DELETE |
|
Remove a policy by identifier. |
Key behaviors that apply to all endpoints:
Storage backend. Write operations require Postgres mode. In config-file mode the storage is read-only and write endpoints return
501 Not Implemented. See Deployment for how to select a backend.Scopes are inferred from the Cedar text. When you add a policy, the service parses the policy head and derives the optional principal, group, action, and resource scopes. You do not send these fields; any extra JSON fields in the payload are ignored. Scope inference rules are described in Policy scopes.
Writes are upserts. Submitting a policy whose canonical text already exists updates that record in place instead of creating a duplicate. The single-policy endpoint signals which happened through its status code and the
createdfield; the batched endpoint signals it per item.Policy validation is enabled by default. Each submitted policy is validated against registered service metadata before it is stored. Validation is controlled by the
POLICY_VALIDATIONenvironment variable, which defaults totrueand is not exposed as a Helm value, so chart deployments run with validation on. Validation failures return400 Bad Request.Resource ID normalization. When a policy pins a resource (for example
resource == ResourceAddress::"https://example.com/file name.usd"), the service URL-encodes the resource id before storage. The same encoding — and the URL remapper, when configured — is applied to theresourcequery parameter onGET /v1beta/policies/, so clients should pass the id in the same textual form the API returns.Cache invalidation and events. Successful writes invalidate the in-memory evaluation cache for the affected
(principal, action, resource)tuple and, when notifications are enabled, publish a permission-change event.
Error codes#
All error responses carry a plain JSON string describing the failure.
Status |
Condition |
|---|---|
|
An |
|
The bearer token is missing, invalid, or expired (authentication enabled). |
|
The caller lacks |
|
No policy exists with the requested identifier ( |
|
A path or query parameter failed deserialization (a non-integer |
|
A storage failure occurred while reading or writing policies. |
|
The configured storage backend does not support writes (config-file mode). |
Request and response models#
PutPolicyPayload#
Request body of PUT /v1beta/policies/, and each item of PUT /v1beta/policies/batch/.
Field |
Type |
Required |
Description |
|---|---|---|---|
|
string |
yes |
Cedar policy text. MUST contain exactly one |
|
integer |
no |
Evaluation order to assign to the stored policy. Defaults to the deployment-wide |
|
string |
no |
Free-form note describing what the policy does or who it was created for. Stored as metadata and never used during authorization. Maximum length 4 096 characters; surrounding whitespace is trimmed and an empty result is stored as unset. |
Any additional JSON fields in the body are silently ignored.
PolicyResponse#
A stored policy, as returned by the read endpoints.
{
"id": 1,
"order": 0,
"policy": "permit(principal == Principal::\"test-user\", action == Action::\"tags:get\", resource == ResourceAddress::\"Astronaut.usd\");",
"resource": {
"id": "Astronaut.usd",
"type": "ResourceAddress",
"data": null
},
"action": {
"name": "get",
"service": "tags"
},
"principal": {
"sub": "test-user",
"info": null
},
"created_at": "2026-04-16T12:34:56Z",
"created_by": "",
"comment": null
}
Field |
Type |
Required |
Description |
|---|---|---|---|
|
integer |
yes |
Unique policy identifier, assigned by the service. |
|
integer |
yes |
Evaluation order; lower values are evaluated first. |
|
string |
yes |
The Cedar policy text (exactly one statement). |
|
object |
no |
Principal scope inferred from the policy head, or |
|
object |
no |
Group scope inferred from a |
|
object |
no |
Action scope inferred from the policy head, or |
|
object |
no |
Resource scope inferred from the policy head, or |
|
object |
no |
Hierarchy scope of a |
|
string |
yes |
RFC 3339 timestamp of when the policy was created. |
|
string |
yes |
Principal id of the caller that created the policy; an empty string when the service was running with authentication disabled. |
|
string |
no |
Administrator-supplied note stored with the policy, or |
PutPolicyResponse#
Response of PUT /v1beta/policies/. Carries every PolicyResponse field plus:
Field |
Type |
Required |
Description |
|---|---|---|---|
|
boolean |
yes |
|
PutPolicyBatchResponse#
Response of PUT /v1beta/policies/batch/.
Field |
Type |
Required |
Description |
|---|---|---|---|
|
array of PutPolicyResponse |
yes |
One entry per submitted item, in request order. |
Policy page#
Response of GET /v1beta/policies/: one page of policies.
Field |
Type |
Required |
Description |
|---|---|---|---|
|
array of PolicyResponse |
yes |
Policies on this page. |
|
integer |
yes |
1-based number of the returned page. |
|
integer |
yes |
Number of items returned on this page. |
|
integer |
no |
Total number of pages when the backend can compute it; |
GET /v1beta/policies/ — list policies#
Returns policies from the database using offset pagination, as a policy page. Optional query parameters narrow the result set by scope.
This is a scope filter, not an authorization query: global policies (permit(principal, action, resource)) are never attached to a specific resource and so are never returned by a resource-filtered listing. Use the authorization API to see the full evaluated set for a request, or the Diagnostics API to see the candidate policies for one request.
Query parameters#
Name |
Type |
Default |
Description |
|---|---|---|---|
|
integer |
|
1-based page number. MUST be |
|
integer |
|
Page size. MUST be between |
|
string |
— |
Filter by principal id, as a plain string (for example |
|
string |
— |
Filter by the group id of |
|
string |
— |
Filter by action in |
|
string |
— |
Filter by resource in |
|
string |
|
Resource scope-matching mode. |
|
integer |
— |
Filter by the exact evaluation order of the policy. |
The action and resource filters are parsed from their string form and MUST be syntactically valid; values that cannot be parsed return 400 Bad Request. When a filter is omitted, policies with any scope on that dimension (including unset) match.
With scope=inherited, the ancestor prefixes are derived from the resource type’s parentType registered in metadata, falling back to the resource’s own type when the type is unregistered or declares no parent. See Parent type for resource hierarchies.
Example#
GET /v1beta/policies/?page=1&limit=10&action=tags:get HTTP/1.1
Host: permission-service.storage-apis.svc.cluster.local:3000
Authorization: Bearer <token>
HTTP/1.1 200 OK
Content-Type: application/json
{
"items": [ /* array of PolicyResponse */ ],
"page": 1,
"page_size": 10,
"page_count": 3
}
GET /v1beta/policies/{id} — fetch a policy#
Returns the PolicyResponse for a single policy, or 404 Not Found when no policy has that identifier.
Path parameters#
Name |
Type |
Description |
|---|---|---|
|
integer |
Policy identifier, as returned by previous calls. |
Example#
GET /v1beta/policies/42 HTTP/1.1
Host: permission-service.storage-apis.svc.cluster.local:3000
Authorization: Bearer <token>
PUT /v1beta/policies/ — add or update a policy#
Stores a single Cedar policy supplied as a PutPolicyPayload. The principal, group, action, and resource scopes are inferred from the Cedar text; there is no need (and no way) to send them explicitly.
The endpoint is an upsert keyed by the canonical policy text:
201 Created— the policy was newly inserted; the PutPolicyResponse body reports"created": true.200 OK— an existing policy with the same canonical text was updated in place; the body reports"created": false.
Example#
PUT /v1beta/policies/ HTTP/1.1
Host: permission-service.storage-apis.svc.cluster.local:3000
Authorization: Bearer <token>
Content-Type: application/json
{
"policy": "permit(principal == Principal::\"test-user\", action == Action::\"tags:get\", resource == ResourceAddress::\"Astronaut.usd\");",
"order": 10,
"comment": "Grants the test user read access to the sample asset."
}
HTTP/1.1 201 Created
Content-Type: application/json
{
"id": 42,
"order": 10,
"policy": "permit(principal == Principal::\"test-user\", action == Action::\"tags:get\", resource == ResourceAddress::\"Astronaut.usd\");",
"resource": {"id": "Astronaut.usd", "type": "ResourceAddress", "data": null},
"action": {"name": "get", "service": "tags"},
"principal": {"sub": "test-user", "info": null},
"created_at": "2026-04-16T12:34:56Z",
"created_by": "admin-user",
"comment": "Grants the test user read access to the sample asset.",
"created": true
}
PUT /v1beta/policies/batch/ — add or update many policies#
Batched version of the single-policy endpoint. The request body is a JSON array; each element is a PutPolicyPayload. Scopes are inferred per item from the Cedar text.
The service validates every item up front. If any item fails validation or scope inference, the whole request is rejected before any policy is written. A single request MAY carry at most 100 items; larger arrays are rejected with 422 Unprocessable Entity, so split the work across multiple requests on the caller side.
A successful request always returns 200 OK with a PutPolicyBatchResponse; each item’s created field reports whether that policy was inserted or updated in place. An empty array is accepted and produces an empty result without contacting the storage backend.
Example#
PUT /v1beta/policies/batch/ HTTP/1.1
Host: permission-service.storage-apis.svc.cluster.local:3000
Authorization: Bearer <token>
Content-Type: application/json
[
{
"policy": "permit(principal == Principal::\"test-user-1\", action == Action::\"tags:get\", resource == ResourceAddress::\"Astronaut.usd\");"
},
{
"policy": "permit(principal == Principal::\"test-user-2\", action == Action::\"tags:get\", resource == ResourceAddress::\"Astronaut.usd\");",
"order": 5
}
]
HTTP/1.1 200 OK
Content-Type: application/json
{
"results": [ /* array of PutPolicyResponse, in the same order as the request */ ]
}
DELETE /v1beta/policies/{id} — delete a policy#
Removes a Cedar policy from the database. The endpoint is idempotent: deleting a policy that does not exist still returns 204 No Content, and no change event is published because nothing was removed.
The permissions:edit gate is evaluated against the stored policy’s own resource scope — canonicalized the same way the authorization path canonicalizes a request — so an edit grant scoped to a resource or to its folder hierarchy matches a policy stored under an alias of that URL.
Path parameters#
Name |
Type |
Description |
|---|---|---|
|
integer |
Policy identifier. |
Example#
DELETE /v1beta/policies/42 HTTP/1.1
Host: permission-service.storage-apis.svc.cluster.local:3000
Authorization: Bearer <token>
HTTP/1.1 204 No Content
Change notifications#
When notifications are enabled, every successful write (PUT /v1beta/policies/, PUT /v1beta/policies/batch/, and DELETE /v1beta/policies/{id}) causes the service to publish a permission-change event to the configured Event Aggregation Service. Each event identifies the (principal, action, resource) tuple whose effective policies may have changed, so downstream services can invalidate their own caches or otherwise react to the update. Failed or rejected writes do not emit events.
Notifications are best-effort and do not block the API response: a caller receives its success status as soon as the policy is persisted, and the event is dispatched asynchronously.
For the event schema, delivery guarantees, and how to configure the Event Aggregation Service endpoint, see Service notifications.
Interactive API reference#
A running service publishes its own OpenAPI definition at /openapi.json and serves an interactive API browser at the root path (/) on the REST port (default 3000). Both reflect the deployed build, so they are the quickest way to confirm the exact payloads a particular version accepts.
See next#
Managing service metadata through the REST API — the catalog of services, actions, and resource types the scopes above refer to.
Related pages:
Writing Cedar policies for Permission Service — syntax, scopes, and conditions.
How authorization requests are evaluated — how
orderand resource evaluation priority drive the decision for stored policies.Deployment — selecting Postgres or config-file mode and seeding initial policies.
Database configuration — on-disk policy format and scope inference details.
Policy management metrics — counters and histograms emitted by these endpoints.