---
title: Guardrails
description: Permission boundaries, service control policies and resource
  control policies, and what each one enforces today.
url: https://hexelstudio.com/docs/identity/guardrails
---

<Warning>
Most guardrails do not affect your normal API calls yet. Service control policies, resource control policies, permission boundaries that target a member or a service user, and **Deny statements in any policy** are applied only by the full evaluator: they change the answers of `/v1/authz/evaluate`, `/v1/authz/simulate` and `/v1/authz/explain`, and the check made when your principals assume a role in another organization, but a call they "deny" still succeeds when you make it. Boundaries with `target_type` `GROUP` or `ROLE` cap nothing. The one guardrail enforced on every call is a permission boundary set as a role's `boundary_id`: it caps that role's sessions. This API may change.
</Warning>

## What takes effect today

| Guardrail | Your normal API calls | Full evaluator (`/v1/authz/*`, cross-organization role assumption) |
| --- | --- | --- |
| Boundary named by a role's `boundary_id` | **Caps every call made with a session of that role** | Not used (the full evaluator does not evaluate role sessions) |
| Boundary with `target_type` `USER` or `SERVICE_USER` | No effect | Caps that member or service user |
| Boundary with `target_type` `GROUP` or `ROLE` | No effect | No effect |
| Service control policy | No effect | Deny statements deny the action |
| Resource control policy | No effect | Deny statements deny the action, when the request names a matching `resource_type` |
| Deny statement in an attached policy | No effect | Denies the action |

