Row-Level Access How-to

Use row-level access in the UI or API

Managing row-level access policies

Row-level access control allows administrators to define filters limiting which data rows users can query. See Row-level Access Control for a description of the feature.

This page demonstrates how to create, modify, delete, and verify row policies which implement the row-level access control feature.

Row-level access overview

Data administrators define and group sets of filters and associate them to roles.

When receiving a query, the Hydrolix cluster combines all row policies for all roles associated with the querying account.

The query system enforces data access permissions by returning only the rows in the raw Hydrolix partitions that pass the combined filter.

Before you begin

Accompanying the instructions on this page are a tiny sample transform and data set for familiarizing yourself with row-level access controls.

Consider also reading Policy design and Combining row policies when working with production-grade systems.

Required permissions

The account used to configure row policies requires several permissions.

  • Row policy management permissions.

    • add_rowpolicy
    • change_rowpolicy
    • view_rowpolicy
    • delete_rowpolicy
    • add_role_rowpolicy
    • remove_role_rowpolicy
  • Role management permissions for role creation.

    • add_role
  • For using the UI the following are useful

    • view_role
    • view_table

Row policy requirements

Each row policy must be uniquely named in a table, must include a valid filter, and refer to an existing role.

Quick links

Action
Create a row policycreate using UIcreate using API
View a row policyview using UIview using API
Update a row policyupdate using UIupdate using API
Delete a row policydelete using UIdelete using API

Demonstration table

The demonstration table for the examples in this how-to is news.requests. The table holds multi-CDN request level data for different sections of a media site. The example row policies install filters to limit access to the Sports section.

Minimal demonstration data and transform are available. This informational table describes the contents of the columns, but especially those used in filter criteria.

ColumnTypeDescription
sectionstringA media section, like Business, Lifestyle, Culture, Sports
client_ipipThe requester's IPv4 or IPv6 address; perhaps anonymized or truncated
client_country_iso_codestringAn ISO-3166-2 country code
response_status_codeuint8The logged numeric HTTP status code
response_bytes_totaluint32Number of bytes in the HTTP response payload sent to the client

The examples below demonstrate row policy management using the above table.

Managing row policies in the UI

(Optional) Create role in UI

Here, we demonstrate creating an empty role that will serve only as a container for the row policies on the news.requests table.

  1. In the top right corner of the screen, click the + Add new button.
  2. Select the Role option in the right sidebar.
  1. Enter a unique role name, and any description.
  2. Click Create role.

Create in UI

  1. In the top right corner of the screen, click the + Add new button.
  2. Select the Row policy option in the right sidebar.
  3. Select an existing Table to which the new row policy will apply.
  4. Enter a unique Name for this row policy in this table.
  5. Enter any valid SQL expression, that evaluates to TRUE to allow users to see the corresponding rows.
  6. Use Add Roles to attach the row policy to one or more existing roles.
  7. Select Restrictive or not. See Combining row policies.
  8. Click Create row policy.

View in UI

  1. Click Security in the left sidebar.
  2. Click Row Policies in the top bar.
  3. Select the Table for the row policies you wish to view.

Update in UI

  1. Click Security in the left sidebar.
  2. Click Row Policies in the top bar.
  3. Select the Table holding the row policies.
  4. Click any text in the desired row or use the vertical ellipsis (⋮) and select the Edit option to open the row policy editor.
  5. Use the Edit Row Policy page to make changes to the row policy.
  6. Click Save Changes.

Delete in UI

  1. Click Security in the left sidebar.
  2. Click Row Policies in the top bar.
  3. Select the Table for the row policies you wish to view.
  4. Use the vertical ellipsis (⋮) and select the Delete option on the row policy you wish to remove.
  5. Cilck Delete on the confirmation dialog.

⚠️Deleting a row policy changes data access rules immediately

Deleting a row policy removes it from roles and changes access rules immediately. This could allow broader or unfiltered table access to accounts formerly constrained by the row policy.

Managing row policies with the API

General workflow

  1. Acquire an auth token using Login to Hydrolix. See also acquiring auth tokens.
  2. Use Create role or choose an existing role to which to assign the row policies.
  3. Create a row policy with the desired filter, assigned roles, and restrictive setting.
  4. Wait a few seconds for the row policies to go into effect in the query system.

Login to the API

Get the bearer token, which is good for the next 24 hours, to authenticate future API calls. This command assumes you've set the $HDX_HOSTNAME, $HDX_USER and $HDX_PASSWORD environment variables:

export HDX_TOKEN=$(
  curl -v -X POST -H "Content-Type: application/json" \
  https://$HDX_HOSTNAME/config/v1/login/ \
  -d "{
    \"username\":\"$HDX_USER\",
    \"password\":\"$HDX_PASSWORD\"  
  }" | jq -r ".auth_token.access_token"
)

