Skip to content
Hexel StudioDocs

Guardrails

Permission boundaries, service control policies and resource control policies, and what each one enforces today.

On this page

What takes effect today

GuardrailYour normal API callsFull evaluator (/v1/authz/*, cross-organization role assumption)
Boundary named by a role's boundary_idCaps every call made with a session of that roleNot used (the full evaluator does not evaluate role sessions)
Boundary with target_type USER or SERVICE_USERNo effectCaps that member or service user
Boundary with target_type GROUP or ROLENo effectNo effect
Service control policyNo effectDeny statements deny the action
Resource control policyNo effectDeny statements deny the action, when the request names a matching resource_type
Deny statement in an attached policyNo effectDenies 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_typetarget_id must beWhat the boundary caps
USERA member of your organizationThat member, in the full evaluator only
SERVICE_USERA service user of your organizationThat service user, in the full evaluator only
GROUPA group of your organizationNothing
ROLEA role of your organizationNothing 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.
  • 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.

FieldTypeDescription
namestringName of the boundary.
display_namestringName shown in the console.
descriptionstringOptional.
target_typestringRequired. USER, SERVICE_USER, GROUP or ROLE.
target_idstringRequired. ID of the target. It must belong to your organization.
policy_documentobjectThe 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"
}
StatusWhen
400target_type is not one of the four types, target_id is missing, or the target does not belong to your organization.
409The target already has a boundary in your organization.

List, read, update and delete

CallPermissionResponse
GET /v1/boundariesiam:boundary:readArray of your organization's boundaries
GET /v1/boundaries/{boundary_id}iam:boundary:readThe boundary, or 404
PATCH /v1/boundaries/{boundary_id}iam:boundary:writeThe updated boundary
DELETE /v1/boundaries/{boundary_id}iam:boundary:write204 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.

FieldTypeDescription
namestringName of the SCP.
display_namestringName shown in the console.
descriptionstringOptional.
policy_documentobjectPolicy document. Its Deny statements are applied.
priorityintegerOrder in which SCPs are read, lowest first. Default 100.
is_enabledbooleanOnly 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": "*"}
      ]
    }
  }'
CallPermissionResponse
POST /v1/scpsiam:scp:write201 with the SCP
GET /v1/scpsiam:scp:readArray of your organization's SCPs and of SCP templates provided by Hexel Studio
GET /v1/scps/{scp_id}iam:scp:readThe SCP, or 404
PATCH /v1/scps/{scp_id}iam:scp:writeThe SCP
DELETE /v1/scps/{scp_id}iam:scp:write204 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:

FieldTypeDescription
resource_typestringThe 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": "*"}
      ]
    }
  }'
CallPermissionResponse
POST /v1/rcpsiam:rcp:write201 with the RCP (id starts with rcp_)
GET /v1/rcpsiam:rcp:readArray of your organization's enabled RCPs
GET /v1/rcps/{rcp_id}iam:rcp:readThe RCP, enabled or not, or 404
PATCH /v1/rcps/{rcp_id}iam:rcp:writeThe RCP
DELETE /v1/rcps/{rcp_id}iam:rcp:write204 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.