Skip to content
Hexel StudioDocs

Roles and assuming a role

Create roles, control who can assume them with trust policies, get temporary role sessions, work across organizations, and revoke sessions.

On this page

A role is an identity with its own policies and no credentials of its own. A member, a service user or another role assumes the role and gets a role session: a short-lived access token that has the role's permissions, and only those, for up to 12 hours.

Use roles to:

  • Give temporary, elevated access instead of standing permissions.
  • Confine a workload to one workspace or environment.
  • Let a principal of another organization act in yours, with both organizations agreeing.

Three things decide what a role does:

PartDecidesSet with
Policies attached to the roleWhat a session may doPOST /v1/policies/{policy_id}/attachments
Trust policyWho may assume the rolePUT /v1/roles/{role_id}/trust-policy
Scope and maximum session durationWhere and for how longPOST /v1/roles

Permissions

OperationAction
Create, update, disable or delete a role; revoke its sessionsiam:role:write
Set a trust policy (or create a role with one)iam:role:write_trust
Read a trust policy, list a role's policies, simulateiam:role:read
Attach a policy to a roleiam:policy:write
List or read rolesAny caller authenticated in the organization

Assuming a role in your own organization needs no permission of your own: the role's trust policy decides. Cross-organization assumption also needs sts:role:assume in your own organization.

Create a role

Bash
curl -X POST https://api.hexelstudio.com/v1/roles \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "prod-deployer",
    "display_name": "Production deployer",
    "description": "Deploys to the production environment",
    "scope": "environment",
    "workspace_id": "'"$WORKSPACE_ID"'",
    "environment_id": "'"$ENVIRONMENT_ID"'",
    "max_session_duration": 3600
  }'
namestringbodyrequired

3 to 100 characters.

display_namestringbodyrequired

3 to 200 characters.

descriptionstringbody

Up to 500 characters.

scopestringbodyrequired

organization, workspace or environment. It limits where a session's permissions apply. See Using a role session.

workspace_idstringbody

Required for workspace and environment roles. Not allowed for organization roles.

environment_idstringbody

Required for environment roles, together with workspace_id. Not allowed otherwise.

max_session_durationintegerbodydefault: 3600

The longest session the role can issue, in seconds: 900 (15 minutes) to 43200 (12 hours).

trust_policyobjectbody

Optional trust policy. Needs iam:role:write_trust. Without one, nobody can assume the role until you set it.

boundary_idstringbody

Optional permission boundary that caps the role's sessions. See Guardrails (Preview).

The response is 201 with the role:

JSON
{
  "id": "role_5b4a3c2d1e0f9a8b7c6d5e4f3a2b1c0d",
  "name": "prod-deployer",
  "display_name": "Production deployer",
  "description": "Deploys to the production environment",
  "scope": "environment",
  "is_system": false,
  "organization_id": "<org-id>",
  "created_by_user_id": "<user-id>",
  "created_at": "2026-10-20T10:00:00Z",
  "updated_at": "2026-10-20T10:00:00Z",
  "role_type": "customer",
  "trust_policy": {"Version": "2026-02-17", "Statement": []},
  "max_session_duration": 3600,
  "is_enabled": true,
  "workspace_id": "<workspace-id>",
  "environment_id": "<environment-id>"
}

List roles with GET /v1/roles (optionally ?scope=workspace), and read one with GET /v1/roles/{role_id}. The list also includes roles that Hexel Studio manages, including roles it creates in your organization for its own services. Those are read-only: you can't update, delete or revoke them.

Update a role

PATCH /v1/roles/{role_id} changes display_name, description, max_session_duration, is_enabled and boundary_id. The name, scope, workspace and environment can't change. Change the trust policy with PUT .../trust-policy.

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

Attach policies

A session has exactly the permissions of the policies attached to the role. Attach one with the policy attachments endpoint and target_type ROLE:

Bash
curl -X POST https://api.hexelstudio.com/v1/policies/<policy-id>/attachments \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"target_type": "ROLE", "target_id": "<role-id>"}'

As with any attachment, you can grant only actions you hold yourself. List a role's attachments with GET /v1/roles/{role_id}/policies. See Write and attach policies.

Trust policies

A trust policy is a JSON document on the role that says who may assume it. It is separate from the policies that say what a session may do.

JSON
{
  "Version": "2026-02-17",
  "Statement": [
    {
      "Sid": "DeployersAndCI",
      "Effect": "Allow",
      "Action": "sts:role:assume",
      "Principal": {
        "Group": ["<group-id>"],
        "ServiceUser": ["<service-user-id>"]
      }
    },
    {
      "Sid": "NoBots",
      "Effect": "Deny",
      "Action": "sts:role:assume",
      "Principal": {"Group": ["<group-id>"]},
      "Condition": {"StringLike": {"ciam:session_name": ["bot-*"]}}
    }
  ]
}
FieldValue
VersionOptional. Use 2026-02-17.
StatementUp to 20 statements. The whole document is at most 10 KB.
SidOptional label, returned by simulate and recorded in the audit log.
EffectAllow or Deny.
Actionsts:role:assume, as a string or an array.
PrincipalWho the statement applies to. Required.
ConditionOptional conditions. See below.

