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
| Feature | Your normal API calls | Full evaluator (/v1/authz/*) |
|---|---|---|
Resource in a policy statement | Ignored | Ignored |
Condition in a policy statement | Ignored | Ignored |
| Resource policies | No effect | No effect |
| Relationship tuples and namespaces | No effect | No effect |
| ABAC policies and attributes | No effect | Applied after Allow policies (see ABAC) |
| Tags | No effect | No 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.
| Field | Type | Description |
|---|---|---|
resource_type | string | Type of the resource, for example workspace. |
resource_id | string | ID of the resource. |
policy_document | object | Policy document. |
is_enabled | boolean | Default 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.
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": "*"}
]
}
}'| Call | Permission | Response |
|---|---|---|
POST /v1/resource-policies | iam:policy:write | 201 with the resource policy |
GET /v1/resource-policies?resource_type=...&resource_id=... | iam:policy:write | The resource policy of that resource, or 404 |
PATCH /v1/resource-policies/{resource_policy_id} | iam:policy:write | The resource policy. A new policy_document passes the same held-actions check. |
DELETE /v1/resource-policies/{resource_policy_id} | iam:policy:write | 204 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
| Field | Type | Description |
|---|---|---|
name | string | Required. Unique in your organization, for example document. |
relations | string | The 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:
{"owner": {}, "editor": {"union": ["owner"]}, "viewer": {"union": ["editor"]}}relations is a string, so encode the schema:
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\"]}}"
}'| Call | Permission | Response |
|---|---|---|
POST /v1/namespaces | iam:relation:write | 201 with the namespace (id starts with ns_), 409 if the name is taken |
GET /v1/namespaces | iam:relation:read | Array of your organization's namespaces |
GET /v1/namespaces/{namespace_id} | iam:relation:read | The namespace, or 404 |
PATCH /v1/namespaces/{namespace_id} | iam:relation:write | The namespace. Only name and relations change. Renaming moves your tuples to the new name. |
DELETE /v1/namespaces/{namespace_id} | iam:relation:write | 204 No Content |
Tuples
| Field | Type | Description |
|---|---|---|
namespace | string | Required. Name of a namespace of your organization, otherwise 404. |
object_id | string | Required. The object, for example a document ID. |
relation | string | Required. For example editor. |
subject_type | string | Required. For example user, or group to give the relation to a group's members. |
subject_id | string | Required. The user or group ID. |
subject_relation | string | Optional. Stored only. |
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:
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.
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>"}'{"allowed": true}The relation holds when one of these is true:
- A tuple matches all five fields exactly.
- A tuple gives the relation on the object to a
group(subject typegroup, in lower case) of your organization, and the subject ID is a member of that group. - 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
| Call | Response |
|---|---|
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
ALLOWpolicy can turn a "denied" answer of/v1/authz/evaluateor/v1/authz/simulateinto "allowed" (reasonabac_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
DENYpolicy cannot take away an action that a policy allows. It only changes the reason of a denial toabac_denied. /v1/authz/explainstops before the ABAC step, so it can answer "denied" whereevaluateanswers "allowed".- Attributes on roles affect nothing.
Attributes
| Field | Type | Description |
|---|---|---|
entity_type | string | Required. USER, SERVICE_USER, GROUP or ROLE. Use upper case. |
entity_id | string | Required. A service user, group or role of your organization, or a user ID. |
attribute_key | string | Required. For example department. |
attribute_value | string | The value, for example finance. |
value_type | string | string (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.
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"}'| Call | Permission | Response |
|---|---|---|
POST /v1/abac/attributes | iam:abac:write | 201 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:write | Array of the entity's attributes in your organization. entity_type must match the case you stored. |
DELETE /v1/abac/attributes/{attribute_id} | iam:abac:write | 204, or 404 |
Policies
| Field | Type | Description |
|---|---|---|
name | string | Name of the policy. |
display_name | string | Name shown in the console. |
description | string | Optional. |
effect | string | Required. ALLOW or DENY (any case). |
actions | array of strings | Required. At least one action pattern, such as workspace:*. |
resources | array of strings | Resource types the policy applies to. Use ["*"] for all. Only checked when the request names a resource_type. |
conditions | object | Attribute keys and the values they must have, for example {"department": "finance"}. All must match. Send {} for no conditions. |
priority | integer | Lower numbers are checked first. Default 0. |
is_enabled | boolean | Default 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.
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
}'| Call | Permission | Response |
|---|---|---|
POST /v1/abac/policies | iam:abac:write | 201 with the policy |
GET /v1/abac/policies | iam:abac:read | Array 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:read | The policy, or 404 |
PATCH /v1/abac/policies/{policy_id} | iam:abac:write | The policy. A changed ALLOW policy passes the held-actions check again. |
DELETE /v1/abac/policies/{policy_id} | iam:abac:write | 204 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.
| Field | Type | Description |
|---|---|---|
resource_type | string | For example role, policy, group or service_user. Not checked. |
resource_id | string | ID of the resource. Not checked. |
key | string | Tag key. |
value | string | Tag value. |
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"}'| Call | Permission | Response |
|---|---|---|
POST /v1/tags | iam:relation:write | 201 with the tag (id starts with tag_) |
GET /v1/tags?resource_type=...&resource_id=... | iam:relation:read | Array of the resource's tags in your organization |
DELETE /v1/tags/{key}?resource_type=...&resource_id=... | iam:relation:write | 204, or 404 |