(Optional) Create role with API

Row policies must be associated with an existing role. See Roles and row policies. In this example, we're creating an empty role.

Use the Create role endpoint.

POST /config/v1/roles/
{
  "name": "news_requests_sports",
  "policies": []
}
{
  "id": 10,
  "name": "news_requests_sports",
  "policies": []
}

Create with API

A new row policy object must contain

  • A row policy name unique for this table
  • A valid filter. See Filters and composition
  • A list of roles. You must supply at least one role and all specified roles must already exist

Use the Create a row policy endpoint.

POST /config/v1/orgs/{org_id}/projects/{project_id}/tables/{table_id}/rowpolicies/
{
    "name": "section-sports",
    "filter": "section = 'Sports'",
    "roles": ["news_requests_sports"],
    "restrictive": false
}
{
  "uuid": "362d993e-2e1b-4db3-8b58-ed337e00cb9d",
  "created": "2025-08-27T15:24:05.592837Z",
  "modified": "2025-08-27T15:24:05.592852Z",
  "roles": [
    "news_requests_sports"
  ],
  "table": "news.requests",
  "name": "section-sports",
  "filter": "section = 'Sports'",
  "restrictive": false
}

An error example

The Config API returns an HTTP 400 status code for the following row policy.

There are three problems with the following update:

  • Row policy named section-sports exists
  • Thefilter has an unmatched ending parenthesis ); it's invalid SQL
  • The roles list is empty

Any one of these problems would be reason for an HTTP 400.

POST  /config/v1/orgs/{org_id}/projects/{project_id}/tables/{table_id}/rowpolicies/
{
    "name": "section-sports",
    "filter": "section = 'Sports')",
    "roles": [],
    "restrictive": false
}

View with API

List all row policies

This example shows multiple row policies. Creation of isocode-us-fr isn't demonstrated on this page, but is shown in the output below.

Use the Get row policies endpoint.

GET  /config/v1/orgs/{org_id}/projects/{project_id}/tables/{table_id}/rowpolicies/
{
  "next": 0,
  "previous": 0,
  "current": 1,
  "num_pages": 1,
  "count": 2,
  "results": [
    {
      "uuid": "362d993e-2e1b-4db3-8b58-ed337e00cb9d",
      "created": "2025-08-27T15:24:05.592837Z",
      "modified": "2025-08-27T15:24:05.592852Z",
      "roles": [
        "news_requests_sports"
      ],
      "table": "news.requests",
      "name": "section-sports",
      "filter": "section = 'Sports'",
      "restrictive": false
    },
    {
      "uuid": "be53f5f7-55d7-4925-b804-52b7c660c860",
      "created": "2025-08-27T17:43:24.894197Z",
      "modified": "2025-08-27T17:43:24.894210Z",
      "roles": [
        "news_requests_sports"
      ],
      "table": "news.requests",
      "name": "isocode-us-fr",
      "filter": "client_country_iso_code IN ('FR', 'US')",
      "restrictive": false
    }
  ]
}

Get a single row policy

Use the Get a row policy endpoint.

GET  /config/v1/orgs/{org_id}/projects/{project_id}/tables/{table_id}/rowpolicies/362d993e-2e1b-4db3-8b58-ed337e00cb9d
{
  "uuid": "362d993e-2e1b-4db3-8b58-ed337e00cb9d",
  "created": "2025-08-27T15:24:05.592837Z",
  "modified": "2025-08-27T15:24:05.592852Z",
  "roles": [
    "news_requests_sports"
  ],
  "table": "news.requests",
  "name": "section-sports",
  "filter": "section = 'Sports'",
  "restrictive": false
}

Update with API

Change an existing row policy

This example switches the filter to use the SQL IN operator. All other fields remain untouched.

Use Partially update a row policy to change an existing row policy.

PATCH  /config/v1/orgs/{org_id}/projects/{project_id}/tables/{table_id}/rowpolicies/362d993e-2e1b-4db3-8b58-ed337e00cb9d
{
  "filter": "section IN ('Sports')"
}
{
  "uuid": "362d993e-2e1b-4db3-8b58-ed337e00cb9d",
  "created": "2025-08-27T15:24:05.592837Z",
  "modified": "2025-08-27T22:40:40.976790Z",
  "roles": [
    "news_requests_sports"
  ],
  "table": "news.requests",
  "name": "section-sports",
  "filter": "section IN ('Sports')",
  "restrictive": false
}

Replace an existing row policy

This example renames the row policy to include the table name to demonstrate replacing an existing policy completely.

Use the Update a row policy endpoint to replace the entire policy.