Principals

KeyMatches
UserThese user IDs.
GroupMembers of these groups. Groups must be in the role's organization.
ServiceUserThese service user IDs.
RoleSessions of these roles, for role chaining.
OrgAny user, service user or role session whose token is for one of these organization IDs. An Allow with Org must have at least one condition.

Values are exact IDs. Wildcards such as * are refused. Federated principals (workload identity providers) are not usable yet: see Workload identity (Preview).

Conditions

A condition block maps an operator to condition keys and allowed values:

JSON
"Condition": {
  "StringEquals": {"ciam:principal_type": ["service_user"]},
  "StringLike": {"ciam:session_name": ["deploy-*"]}
}
KeyValue
ciam:principal_typeThe caller's type: user, service_user or role.
ciam:session_nameThe session name requested.
ciam:source_typeWho originally authenticated: user or service_user. Stays the same through role chaining.
ciam:source_idThe ID of that original user or service user.
ciam:source_orgThat identity's organization.
OperatorHolds when
StringEquals, StringLikeThe key is present and its value matches one of the values.
StringNotEquals, StringNotLikeThe key is absent, or its value matches none of the values.
BoolThe key is present and equals "true" or "false" as given.
ForAnyValue:StringEquals, ForAnyValue:StringLikeAt least one of the key's values matches.

In ...Like operators, * matches any run of characters and ? matches one character. All operators and keys in a statement must hold (AND). Values in one list are alternatives (OR).

How a trust policy is evaluated

  1. Only statements whose Action and Principal match the caller count.
  2. If a matching Deny statement's conditions hold, assumption is refused.
  3. Otherwise a matching Allow statement whose conditions hold is required.
  4. Anything else is refused.

The role must also be enabled, and the caller must be active: a suspended member, a disabled service user, or a revoked or disabled role session can't assume a role.

Set a trust policy

PUT /v1/roles/{role_id}/trust-policy replaces the whole document. Read it with GET on the same path.

Bash
curl -X PUT https://api.hexelstudio.com/v1/roles/<role-id>/trust-policy \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d @trust-policy.json

The response is the updated role. An invalid document is refused with 400 and every problem found:

JSON
{
  "error": "invalid trust policy",
  "problems": [
    "statement[0]: Principal values must be explicit IDs; wildcards are not allowed",
    "statement[1]: unknown condition key \"ciam:email\""
  ]
}

Assume a role

To assume a role, exchange an access token you already hold at the token service, using OAuth 2.0 token exchange (RFC 8693). The token can be a member's token, a service user's token or a role session.

curl -X POST https://sts.hexelstudio.com/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
    "subject_token": "'"$ACCESS_TOKEN"'",
    "subject_token_type": "urn:ietf:params:oauth:token-type:access_token",
    "resource": "<role-id>",
    "session_name": "deploy-1234",
    "duration_seconds": 1800
  }'
grant_typestringbodyrequired

urn:ietf:params:oauth:grant-type:token-exchange.

subject_tokenstringbodyrequired

Your current Hexel Studio access token. It must be for an organization.

subject_token_typestringbodyrequired

urn:ietf:params:oauth:token-type:access_token.

resourcestringbodyrequired

The ID of the role to assume (role_...).

session_namestringbody

A name for the session. See Session names.

duration_secondsintegerbody

How long the session should last. See Session duration.

The body can also be form-encoded (Content-Type: application/x-www-form-urlencoded). The response is:

JSON
{
  "access_token": "eyJhbGciOiJSUzI1NiIs...",
  "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
  "token_type": "Bearer",
  "expires_in": 1800
}

There is no refresh token. When the session ends, assume the role again.

A service user can skip the separate exchange: add resource, and optionally session_name and duration_seconds, to its API key or client credentials request. See Get a role session directly. The response is the same.

StatuserrorCause
400invalid_requestsubject_token or resource is missing, or session_name is invalid.
400unsupported_token_typesubject_token_type is not the access token type.
400invalid_targetresource is not a role ID.
401invalid_grantThe subject token is invalid, expired, revoked, or not for an organization.
403access_deniedThe trust policy or another check refused the assumption.
429rate_limitedToo many tokens for this caller. See Token endpoint limits.
503temporarily_unavailableRetry with backoff.

A 403 doesn't say why. Use simulate to find out, or look for role.assume_denied in the audit log.

Session duration

  • Without duration_seconds, the session lasts one hour, or the role's max_session_duration if that is shorter.
  • With duration_seconds, you get that duration, capped at the role's max_session_duration.
  • expires_in in the response is the duration you got.

Session names

A session name is 2 to 64 characters from A-Z a-z 0-9 _ = , . @ -. It is recorded in the audit log and available to trust policy conditions as ciam:session_name. Use it to tell sessions apart, for example a CI run ID. Without one, a name is derived from your ID.

