> For the complete documentation index, see [llms.txt](https://docs.opendatadiscovery.org/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.opendatadiscovery.org/configuration-and-deployment/enable-security/authorization/policies.md).

# Policies

ODD Platform allows to manage access to resources by creating policies and attaching them to owners through roles.

Policies are described in JSON format and validated with [JSON Schema](https://json-schema.org).

{% hint style="warning" %}
**Deleting a policy is blocked while a role is still bound to it.** ODD refuses to delete a policy that any role still references — the attempt fails with *"Policy is attached to a role."* To delete a policy, first detach it from every role on the **Roles** tab; once no role references it, the deletion succeeds and the permissions it granted go with it. This mirrors the cascade-delete guards on namespaces and owners — see [Permissions](/configuration-and-deployment/enable-security/authorization/permissions.md).
{% endhint %}

## JSON policy structure

Each policy is represented by an array of statements and each statement defines a resource with optional conditions and [permissions](/configuration-and-deployment/enable-security/authorization/permissions.md) which will be allowed for given resource.

{% code title="Basic policy structure" %}

```json
{
  "statements": [
    {
      "resource": {
        "type": "",
        "conditions": {}
      },
      "permissions": []
    },
    {
      "resource": {
        "type": "",
        "conditions": {}
      },
      "permissions": []
    }
  ]
}
```

{% endcode %}

### Resource type

There are 3 possible types of policy resource:

* **DATA\_ENTITY** - Indicates, that current permissions are applied for data entity
* **TERM** - Indicates, that current permissions are applied for dictionary term
* **MANAGEMENT** - Indicates, that current permissions are general and work all over the platform
* **QUERY\_EXAMPLE** - Indicates, that the current permissions are applied for query examples

{% hint style="info" %}
Each type can be combined only with associated permissions and conditions, e.g. if you describe statement for **DATA\_ENTITY** type you can only use data entity's conditions and [permissions](/configuration-and-deployment/enable-security/authorization/permissions.md).
{% endhint %}

### Conditions

Conditions allow to specify the circumstances under which the policy grants permission.

This is an optional field and in case of absence, permissions will be applied to all resource type entries.

{% hint style="warning" %}
Conditions can't be applied to **MANAGEMENT** resource type
{% endhint %}

In ODD Platform we have pre-defined [condition operators](#condition-operators) and [fields](#condition-fields), which can be used with these operators.

#### Condition operators

Currently we support next operators:

* `all` - all conditions under this operator must be positive
* `any` - at least one condition under this operation must be positive
* `eq` - [condition field](#condition-fields) must be equal to some value
* `not_eq` - [condition field](#condition-fields) must not be equal to some value
* `match` - [condition field](#condition-fields) must match some value
* `not_match` - [condition field](#condition-fields) must not match some value
* `is` - [condition field](#condition-fields) must be true
* `not_is` - [condition field](#condition-fields) must be false

#### **Condition fields**

There are couple of pre-defined fields, which can be used in conditions. Each resource type has its own fields.

**Data entity**

* `dataEntity:oddrn` - data entity's ODDRN
* `dataEntity:internalName` - data entity's business name
* `dataEntity:externalName` - data entity's ingested name
* `dataEntity:type` - data entity's type name
* `dataEntity:class` - data entity's class name
* `dataEntity:datasource:oddrn` - data entity's datasource ODDRN
* `dataEntity:datasource:name` - data entity's datasource name
* `dataEntity:namespace:name` - data entity's namespace name
* `dataEntity:tag:name` - data entity's tag name
* `dataEntity:owner` - data entity's owner
* `dataEntity:owner:title` - data entity's owner title (see [Title vocabulary caveat](#title-vocabulary-caveat-for-ownertitle-conditions) below)

**Term**

* `term:name` - term's name
* `term:namespace:name` - term's namespace name
* `term:tag:name` - term's tag name
* `term:owner` - term's owner
* `term:owner:title` - term's owner title (see [Title vocabulary caveat](#title-vocabulary-caveat-for-ownertitle-conditions) below)

#### Condition examples

1. User must be term's owner, term must be in Open Data Discovery namespace and have tag, which name equals to `Test`.

   ```json
   {
     "all": [
       {
         "is": "term:owner"
       },
       {
         "eq": {
           "term:namespace:name": "Open Data Discovery"
         }
       },
       {
         "match": {
           "term:tag:name": "Test"
         }
       }
     ]
   }
   ```
2. At least one of the conditions must be positive: User must be data entity's owner **OR** data entity shouldn't have tag `PII`.

   ```json
   {
     "any": [
       {
         "is": "dataEntity:owner"
       },
       {
         "not_eq": {
           "dataEntity:tag:name": "PII"
         }
       }
     ]
   }
   ```

## Permissions

Please check the [Permissions](/configuration-and-deployment/enable-security/authorization/permissions.md) section for all available permissions list.

## Policy examples

#### Data entity policy with conditions

Policy allows to update business name, description and custom metadata if user is data entity's owner and this data entity is in `Open Data Discovery` namespace

```json
{
  "statements": [
    {
      "resource": {
        "type": "DATA_ENTITY",
        "conditions": {
          "all": [
            {
              "is": "dataEntity:owner"
            },
            {
              "eq": {
                "dataEntity:namespace:name": "Open Data Discovery"
              }
            }
          ]
        }
      },
      "permissions": [
        "DATA_ENTITY_INTERNAL_NAME_UPDATE",
        "DATA_ENTITY_CUSTOM_METADATA_CREATE",
        "DATA_ENTITY_CUSTOM_METADATA_UPDATE",
        "DATA_ENTITY_CUSTOM_METADATA_DELETE",
        "DATA_ENTITY_DESCRIPTION_UPDATE"
      ]
    }
  ]
}
```

#### Data entity policy without conditions

All actions are allowed for all data entities

```json
{
  "statements": [
    {
      "resource": {
        "type": "DATA_ENTITY"
      },
      "permissions": [
        "ALL"
      ]
    }
  ]
}
```

#### Dictionary term policy with conditions

Policy allows to update term information and ownership if it has `Customer` tag

```json
{
  "statements": [
    {
      "resource": {
        "type": "TERM",
        "conditions": {
          "eq": {
            "term:tag:name": "Customer"
          }
        }      
      },
      "permissions": [
        "TERM_UPDATE",
        "TERM_OWNERSHIP_CREATE",
        "TERM_OWNERSHIP_UPDATE",
        "TERM_OWNERSHIP_DELETE"
      ]
    }
  ]
}
```

#### Management policy

Policy allows to manage datasources, collectors and namespaces

```json
{
  "statements": [
    {
      "resource": {
        "type": "MANAGEMENT"
      },
      "permissions": [
        "DATA_SOURCE_CREATE",
        "DATA_SOURCE_UPDATE",
        "DATA_SOURCE_DELETE",
        "DATA_SOURCE_TOKEN_REGENERATE",
        "COLLECTOR_CREATE",
        "COLLECTOR_UPDATE",
        "COLLECTOR_DELETE",
        "COLLECTOR_TOKEN_REGENERATE",
        "NAMESPACE_CREATE",
        "NAMESPACE_UPDATE",
        "NAMESPACE_DELETE"
      ]
    }
  ]
}
```

#### Combined policy

Policy allows to edit term information and permits all actions for data entities from `Finance` namespace.

```json
{
  "statements": [
    {
      "resource": {
        "type": "TERM",
        "conditions": {
          "eq": {
            "term:namespace:name": "Finance"
          }
        }
      },
      "permissions": [
        "TERM_UPDATE"
      ]
    },
    {
      "resource": {
        "type": "DATA_ENTITY",
        "conditions": {
          "eq": {
            "dataEntity:namespace:name": "Finance"
          }
        }
      },
      "permissions": [
        "ALL"
      ]
    }
  ]
}
```

## Title vocabulary caveat for `:owner:title` conditions

The `dataEntity:owner:title` and `term:owner:title` condition fields evaluate against the platform's `Title` table — a free-text vocabulary populated by users at ownership-grant time, not a curated allowlist. Two operator-visible behaviours follow from this and apply to every Policy condition referencing those fields.

{% hint style="warning" %}
**Title strings are case-sensitive, exact-match, and not normalised.** The platform's `TitleService.getOrCreate(name)` accepts any string verbatim: no case-folding, no whitespace trimming, no slug-deduping, no `@Pattern` or `@Size` validation. The underlying `title.name` column has no `CHECK` constraint. Two users who type `'Data Steward'`, `'data steward'`, `'DATA STEWARD'`, `' Data Steward '`, or `'data-steward'` into the same role-attach form each accumulate as **distinct rows** in the `Title` table. A Policy condition `dataEntity:owner:title == 'Data Steward'` silently misses every other-casing variant — operators carrying the wrong-cased Title receive access-denied with no platform-visible diagnostic.

The Title table is also written through a side-channel: any caller with `DATA_ENTITY_OWNERSHIP_CREATE` or `TERM_OWNERSHIP_CREATE` can mint a new Title row by typing a never-before-seen string into an ownership-grant form. New Title strings become part of the Policy-condition vocabulary the moment they are written.

**Mitigations operators can apply today** (pending an upstream curated-Title-vocabulary fix): enumerate the variants explicitly with an `any` block of `eq` conditions — `{ "any": [ { "eq": { "dataEntity:owner:title": "Data Steward" } }, { "eq": { "dataEntity:owner:title": "data steward" } }, { "eq": { "dataEntity:owner:title": "DATA STEWARD" } } ] }`. (There is **no `in` operator** — see [Condition operators](#condition-operators) for the supported set; a condition using `in` is rejected by the policy JSON Schema, and the platform returns an error rather than saving the policy.) OR restrict the two `*_OWNERSHIP_CREATE` permissions to a vocabulary-steward role so the Title set stays bounded; OR deploy a periodic SQL job that consolidates obvious typo-variants.
{% endhint %}

{% hint style="info" %}
**There is no Management UI tab for Titles.** The Management surface lists Namespaces, Datasources, Integrations, Collectors, Owners, Tags, Associations, Roles, and Policies — no Titles entry. Operators cannot browse, merge, or delete duplicate or typo'd Titles via the UI today. Title curation requires direct database access against the `title` table until a Titles management surface ships upstream.
{% endhint %}

## Performance characteristics

Every authenticated request that reaches a permission-gated endpoint resolves the caller's Policies before the handler runs. The resolution is **not cached** at the request scope, the user scope, or any other scope — `PolicyService.getCurrentUserPolicies` is invoked from the platform's permission extractors on every authorized HTTP request and executes two JOIN roundtrips against PostgreSQL each time (a 5-table user→owner→user-owner-mapping chain to resolve the caller's roles, then a 2-table role→policies chain to fetch each role's policy set).

**Throughput implication.** For a platform receiving N authenticated requests per second, the authorization hot-path generates approximately `2 × N` PostgreSQL queries per second over and above the application-logic queries served by the matching handler. Size the R2DBC connection pool (`spring.r2dbc.pool.max-size`) and the database CPU budget accordingly — a practical heuristic is to provision 4–5 concurrent JOIN slots per peak authenticated request per second, plus headroom for the application-logic queries each request also triggers.

**Operator-tunable knob today: none.** There is no in-process cache, no `@Cacheable` annotation, no operator setting to tune the cache TTL. An upstream request-scoped cache for `getCurrentUserPolicies` is tracked; until it ships, the per-request cost is the floor.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.opendatadiscovery.org/configuration-and-deployment/enable-security/authorization/policies.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
