Skip to content
Hexel StudioDocs

Test access

Ask Hexel Studio whether a member or service user may perform an action, see each step of the decision, check several actions at once, and validate policy documents.

On this page

The /v1/authz endpoints answer "may this principal do this?" without making the call. Use them to check a policy before you attach it widely, or to find out why a call was denied.

EndpointAnswers
POST /v1/authz/evaluateAllowed or denied for one action, with the reason
POST /v1/authz/explainThe same decision, step by step
POST /v1/authz/simulateAllowed or denied for a list of actions
GET /v1/authz/expandSome of the principals that have access to a resource
POST /v1/authz/validate-policyWhether a policy document is well formed

Every endpoint needs iam:authz:simulate. Owners and admins hold it by default.

How these results differ from real calls

These endpoints use the full evaluator. Your normal API calls use the cached permission check described in Policies. The two agree on what is allowed by policies, groups and permission sets, but the full evaluator also applies features that don't affect real calls yet:

The full evaluator appliesEffect on these resultsEffect on real calls
Deny statements in policiesCan denyNone
Service control policies, resource control policies, permission boundaries on members and service usersCan denyNone. See Guardrails (Preview).
Session policies, when you send session_idCan denyNone. See Session policies (Preview).
ABAC rules (evaluate and simulate only)Can allow or denyNone. See Fine-grained access (Preview).

So a result of policy_denied, scp_denied, rcp_denied, boundary_exceeded, session_policy_denied or abac_denied describes what Hexel Studio will enforce later, not what happens today: the real call may succeed.

There are other differences to keep in mind:

  • Workspace and environment. These endpoints don't check that workspace_id and environment_id exist in your organization. A real call that names a workspace or environment that isn't active in your organization is denied.
  • Actions checked at organization level. Some actions, such as workspace:workspace:create, are always checked at organization level by the real API. Test them without workspace_id.
  • Roles. You can test members and service users, not roles. A role ID as subject_id returns user_suspended.
  • Caching. evaluate and simulate results can be up to 30 seconds old. explain is never cached.

When the subject has no Deny statements and none of the Preview features apply, and you name the same scope as the real call, the answer matches what the real call gets.

Evaluate one action

curl -X POST https://api.hexelstudio.com/v1/authz/evaluate \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "subject_id": "<user-id>",
    "organization_id": "'"$ORG_ID"'",
    "action": "env:env:create",
    "workspace_id": "'"$WORKSPACE_ID"'"
  }'
subject_idstringbody

The user ID of a member of your organization, or the ID of one of its service users (su_...). Leave it out to test yourself. A person or service user outside your organization returns 404.

organization_idstringbody

Optional. The test always runs in your token's organization; if you send this field, it must be that organization's ID (403 otherwise).

actionstringbodyrequired

The action to test, for example workspace:workspace:delete. See the actions reference.

workspace_idstringbody

The workspace the call would be made in. Grants placed on this workspace count.

environment_idstringbody

The environment the call would be made in. Send workspace_id with it.

resource_typestringbody

A resource type. Only resource control policies (Preview) read it.

resource_idstringbody

A resource ID. Only resource permissions (Preview) read it.

session_idstringbody

An access token's session ID (sid claim), to apply that session's session policies (Preview).

ipstringbody

Request context for ABAC rules (Preview).

user_agentstringbody

Request context for ABAC rules (Preview).

JSON
{
  "allowed": true,
  "reason": "policy_allow",
  "cacheable": true,
  "principal": "<user-id>",
  "action": "env:env:create",
  "resource_type": "",
  "workspace_id": "ws_2c3d4e...",
  "environment_id": "",
  "evaluation_ms": 14
}
allowedboolean

The decision.

reasonstring

Why. See Reasons.

cacheableboolean

Whether Hexel Studio may reuse this result for the same request for up to 30 seconds.

principalstring

The subject that was evaluated.

evaluation_msinteger

Time taken, in milliseconds. 0 when the result came from the cache.

action, resource_type, resource_id, workspace_id and environment_id echo the request.

Reasons

The steps run in this order, and the first one that decides ends the evaluation:

OrderReasonallowedMeaning
1user_suspendedfalseThe subject is not an active account or service user. Also returned for unknown IDs and roles.
2org_suspendedfalseThe organization is not active, or the member's membership is not active.
3scp_deniedfalseA service control policy denies the action (Preview).
4rcp_deniedfalseA resource control policy for resource_type denies the action (Preview).
5boundary_exceededfalseThe subject's permission boundary doesn't allow the action (Preview).
6session_policy_deniedfalseThe session's session policies don't allow the action (Preview). session_policy_unavailable means they couldn't be read.
7policy_deniedfalseA Deny statement in a policy that applies to the subject matches. Not enforced on real calls yet.
9policy_allowtrueAn Allow statement matches. This is what real calls check.
10resource_permissiontrueA resource permission on resource_id allows it (Preview).
11abac_allow, abac_deniedtrue, falseAn ABAC rule decides (Preview).
12default_denyfalseNothing allows the action. A real call is denied too.