PUT  /config/v1/orgs/{org_id}/projects/{project_id}/tables/{table_id}/rowpolicies/362d993e-2e1b-4db3-8b58-ed337e00cb9d
{
    "name": "news-requests-section-sports",
    "filter": "section IN ('Sports')",
    "roles": ["news_requests_sports"],
    "restrictive": false
}
{
  "uuid": "362d993e-2e1b-4db3-8b58-ed337e00cb9d",
  "created": "2025-08-27T15:24:05.592837Z",
  "modified": "2025-08-28T13:57:43.993920Z",
  "roles": [
    "news_requests_sports"
  ],
  "table": "news.requests",
  "name": "news-requests-section-sports",
  "filter": "section IN ('Sports')",
  "restrictive": false
}

Delete with API

This example removes the row policy created earlier on this page.

Use Delete a row policy to remove a row policy from the system completely.

DELETE  /config/v1/orgs/{org_id}/projects/{project_id}/tables/{table_id}/rowpolicies/362d993e-2e1b-4db3-8b58-ed337e00cb9d

Upon success, the server returns HTTP status code 204 and no content.

Verification steps

Changes to row policies are applied immediately to accounts querying the tables. There is a brief delay as the updated row policies are combined, distributed to the query services, and applied to new incoming queries.

To verify the installed row policies match your intended data access plan, apply the roles to an account and issue queries using that account. A service account is convenient for programmatic verification.

The single row policy associated with the role news_requests_sports allows accounts with that role to see only rows where the section column contains exactly the value Sports. Here's an example of a simple verification step.

$ echo "SELECT DISTINCT section FROM news.requests;" \
  | mysql 2>/dev/null --host "${HDX_HOSTNAME}" --skip-column-names  --batch --user __api_token__ --port 9004 --password="${HDX_TOKEN}"
Sports
$ echo "SELECT DISTINCT section FROM news.requests;" \
  | mysql 2>/dev/null --host "${HDX_HOSTNAME}" --skip-column-names  --batch --user __api_token__ --port 9004 --password="${HDX_TOKEN}"
Sports
Lifestyle
Culture
Business

Troubleshooting

Problem: Expected row policy not in effect

If a row policy doesn't seem to affect a user as expected, verify the role associated to the row policy matches one the user's roles.

  1. Locate the row policies for the table, and identify the roles for the row policies.
  2. Examine the roles associated with a user.
  3. Confirm the association of the row policies to a role associated with the user.

Related material

Demonstration samples

Here is an example transform and several rows of data used in illustrating the row policies feature.

Demonstration transform

{
  "transform": {
    "type": "json",
    "settings": {
      "output_columns": [
        {
          "name": "timestamp",
          "datatype": {
            "type": "datetime",
            "primary": true,
            "format": "2006-01-02 15:04:05 MST",
            "resolution": "ms",
            "source": {
              "from_automatic_value": "current_time"
            }
          }
        },
        {
          "name": "section",
          "datatype": {
            "type": "string"
          }
        },
        {
          "name": "client_country_iso_code",
          "datatype": {
            "type": "string"
          }
        },
        {
          "name": "client_ip",
          "datatype": {
            "type": "ip"
          }
        },
        {
          "name": "response_status_code",
          "datatype": {
            "type": "uint8"
          }
        },
        {
          "name": "response_bytes_total",
          "datatype": {
            "type": "uint32"
          }
        }
      ]
    }
  }
}

Demonstration data

