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, and delete the policies the service evaluates. This page describes each endpoint, its parameters, request and response shapes, and the status codes callers should expect.

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.

Overview#

The policy management API is a CRUD surface over Cedar policy records:

Method

Path

Purpose

GET

/v1beta/policies/

List policies with offset pagination and filters.

GET

/v1beta/policies/{id}

Fetch a single policy by numeric ID.

PUT

/v1beta/policies/

Add a single policy.

PUT

/v1beta/policies/batch/

Add many policies in one request.

DELETE

/v1beta/policies/{id}

Remove a policy by numeric ID.

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, 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.

  • Optional policy validation. When the service is started with --policy-validation (env POLICY_VALIDATION=true), each submitted policy is validated against registered service metadata before it is stored. Validation failures return 400 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 is applied to the resource query parameter on GET /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.

Authentication and authorization#

When authentication is enabled on the service, callers must include a valid bearer token:

Authorization: Bearer <token>

Requests without a token receive 401 Unauthorized. When authentication is disabled all endpoints are reachable without a token.

Two meta-permissions gate access to the API:

  • View — reading policies requires the caller to be authorized for Action::"permissions:view". This gate is evaluated using the service’s own Cedar engine against the currently stored policies.

  • Edit — creating or deleting policies requires the caller to be authorized for Action::"permissions:edit".

Callers who lack the required meta-permission receive 403 Forbidden. When the service bootstraps an empty database, a typical set of admin policies is seeded through database.init.policies (see Deployment) so that administrators can manage policies through this API.

Policy record#

All endpoints that return policies use the PolicyResponse shape:

{
  "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": ""
}

Field reference:

Field

Type

Description

id

integer (i64)

Unique policy identifier, assigned by the service.

order

integer (i32)

Evaluation order; lower values are evaluated first.

policy

string

The Cedar policy text (exactly one statement).

principal

object | null

Principal scope inferred from the policy head, or null if the head does not pin a principal. sub is the principal id; info carries additional claim data when present.

action

object | null

Action scope inferred from the policy head, or null if unset.

resource

object | null

Resource scope inferred from the policy head, or null if unset. data is an optional map of resource attributes.

created_at

string (RFC 3339 timestamp)

When the policy was created.

created_by

string

Principal id of the caller that created the policy; empty string if the service was running with authentication disabled.

GET /v1beta/policies/ — list policies#

Returns policies from the database using offset pagination. Optional query parameters narrow the result set by scope.

Query parameters#

Name

Type

Default

Description

page

integer

1

1-based page number. Must be >= 1.

limit

integer

10

Page size. Must be in 1..=50.

principal

string

Filter by principal id. Use the literal string NULL to match policies whose principal scope is unset.

action

string

Filter by action in Cedar form, for example Action::"tags:get". Use NULL to match policies whose action scope is unset.

resource

string

Filter by resource in Cedar form, for example ResourceAddress::"Astronaut.usd". Use NULL to match policies whose resource scope is unset.

The principal, action, and resource filters are parsed from their string form and must be syntactically valid; invalid values return 400 Bad Request. When a filter is omitted, policies with any scope on that dimension (including unset) match.

Request#

GET /v1beta/policies/?page=1&limit=10 HTTP/1.1
Host: permission-service.storage-apis.svc.cluster.local:3000
Authorization: Bearer <token>

Responses#

Status

Body

Meaning

200 OK

PageResult<PolicyResponse>

Page of policies returned.

400 Bad Request

error message (string)

Invalid principal, action, or resource filter.

401 Unauthorized

Missing or invalid bearer token (auth enabled).

403 Forbidden

error message (string)

Caller lacks permissions:view.

422 Unprocessable Entity

error message (string)

Query parameter failed deserialization (for example page or limit out of range).

500 Internal Server Error

error message (string)

Storage failure while reading policies.

Successful response shape:

{
  "items": [ /* array of PolicyResponse */ ],
  "page": 1,
  "page_size": 10,
  "page_count": 3
}

page_size reflects the number of items returned on this page. page_count is the total number of pages when the backend can compute it; it may be null for backends that do not report a total.

GET /v1beta/policies/{id} — fetch a policy#