Explain a decision

POST /v1/authz/explain takes the same body as evaluate and returns every step it ran. Use it to see which step decided.

Bash
curl -X POST https://api.hexelstudio.com/v1/authz/explain \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "subject_id": "<user-id>",
    "organization_id": "'"$ORG_ID"'",
    "action": "iam:policy:write"
  }'
JSON
{
  "allowed": false,
  "reason": "default_deny",
  "steps": [
    {"step": "principal", "result": "OK", "details": "uid=<user-id> org=org_9a8b7c..."},
    {"step": "user_status", "result": "OK", "details": "ACTIVE"},
    {"step": "org_status", "result": "OK", "details": "ACTIVE"},
    {"step": "scp_check", "result": "OK", "details": "no SCP blocks this action"},
    {"step": "rcp_check", "result": "SKIP", "details": "no resource_type provided"},
    {"step": "boundary_check", "result": "SKIP", "details": "no boundary attached"},
    {"step": "session_policy", "result": "SKIP", "details": "no session"},
    {"step": "explicit_deny", "result": "OK", "details": "no explicit deny"},
    {"step": "policy_allow", "result": "NO_MATCH", "details": "no matching Allow policy"},
    {"step": "default", "result": "DENY", "details": "no policy grants access"}
  ]
}

Each step has a step name, a result (OK, SKIP, ALLOW, DENY, NO_MATCH or FAIL) and details. The reason values are the ones in Reasons. Other step names can appear; they don't change the decision for your organization.

explain does not run ABAC rules, so it can return default_deny where evaluate returns abac_allow.

Simulate several actions

POST /v1/authz/simulate evaluates a list of actions for one subject in one scope. Each action is evaluated as evaluate would.

Bash
curl -X POST https://api.hexelstudio.com/v1/authz/simulate \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "principal_id": "su_4f5a6b...",
    "organization_id": "'"$ORG_ID"'",
    "actions": ["workspace:workspace:read", "env:env:create", "iam:policy:write"]
  }'
principal_idstringbody

The user ID of a member of your organization, or the ID of one of its service users. Leave it out to test yourself. Note the name: principal_id, not subject_id.

organization_idstringbody

Optional. If you send it, it must be your token's organization (403 otherwise).

actionsstring[]bodyrequired

The actions to test.

workspace_idstringbody

As for evaluate.

environment_idstringbody

As for evaluate.

resource_typestringbody

As for evaluate.

resource_idstringbody

As for evaluate.

session_idstringbody

As for evaluate.

JSON
{
  "results": [
    {"action": "workspace:workspace:read", "allowed": true, "reason": "policy_allow"},
    {"action": "env:env:create", "allowed": true, "reason": "policy_allow"},
    {"action": "iam:policy:write", "allowed": false, "reason": "default_deny"}
  ]
}

List principals with direct grants

GET /v1/authz/expand returns IDs of principals in your organization that have access through two specific kinds of grant:

  • users with a permission set assigned directly to them, whatever the set and its scope, and
  • principals with a resource permission on the given resource (Preview).
Bash
curl "https://api.hexelstudio.com/v1/authz/expand?resource_type=workspace&resource_id=$WORKSPACE_ID" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
resource_typestringqueryrequired

The resource type. Missing: 400 with {"error": "resource_type required"}.

resource_idstringquery

The resource ID.

JSON
{"principals": ["<user-id>"]}

Validate a policy document

POST /v1/authz/validate-policy checks that a policy document is well formed. Send the document as a JSON string in document:

Bash
curl -X POST https://api.hexelstudio.com/v1/authz/validate-policy \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"document": "{\"Version\":\"2026-02-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Action\":\"env:env:read\",\"Resource\":\"*\"}]}"}'
JSON
{"valid": true, "errors": null}

When the document is not valid, valid is false and errors lists every problem, for example "statement[0]: Effect must be 'Allow' or 'Deny'". Sending document as a JSON object instead of a string returns 400. See Validate a policy for what is checked.

Errors

StatusBodyCause
400{"error": "..."}The body is not valid JSON or a field has the wrong type.
400{"error": "sid is not supported; name the principal with subject_id"}The body has a sid field. Use subject_id, and session_id for session policies.
400{"error": "subject_id is required"}You are calling with a role session and sent no subject.
403{"error": "forbidden"}You don't hold iam:authz:simulate.
403{"error": "organization_id must be the token's organization"}organization_id names another organization.
404{"error": "subject not found in this organization"}The subject is not a member or service user of your organization.
503{"error": "membership lookup failed"}Membership couldn't be checked. Try again.