Guardrails
Permission boundaries, service control policies and resource control policies, and what each one enforces today.
On this page
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. 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:
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_typeandtarget_iddo 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. - 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 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. |
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:
{
"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.
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:
{
"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:
{"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.
| 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.
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. |
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.
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.