---
title: Fine-grained access
description: Policy Resource and Condition, resource policies, relationship
  tuples, ABAC attributes and policies, tags and namespaces, and what each one
  does today.
url: https://hexelstudio.com/docs/identity/fine-grained-access
---

<Warning>
None of these features narrow or widen access on your normal API calls yet. `Resource` and `Condition` in policy statements are stored but ignored everywhere: an Allow statement grants its actions across your whole organization whatever its `Resource` says. Resource policies, relationship tuples, namespaces and tags are stored but no access decision reads them; `POST /v1/relations/check` answers from your tuples, but nothing else does. ABAC policies are applied only by the full evaluator behind `/v1/authz/evaluate` and `/v1/authz/simulate`, only when no policy allows the action, and `/v1/authz/explain` does not show them. This API may change.
</Warning>

## 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](#abac-attributes-and-policies)) |
| Tags | No effect | No effect |

The two decision paths are explained in [What Preview means](https://hexelstudio.com/docs/identity/preview#two-ways-a-request-is-checked).

## 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](https://hexelstudio.com/docs/identity/session-policies) require it, but neither field changes what a statement allows.

<Warning>
Do not rely on `Resource` to limit a grant. This statement allows deleting **every** workspace in the organization, not one:

```json
{"Effect": "Allow", "Action": "workspace:workspace:delete", "Resource": "workspace/<workspace-id>"}
```
</Warning>

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](https://hexelstudio.com/docs/identity/write-policies#grant-access-in-one-workspace-or-environment)), or use a role scoped to that workspace or environment: its sessions can act only there (see [Roles](https://hexelstudio.com/docs/identity/roles)).

Trust policies are different: their `Condition` blocks are evaluated. See [Roles](https://hexelstudio.com/docs/identity/roles) and [Workload identity](https://hexelstudio.com/docs/identity/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.

```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": "*"}
      ]
    }
  }'
```

| 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:

```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\"]}}"
  }'
```

| 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. |

```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

| 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 `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

| 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.

```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"}'
```

| 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`.

```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
  }'
```

| 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. |

```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"}'
```

| 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` |