Returns the PolicyResponse for a single policy.

Path parameters#

Name

Type

Description

id

integer (i64)

Policy identifier, as returned by previous calls.

Request#

GET /v1beta/policies/42 HTTP/1.1
Host: permission-service.storage-apis.svc.cluster.local:3000
Authorization: Bearer <token>

Responses#

Status

Body

Meaning

200 OK

PolicyResponse

Policy found.

401 Unauthorized

Missing or invalid bearer token (auth enabled).

403 Forbidden

error message (string)

Caller lacks permissions:view.

404 Not Found

error message (string)

No policy with that id exists.

422 Unprocessable Entity

error message (string)

id is not a valid integer.

PUT /v1beta/policies/ — add a policy#

Adds a single Cedar policy. The principal, action, and resource scopes are inferred from the Cedar text; there is no need (and no way) to send them explicitly.

Request body#

Field

Type

Required

Description

policy

string

yes

Cedar policy text. Must contain exactly one permit or forbid statement. Maximum length 65 535 characters.

order

integer

no

Evaluation order to assign to the stored policy. Defaults to the service’s --default-policy-order (env DEFAULT_POLICY_ORDER, 0 by default). Lower values are evaluated first.

Any additional JSON fields in the body are silently ignored.

Request#

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
}

Responses#

Status

Body

Meaning

200 OK

PolicyResponse

Policy stored; the response includes the assigned id, created_at, created_by, and inferred scopes.

400 Bad Request

error message (string)

The Cedar text failed to parse, policy validation (when enabled) rejected it, or the storage backend rejected the record (for example, a duplicate).

401 Unauthorized

Missing or invalid bearer token (auth enabled).

403 Forbidden

error message (string)

Caller lacks permissions:edit.

422 Unprocessable Entity

error message (string)

JSON body failed to deserialize (for example missing policy field) or policy exceeded the length limit.

500 Internal Server Error

error message (string)

Storage failure while writing the policy.

501 Not Implemented

error message (string)

The configured storage backend does not support writes (for example config-file mode).

PUT /v1beta/policies/batch/ — add many policies#

Batched version of the single-policy endpoint. The request body is a JSON array; each element is the same object accepted by PUT /v1beta/policies/. 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 can carry at most 100 items. Larger arrays are rejected with 422 Unprocessable Entity; split the work across multiple requests on the caller side.

Request#

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
  }
]

An empty array is accepted and produces an empty result without contacting the storage backend.

Responses#

Status

Body

Meaning

200 OK

PutPolicyBatchResponse

All policies stored.

400 Bad Request

error message (string)

Cedar parsing, validation, or storage rejected an item. Error messages are prefixed with batches.<index>: to identify the offending item.

401 Unauthorized

Missing or invalid bearer token (auth enabled).

403 Forbidden

error message (string)

Caller lacks permissions:edit.

422 Unprocessable Entity

error message (string)

An item failed per-field validation (for example, policy length), or the request carries more than 100 items.

500 Internal Server Error

error message (string)

Storage failure while writing the batch.

501 Not Implemented

error message (string)

Storage backend does not support writes.

Successful response shape:

{
  "results": [ /* array of PolicyResponse, 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.

Path parameters#

Name

Type

Description

id

integer (i64)

Policy identifier.

Request#

DELETE /v1beta/policies/42 HTTP/1.1
Host: permission-service.storage-apis.svc.cluster.local:3000
Authorization: Bearer <token>

Responses#

Status

Body

Meaning

204 No Content

Policy removed, or no policy with that id existed.

401 Unauthorized

Missing or invalid bearer token (auth enabled).

403 Forbidden

error message (string)

Caller lacks permissions:edit.

422 Unprocessable Entity

error message (string)

id is not a valid integer.

500 Internal Server Error

error message (string)

Storage failure while deleting the policy.

501 Not Implemented

error message (string)

Storage backend does not support writes.

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#

When the service runs with the REST API enabled, the full OpenAPI definition is served at /openapi.json and an interactive Swagger UI is available at /swagger-ui on the REST port (default 3000). Those surfaces stay in sync with the service implementation and cover every field and schema referenced on this page.

See also#