Skip to content
Hexel StudioDocs

Fine-grained access

Policy Resource and Condition, resource policies, relationship tuples, ABAC attributes and policies, tags and namespaces, and what each one does today.

On this page

What takes effect today

FeatureYour normal API callsFull evaluator (/v1/authz/*)
Resource in a policy statementIgnoredIgnored
Condition in a policy statementIgnoredIgnored
Resource policiesNo effectNo effect
Relationship tuples and namespacesNo effectNo effect
ABAC policies and attributesNo effectApplied after Allow policies (see ABAC)
TagsNo effectNo effect

The two decision paths are explained in What Preview means.

Resource and Condition in policies

A policy statement can have a Resource and a Condition. Hexel Studio stores both and returns them, but does not match either of them in any decision. POST /v1/authz/validate-policy reports a statement without Resource as a problem, and session policies require it, but neither field changes what a statement allows.

Use "Resource": "*" so that your policies say what they do. To limit access to one workspace or environment today, assign a permission set at that scope (see Write policies), or use a role scoped to that workspace or environment: its sessions can act only there (see Roles).

Trust policies are different: their Condition blocks are evaluated. See Roles and Workload identity.

Resource policies

A resource policy is a policy document attached to one resource, identified by a resource type and ID. Your organization can have one resource policy per resource. Resource policies are stored only.

FieldTypeDescription
resource_typestringType of the resource, for example workspace.
resource_idstringID of the resource.
policy_documentobjectPolicy document.
is_enabledbooleanDefault true.

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

You must already hold, across your organization, every action that the document's Allow statements name. Otherwise the call answers 403 with missing_actions, as when you attach a policy.

Bash
curl -X POST https://api.hexelstudio.com/v1/resource-policies \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "resource_type": "workspace",
    "resource_id": "<workspace-id>",
    "policy_document": {
      "Version": "2026-02-17",
      "Statement": [
        {"Effect": "Allow", "Action": "workspace:workspace:read", "Resource": "*"}
      ]
    }
  }'
CallPermissionResponse
POST /v1/resource-policiesiam:policy:write201 with the resource policy
GET /v1/resource-policies?resource_type=...&resource_id=...iam:policy:writeThe resource policy of that resource, or 404
PATCH /v1/resource-policies/{resource_policy_id}iam:policy:writeThe resource policy. A new policy_document passes the same held-actions check.
DELETE /v1/resource-policies/{resource_policy_id}iam:policy:write204 No Content

Reading a resource policy needs iam:policy:write, not iam:policy:read.

Relationship tuples and namespaces

Relationship tuples record facts such as "user X is an editor of document Y". A namespace names a kind of object and defines how its relations inherit from each other. A tuple links a subject to an object through a relation. You can store tuples and ask whether a relation holds, but no access decision reads them: they do not grant or deny any API call.

Namespaces

FieldTypeDescription
namestringRequired. Unique in your organization, for example document.
relationsstringThe relation schema, as a JSON string. Default "{}".

The schema maps each relation to its definition. A relation with "union" also holds for every subject that has one of the listed relations:

JSON
{"owner": {}, "editor": {"union": ["owner"]}, "viewer": {"union": ["editor"]}}

relations is a string, so encode the schema:

Bash
curl -X POST https://api.hexelstudio.com/v1/namespaces \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "document",
    "relations": "{\"owner\": {}, \"editor\": {\"union\": [\"owner\"]}, \"viewer\": {\"union\": [\"editor\"]}}"
  }'
CallPermissionResponse
POST /v1/namespacesiam:relation:write201 with the namespace (id starts with ns_), 409 if the name is taken
GET /v1/namespacesiam:relation:readArray of your organization's namespaces
GET /v1/namespaces/{namespace_id}iam:relation:readThe namespace, or 404
PATCH /v1/namespaces/{namespace_id}iam:relation:writeThe namespace. Only name and relations change. Renaming moves your tuples to the new name.
DELETE /v1/namespaces/{namespace_id}iam:relation:write204 No Content

Tuples

FieldTypeDescription
namespacestringRequired. Name of a namespace of your organization, otherwise 404.
object_idstringRequired. The object, for example a document ID.
relationstringRequired. For example editor.
subject_typestringRequired. For example user, or group to give the relation to a group's members.
subject_idstringRequired. The user or group ID.
subject_relationstringOptional. Stored only.
Bash
curl -X POST https://api.hexelstudio.com/v1/relations \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "namespace": "document",
    "object_id": "doc-42",
    "relation": "editor",
    "subject_type": "user",
    "subject_id": "<user-id>"
  }'

The response is 201 Created with the tuple (id starts with rt_, plus organization_id and created_at).

To delete a tuple, send the same five required fields to DELETE /v1/relations:

Bash
curl -X DELETE https://api.hexelstudio.com/v1/relations \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"namespace": "document", "object_id": "doc-42", "relation": "editor", "subject_type": "user", "subject_id": "<user-id>"}'

Check a relation

POST /v1/relations/check answers whether a subject has a relation to an object in your organization.

Bash
curl -X POST https://api.hexelstudio.com/v1/relations/check \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"namespace": "document", "object_id": "doc-42", "relation": "viewer", "subject_type": "user", "subject_id": "<user-id>"}'
JSON
{"allowed": true}

The relation holds when one of these is true:

  1. A tuple matches all five fields exactly.
  2. A tuple gives the relation on the object to a group (subject type group, in lower case) of your organization, and the subject ID is a member of that group.
  3. The namespace schema lists, under union, another relation that holds by these same rules.

allowed is only an answer to your question. Your application can use it, but Hexel Studio does not use it to decide any API call.

Read and look up tuples

CallResponse
GET /v1/relations?namespace=&object_id=&relation=Your organization's tuples, filtered by any of the three parameters
GET /v1/relations/lookup-subjects?namespace=&object_id=&relation=Tuples that give the relation on that object
GET /v1/relations/lookup-objects?namespace=&relation=&subject_type=&subject_id=Tuples that give that subject the relation

Lookups return stored tuples only. They do not follow groups or union.

Writing, deleting, reading, checking and looking up tuples all need iam:relation:write.

ABAC attributes and policies

Attribute-based access control (ABAC) lets you attach key-value attributes to members, service users and groups, and write rules that allow or deny actions when the attributes match.

ABAC rules are applied only by the full evaluator, and only at the end: when no policy allows the action, the first enabled ABAC policy that matches decides. In practice:

  • An ABAC ALLOW policy can turn a "denied" answer of /v1/authz/evaluate or /v1/authz/simulate into "allowed" (reason abac_allow). It does not allow your real API calls. The only other place the full evaluator runs is the check on your organization when one of your principals assumes a role in another organization.
  • An ABAC DENY policy cannot take away an action that a policy allows. It only changes the reason of a denial to abac_denied.
  • /v1/authz/explain stops before the ABAC step, so it can answer "denied" where evaluate answers "allowed".
  • Attributes on roles affect nothing.

Attributes

FieldTypeDescription
entity_typestringRequired. USER, SERVICE_USER, GROUP or ROLE. Use upper case.
entity_idstringRequired. A service user, group or role of your organization, or a user ID.
attribute_keystringRequired. For example department.
attribute_valuestringThe value, for example finance.
value_typestringstring (default), number, boolean or json. Stored only: values are compared as text.

Setting a key that the entity already has in your organization replaces its value. Attributes belong to your organization: other organizations' rules never see them.

Bash
curl -X POST https://api.hexelstudio.com/v1/abac/attributes \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"entity_type": "GROUP", "entity_id": "<group-id>", "attribute_key": "department", "attribute_value": "finance"}'
CallPermissionResponse
POST /v1/abac/attributesiam:abac:write201 with the attribute (id starts with attr_). 404 if a service user, group or role is not in your organization.
GET /v1/abac/attributes?entity_type=...&entity_id=...iam:abac:writeArray of the entity's attributes in your organization. entity_type must match the case you stored.
DELETE /v1/abac/attributes/{attribute_id}iam:abac:write204, or 404

Policies

FieldTypeDescription
namestringName of the policy.
display_namestringName shown in the console.
descriptionstringOptional.
effectstringRequired. ALLOW or DENY (any case).
actionsarray of stringsRequired. At least one action pattern, such as workspace:*.
resourcesarray of stringsResource types the policy applies to. Use ["*"] for all. Only checked when the request names a resource_type.
conditionsobjectAttribute keys and the values they must have, for example {"department": "finance"}. All must match. Send {} for no conditions.
priorityintegerLower numbers are checked first. Default 0.
is_enabledbooleanDefault true.

A condition holds when the principal's own attribute with that key has that value. If the principal has no attribute with that key, it holds when one of the principal's groups has the key with that value.

An ALLOW policy can name only actions you already hold across your organization, otherwise 403 with missing_actions.

Bash
curl -X POST https://api.hexelstudio.com/v1/abac/policies \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "finance-read",
    "display_name": "Finance can read workspaces",
    "effect": "ALLOW",
    "actions": ["workspace:workspace:read"],
    "resources": ["*"],
    "conditions": {"department": "finance"},
    "priority": 10
  }'
CallPermissionResponse
POST /v1/abac/policiesiam:abac:write201 with the policy
GET /v1/abac/policiesiam:abac:readArray of your organization's ABAC policies and of templates provided by Hexel Studio. Templates do not apply to your organization.
GET /v1/abac/policies/{policy_id}iam:abac:readThe policy, or 404
PATCH /v1/abac/policies/{policy_id}iam:abac:writeThe policy. A changed ALLOW policy passes the held-actions check again.
DELETE /v1/abac/policies/{policy_id}iam:abac:write204 No Content

Tags

Tags are key-value labels on a resource, identified by a resource type and ID. They are stored only: no decision reads them. Tag keys are unique per resource: adding a key that exists replaces its value.

FieldTypeDescription
resource_typestringFor example role, policy, group or service_user. Not checked.
resource_idstringID of the resource. Not checked.
keystringTag key.
valuestringTag value.
Bash
curl -X POST https://api.hexelstudio.com/v1/tags \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"resource_type": "role", "resource_id": "<role-id>", "key": "team", "value": "payments"}'
CallPermissionResponse
POST /v1/tagsiam:relation:write201 with the tag (id starts with tag_)
GET /v1/tags?resource_type=...&resource_id=...iam:relation:readArray of the resource's tags in your organization
DELETE /v1/tags/{key}?resource_type=...&resource_id=...iam:relation:write204, or 404