Using a role session

Send the session as a bearer token:

Bash
curl https://api.hexelstudio.com/v1/workspaces \
  -H "Authorization: Bearer $ROLE_TOKEN"
  • The session is for the role's organization. It has only the permissions of the policies attached to the role, not those of whoever assumed it.
  • A workspace role's permissions apply only to requests for its workspace: send X-Workspace-Id. An environment role's apply only to its environment: send X-Workspace-Id and X-Environment-Id. See Headers.
  • The session stops working when it expires, when its sessions are revoked, when the role is disabled or deleted, or when the member or service user that started it is suspended or disabled.

Role chaining

A role session can assume another role of the same organization. The second role's trust policy must name the first role under Principal.Role:

JSON
{
  "Effect": "Allow",
  "Action": "sts:role:assume",
  "Principal": {"Role": ["<first-role-id>"]}
}
  • The new session can't outlast the current one. At least 60 seconds must be left on the current session.
  • The original identity carries through. ciam:source_type, ciam:source_id and ciam:source_org still name the member or service user who started the chain.
  • A role session can't assume a role in another organization.

Cross-organization access

A user or service user of organization A can assume a role in organization B. Both organizations must agree:

  1. Organization B trusts the caller

    The role's trust policy in B names the caller under User or ServiceUser, or names organization A under Org with at least one condition:

    JSON
    {
      "Sid": "PartnerCI",
      "Effect": "Allow",
      "Action": "sts:role:assume",
      "Principal": {"Org": ["<org-a-id>"]},
      "Condition": {"StringEquals": {"ciam:source_id": ["<service-user-id>"]}}
    }
  2. Organization A allows the caller to assume roles

    A policy in organization A, attached to the caller directly or through a group, allows sts:role:assume:

    JSON
    {
      "Version": "2026-02-17",
      "Statement": [{"Effect": "Allow", "Action": ["sts:role:assume"]}]
    }
  3. The caller assumes the role

    The caller exchanges its own token for organization A, with resource set to the role in B. The session it gets is for organization B.

If either side is missing, the request fails with 403.

Test a trust policy

POST /v1/roles/{role_id}/trust-policy/simulate runs the same decision as a real assumption, without issuing a session or changing anything. Pass policy to test a draft before you save it.

Bash
curl -X POST https://api.hexelstudio.com/v1/roles/<role-id>/trust-policy/simulate \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "caller": {"principal_type": "service_user", "id": "<service-user-id>"},
    "session_name": "deploy-1234"
  }'
callerobjectbodyrequired

principal_type (user, service_user or role) and id. The caller must belong to the role's organization. A role caller is simulated as a fresh one-hour session.

session_namestringbody

The session name to test.

policyobjectbody

A draft trust policy to test instead of the saved one. It is validated like PUT .../trust-policy.

JSON
{
  "allowed": true,
  "reason": "trust_allow",
  "role_id": "<role-id>",
  "organization_id": "<org-id>",
  "scope": {"workspace_id": "<workspace-id>", "environment_id": "<environment-id>"},
  "duration": 3600,
  "matched_statement": "DeployersAndCI",
  "source": {"type": "service_user", "id": "<service-user-id>", "org": "<org-id>"}
}

When allowed is false, reason says why:

reasonMeaning
trust_no_matchNo Allow statement matched the caller and its conditions.
trust_explicit_denyA Deny statement matched. matched_statement names it.
trust_condition_errorA Deny statement's conditions couldn't be evaluated, so it refuses.
trust_policy_invalidThe saved trust policy can't be read.
role_disabledThe role is disabled.
caller_inactiveThe caller is suspended, disabled or no longer exists.
caller_not_in_orgThe caller is not an active member of an active organization.
caller_revokedThe caller is a role session that was revoked.
source_inactiveThe member or service user that started the chain is no longer active.
caller_session_expiringThe calling role session has less than 60 seconds left.
caller_not_permittedCross-organization: the caller's own organization doesn't allow sts:role:assume.
cross_org_chaining_not_supportedA role session tried to assume a role in another organization.

Simulation can't test callers from another organization (400).

Revoke sessions

To end every current session of a role, for example after a leaked token:

Bash
curl -X POST https://api.hexelstudio.com/v1/roles/<role-id>/revoke-sessions \
  -H "Authorization: Bearer $ACCESS_TOKEN"
JSON
{"role_id": "<role-id>", "not_before": 1792490400}

Sessions issued before not_before (Unix seconds) are refused within 30 seconds, and can't be used to chain into other roles. The role stays enabled: new sessions can be assumed right away. To stop new sessions too, disable the role or change its trust policy.

Disable or delete a role

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

Disabling a role refuses new assumptions and revokes its current sessions. Enabling it again ({"is_enabled": true}) allows new sessions. Sessions revoked by the disable stay revoked.

DELETE /v1/roles/{role_id} revokes the role's sessions, removes its policy attachments and deletes it. The response is 204.