[
    {
      "section": "Sports",
      "client_country_iso_code": "US",
      "client_ip": "71.57.62.50",
      "response_status_code": 200,
      "response_bytes_total": 80707
    },
    {
      "section": "Sports",
      "client_country_iso_code": "US",
      "client_ip": "24.27.228.131",
      "response_status_code": 200,
      "response_bytes_total": 9407
    },
    {
      "section": "Sports",
      "client_country_iso_code": "IN",
      "client_ip": "61.11.4.75",
      "response_status_code": 200,
      "response_bytes_total": 176767
    },
    {
      "section": "Sports",
      "client_country_iso_code": "IN",
      "client_ip": "61.11.3.99",
      "response_status_code": 200,
      "response_bytes_total": 12820
    },
    {
      "section": "Sports",
      "client_country_iso_code": "FR",
      "client_ip": "80.12.11.178",
      "response_status_code": 200,
      "response_bytes_total": 56871
    },
    {
      "section": "Sports",
      "client_country_iso_code": "FR",
      "client_ip": "80.12.14.207",
      "response_status_code": 200,
      "response_bytes_total": 131531
    },
    {
      "section": "Sports",
      "client_country_iso_code": "DE",
      "client_ip": "37.50.61.111",
      "response_status_code": 200,
      "response_bytes_total": 41414
    },
    {
      "section": "Sports",
      "client_country_iso_code": "DE",
      "client_ip": "62.214.238.149",
      "response_status_code": 200,
      "response_bytes_total": 53225
    },
    {
      "section": "Lifestyle",
      "client_country_iso_code": "US",
      "client_ip": "71.56.168.94",
      "response_status_code": 200,
      "response_bytes_total": 83390
    },
    {
      "section": "Lifestyle",
      "client_country_iso_code": "US",
      "client_ip": "16.191.1.197",
      "response_status_code": 200,
      "response_bytes_total": 154
    },
    {
      "section": "Lifestyle",
      "client_country_iso_code": "IN",
      "client_ip": "115.112.145.207",
      "response_status_code": 200,
      "response_bytes_total": 74171
    },
    {
      "section": "Lifestyle",
      "client_country_iso_code": "IN",
      "client_ip": "61.11.1.136",
      "response_status_code": 200,
      "response_bytes_total": 189135
    },
    {
      "section": "Lifestyle",
      "client_country_iso_code": "FR",
      "client_ip": "80.12.6.219",
      "response_status_code": 200,
      "response_bytes_total": 71261
    },
    {
      "section": "Lifestyle",
      "client_country_iso_code": "FR",
      "client_ip": "2.12.104.84",
      "response_status_code": 200,
      "response_bytes_total": 19601
    },
    {
      "section": "Lifestyle",
      "client_country_iso_code": "DE",
      "client_ip": "62.214.243.241",
      "response_status_code": 200,
      "response_bytes_total": 75508
    },
    {
      "section": "Lifestyle",
      "client_country_iso_code": "DE",
      "client_ip": "2.162.119.239",
      "response_status_code": 200,
      "response_bytes_total": 24924
    },
    {
      "section": "Business",
      "client_country_iso_code": "US",
      "client_ip": "71.56.54.252",
      "response_status_code": 200,
      "response_bytes_total": 41596
    },
    {
      "section": "Business",
      "client_country_iso_code": "US",
      "client_ip": "24.27.231.214",
      "response_status_code": 200,
      "response_bytes_total": 13349
    },
    {
      "section": "Business",
      "client_country_iso_code": "IN",
      "client_ip": "115.112.160.225",
      "response_status_code": 200,
      "response_bytes_total": 177977
    },
    {
      "section": "Business",
      "client_country_iso_code": "IN",
      "client_ip": "61.11.11.250",
      "response_status_code": 200,
      "response_bytes_total": 173828
    },
    {
      "section": "Business",
      "client_country_iso_code": "FR",
      "client_ip": "80.12.10.255",
      "response_status_code": 200,
      "response_bytes_total": 128701
    },
    {
      "section": "Business",
      "client_country_iso_code": "FR",
      "client_ip": "2.12.78.25",
      "response_status_code": 200,
      "response_bytes_total": 92159
    },
    {
      "section": "Business",
      "client_country_iso_code": "DE",
      "client_ip": "2.175.131.2",
      "response_status_code": 200,
      "response_bytes_total": 43755
    },
    {
      "section": "Business",
      "client_country_iso_code": "DE",
      "client_ip": "2.171.167.248",
      "response_status_code": 200,
      "response_bytes_total": 29180
    },
    {
      "section": "Culture",
      "client_country_iso_code": "US",
      "client_ip": "24.27.228.54",
      "response_status_code": 200,
      "response_bytes_total": 33120
    },
    {
      "section": "Culture",
      "client_country_iso_code": "US",
      "client_ip": "16.191.1.201",
      "response_status_code": 200,
      "response_bytes_total": 182454
    },
    {
      "section": "Culture",
      "client_country_iso_code": "IN",
      "client_ip": "115.112.230.109",
      "response_status_code": 200,
      "response_bytes_total": 184091
    },
    {
      "section": "Culture",
      "client_country_iso_code": "IN",
      "client_ip": "115.112.66.224",
      "response_status_code": 200,
      "response_bytes_total": 16422
    },
    {
      "section": "Culture",
      "client_country_iso_code": "FR",
      "client_ip": "80.12.8.83",
      "response_status_code": 200,
      "response_bytes_total": 98296
    },
    {
      "section": "Culture",
      "client_country_iso_code": "FR",
      "client_ip": "80.12.0.253",
      "response_status_code": 200,
      "response_bytes_total": 178469
    },
    {
      "section": "Culture",
      "client_country_iso_code": "DE",
      "client_ip": "2.163.213.161",
      "response_status_code": 200,
      "response_bytes_total": 95524
    },
    {
      "section": "Culture",
      "client_country_iso_code": "DE",
      "client_ip": "62.214.244.253",
      "response_status_code": 200,
      "response_bytes_total": 146540
    }
]