The two decision paths are explained in [What Preview means](https://hexelstudio.com/docs/identity/preview#two-ways-a-request-is-checked). Your normal API calls are allowed by the **Allow** statements of the caller's policies alone.

## Permission boundaries

A permission boundary is a policy document that sets the most a principal can do. It never grants anything. An action is within the boundary when one of the boundary's **Allow** statements matches it. The boundary's Deny statements, and the `Resource` and `Condition` of its statements, are ignored. A boundary whose document cannot be read allows nothing.

Each boundary has one target. A target has at most one boundary in your organization.

| `target_type` | `target_id` must be | What the boundary caps |
| --- | --- | --- |
| `USER` | A member of your organization | That member, in the full evaluator only |
| `SERVICE_USER` | A service user of your organization | That service user, in the full evaluator only |
| `GROUP` | A group of your organization | Nothing |
| `ROLE` | A role of your organization | Nothing by itself. Set the boundary as the role's `boundary_id` to cap the role. |

`target_type` is case-insensitive and stored in upper case.

### Cap a role's sessions

This is the part of permission boundaries that is enforced today. When a role has a `boundary_id`, every session of the role can use only the actions that are allowed both by the role's policies and by the boundary. This applies to every API call made with the role session's token.

Set the boundary when you create the role, or update the role:

```bash
curl -X PATCH https://api.hexelstudio.com/v1/roles/<role-id> \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"boundary_id": "<boundary-id>"}'
```

- The boundary must belong to your organization. Its `target_type` and `target_id` do not matter for this.
- Send `"boundary_id": ""` to remove the boundary from the role.
- Updating a role needs `iam:role:write`. Changing or removing a role's existing boundary also needs the rule in [Lifting a boundary](#lifting-a-boundary).
- If you delete a boundary that a role still names, the role's sessions can do nothing until you set another boundary or clear `boundary_id`.

See [Roles](https://hexelstudio.com/docs/identity/roles) for creating roles and assuming them.

### Create a boundary

`POST /v1/boundaries` needs `iam:boundary:write`.

| Field | Type | Description |
| --- | --- | --- |
| `name` | string | Name of the boundary. |
| `display_name` | string | Name shown in the console. |
| `description` | string | Optional. |
| `target_type` | string | **Required.** `USER`, `SERVICE_USER`, `GROUP` or `ROLE`. |
| `target_id` | string | **Required.** ID of the target. It must belong to your organization. |
| `policy_document` | object | The boundary document. Only its Allow statements count. |

```bash
curl -X POST https://api.hexelstudio.com/v1/boundaries \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "ci-readonly",
    "display_name": "CI read-only",
    "target_type": "ROLE",
    "target_id": "<role-id>",
    "policy_document": {
      "Version": "2026-02-17",
      "Statement": [
        {"Effect": "Allow", "Action": ["workspace:*", "iam:policy:read"], "Resource": "*"}
      ]
    }
  }'
```

The response is `201 Created` with the boundary:

```json
{
  "id": "pb_...",
  "name": "ci-readonly",
  "display_name": "CI read-only",
  "organization_id": "<org-id>",
  "target_type": "ROLE",
  "target_id": "<role-id>",
  "policy_document": {
    "Version": "2026-02-17",
    "Statement": [
      {"Effect": "Allow", "Action": ["workspace:*", "iam:policy:read"], "Resource": "*"}
    ]
  },
  "created_by_user_id": "<user-id>",
  "created_at": "2026-10-20T09:00:00Z",
  "updated_at": "2026-10-20T09:00:00Z"
}
```

| Status | When |
| --- | --- |
| `400` | `target_type` is not one of the four types, `target_id` is missing, or the target does not belong to your organization. |
| `409` | The target already has a boundary in your organization. |

### List, read, update and delete

| Call | Permission | Response |
| --- | --- | --- |
| `GET /v1/boundaries` | `iam:boundary:read` | Array of your organization's boundaries |
| `GET /v1/boundaries/{boundary_id}` | `iam:boundary:read` | The boundary, or `404` |
| `PATCH /v1/boundaries/{boundary_id}` | `iam:boundary:write` | The updated boundary |
| `DELETE /v1/boundaries/{boundary_id}` | `iam:boundary:write` | `204 No Content` |

`PATCH` takes any of `name`, `display_name`, `description`, `target_type`, `target_id` and `policy_document`. `id`, `organization_id`, `created_by_user_id` and the timestamps never change.

```bash
curl -X PATCH https://api.hexelstudio.com/v1/boundaries/<boundary-id> \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"policy_document": {"Version": "2026-02-17", "Statement": [{"Effect": "Allow", "Action": ["workspace:workspace:read"], "Resource": "*"}]}}'
```

### Lifting a boundary

Creating a boundary, or narrowing one, only restricts. Deleting a boundary, moving it to another target, widening its document, or changing or clearing a role's `boundary_id` lifts a cap: actions the capped principal was granted but the boundary blocked become usable. Hexel Studio therefore requires that you already hold, across your organization, every action such a change unblocks. Otherwise the call fails with `403`:

```json
{
  "error": "forbidden",
  "reason": "cannot lift a permission boundary over actions the caller does not hold",
  "missing_actions": ["workspace:workspace:delete"]
}
```

You also cannot change or delete a boundary that caps you, and a role session cannot change its own role's `boundary_id`:

```json
{"error": "forbidden", "reason": "cannot change a permission boundary that caps the caller"}
```

## Service control policies

A service control policy (SCP) is an organization-wide list of actions that nobody in the organization may perform. Only its **Deny** statements count: when an enabled SCP has a Deny statement that matches an action, the full evaluator denies the action with reason `scp_denied`. Allow statements in an SCP have no effect.

<Note>
SCPs do not block your normal API calls today. They change only the answers of `/v1/authz/*` and the check on your organization when one of your principals assumes a role in another organization.
</Note>

| Field | Type | Description |
| --- | --- | --- |
| `name` | string | Name of the SCP. |
| `display_name` | string | Name shown in the console. |
| `description` | string | Optional. |
| `policy_document` | object | Policy document. Its Deny statements are applied. |
| `priority` | integer | Order in which SCPs are read, lowest first. Default `100`. |
| `is_enabled` | boolean | Only enabled SCPs are applied. |

The response adds `id` (`scp_...`), `organization_id`, `created_by_user_id`, `created_at` and `updated_at`.

```bash
curl -X POST https://api.hexelstudio.com/v1/scps \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "no-workspace-delete",
    "display_name": "No workspace deletion",
    "priority": 10,
    "policy_document": {
      "Version": "2026-02-17",
      "Statement": [
        {"Effect": "Deny", "Action": "workspace:workspace:delete", "Resource": "*"}
      ]
    }
  }'
```

| Call | Permission | Response |
| --- | --- | --- |
| `POST /v1/scps` | `iam:scp:write` | `201` with the SCP |
| `GET /v1/scps` | `iam:scp:read` | Array of your organization's SCPs and of SCP templates provided by Hexel Studio |
| `GET /v1/scps/{scp_id}` | `iam:scp:read` | The SCP, or `404` |
| `PATCH /v1/scps/{scp_id}` | `iam:scp:write` | The SCP |
| `DELETE /v1/scps/{scp_id}` | `iam:scp:write` | `204 No Content` |

Templates provided by Hexel Studio can be read but not changed (`PATCH` and `DELETE` answer `404`), and they do not apply to your organization.

## Resource control policies

A resource control policy (RCP) works like an SCP, scoped to one resource type. When a request names a `resource_type` and an enabled RCP for that type has a matching Deny statement, the full evaluator denies the action with reason `rcp_denied`. A request that names no resource type is never checked against RCPs.

The fields are the same as for an SCP, plus:

| Field | Type | Description |
| --- | --- | --- |
| `resource_type` | string | The resource type the RCP applies to. It must equal the `resource_type` of the request. |

```bash
curl -X POST https://api.hexelstudio.com/v1/rcps \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "protect-workspaces",
    "display_name": "Protect workspaces",
    "resource_type": "workspace",
    "policy_document": {
      "Version": "2026-02-17",
      "Statement": [
        {"Effect": "Deny", "Action": "workspace:workspace:delete", "Resource": "*"}
      ]
    }
  }'
```

| Call | Permission | Response |
| --- | --- | --- |
| `POST /v1/rcps` | `iam:rcp:write` | `201` with the RCP (`id` starts with `rcp_`) |
| `GET /v1/rcps` | `iam:rcp:read` | Array of your organization's **enabled** RCPs |
| `GET /v1/rcps/{rcp_id}` | `iam:rcp:read` | The RCP, enabled or not, or `404` |
| `PATCH /v1/rcps/{rcp_id}` | `iam:rcp:write` | The RCP |
| `DELETE /v1/rcps/{rcp_id}` | `iam:rcp:write` | `204 No Content` |

A disabled RCP does not appear in the list. Keep its ID if you plan to enable it again with `PATCH`.

## Deny statements in policies

A policy statement with `"Effect": "Deny"` is stored and validated like any other statement, but the cached permission check that decides your API calls reads only Allow statements. A Deny statement therefore does not stop a call that another policy allows. In detail:

- Deny statements in policies attached to members, service users and groups, and in permission sets, are applied only by the full evaluator (reason `policy_denied`).
- Deny statements in a role's policies are ignored for that role's sessions.
- Deny statements in a permission boundary are ignored everywhere.

To take an action away from someone today, remove the Allow that grants it: detach the policy, edit it, or remove the member from the group. For a role, set a permission boundary as its `boundary_id`. See [Write policies](https://hexelstudio.com/docs/identity/write-policies).

## Check what the full evaluator would decide

`POST /v1/authz/explain` shows each step of the full evaluator, including the SCP, RCP, boundary and Deny steps, for a principal and action. Remember that the answer describes the full evaluator, not your normal API calls. See [Test access](https://hexelstudio.com/docs/identity/test-access).
