Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions documentation/changelog.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ This page tracks significant updates to the QuestDB documentation.

### New

- [Access control functions](/docs/query/functions/access-control/) - QuestDB Enterprise's four ACL table functions: `all_permissions()` lists available permissions and their grant levels, `permissions()` inspects one principal, and `active_permissions()` and `active_grants()` search effective permissions and direct grants across principals
- [Memory limits](/docs/configuration/cairo-engine/#memory-limits) - New section covering the per-query, materialized view refresh, WAL apply, and live view refresh memory limits, what counts toward them, and what happens on a breach, plus the previously undocumented [`cairo.mat.view.max.refresh.retries`](/docs/configuration/materialized-views/#cairomatviewmaxrefreshretries), [`cairo.mat.view.refresh.busy.retry.limit`](/docs/configuration/materialized-views/#cairomatviewrefreshbusyretrylimit), [`cairo.mat.view.refresh.busy.retry.timeout`](/docs/configuration/materialized-views/#cairomatviewrefreshbusyretrytimeout), [`cairo.write.back.off.timeout.on.mem.pressure`](/docs/configuration/cairo-engine/#cairowritebackofftimeoutonmempressure), [`ram.usage.limit.bytes`](/docs/configuration/cairo-engine/#ramusagelimitbytes), and [`ram.usage.limit.percent`](/docs/configuration/cairo-engine/#ramusagelimitpercent) keys
- [RBAC memory limits](/docs/security/rbac/#memory-limits) - Per-user, per-group, and per-service-account query memory limits in QuestDB Enterprise: `SET MEMORY LIMIT` on `ALTER USER`, `ALTER GROUP`, and `ALTER SERVICE ACCOUNT`, how limits resolve, the `SET MEMORY LIMIT` permission, and the upgrade migration
- [ALTER GROUP](/docs/query/sql/acl/alter-group/) - New reference page covering `SET MEMORY LIMIT` and external alias mapping
Expand Down
6 changes: 5 additions & 1 deletion documentation/getting-started/enterprise-quick-start.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,11 @@ GRANT ALL ON table2 TO user2 WITH GRANT OPTION;

Permission grants can be specific and fine-tuned.

List the full list of applied permissions with `all_permissions()`.
Check what a user can do with `SHOW PERMISSIONS user1;`, or audit grants across
all users, groups, and service accounts with
[`active_grants()`](/docs/query/functions/access-control/#active_grants).
[`all_permissions()`](/docs/query/functions/access-control/#all_permissions)
lists the permissions available to grant.

- For the full role-based access control docs, including group management, see
the [RBAC operations guide](/docs/security/rbac/).
Expand Down
351 changes: 351 additions & 0 deletions documentation/query/functions/access-control.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,351 @@
---
title: Access control functions
sidebar_label: Access control
description: >-
Audit QuestDB Enterprise RBAC in SQL with active_grants(),
active_permissions(), permissions(), and all_permissions(): list grants and
find who can read a table.
---

import { EnterpriseNote } from "@site/src/components/EnterpriseNote"

<EnterpriseNote>
Access control functions are available in QuestDB Enterprise.
</EnterpriseNote>

QuestDB Enterprise provides four SQL table functions for inspecting
[role-based access control](/docs/security/rbac/): `all_permissions()` lists
available permission names and scopes; `permissions()` shows one principal's
access; `active_permissions()` and `active_grants()` let you search effective
permissions or direct grants across all persisted internal users, groups, and
service accounts. Use them to find who can access a table or who has been
granted a particular permission.

## Syntax

```questdb-sql title="Available permission names and levels"
all_permissions()
```

```questdb-sql title="Current or named entity's permissions"
permissions([entityName])
```

```questdb-sql title="Effective permissions across entities"
active_permissions()
```

```questdb-sql title="Direct grants across entities"
active_grants()
```

All four are table functions used in the `FROM` clause. Their results can be
filtered, joined, and ordered with SQL.

- `entityName` (optional, string literal): existing user, group, or service
account to inspect. Omit it to inspect the current entity.

## all_permissions

`all_permissions()` takes no arguments and returns the permission names
supported by the server and the levels where they can be granted. It lists
permissions, **not** principals or their grants. Its columns are `permission`
(STRING) and `level` (STRING). `level` is `Database`, `Database|Table`, or
`Database|Table|Column` according to the permission's allowed scopes.

For example, check where `SELECT` can be granted:

```questdb-sql title="Where SELECT can be granted"
SELECT permission, level
FROM all_permissions()
WHERE permission = 'SELECT';
```

See the [permissions reference](/docs/security/rbac/#permissions) for the
available permissions and their uses.

## permissions

`permissions()` returns the effective permissions of one user, group, or
service account. Without an argument, it returns permissions for the current
principal. Pass an existing user, group, or service account name as a string to
inspect that entity instead. It returns the same result as
[`SHOW PERMISSIONS`](/docs/query/sql/show/#show-permissions),
but can be composed with `WHERE`, `ORDER BY`, and other SQL clauses:

```questdb-sql title="SELECT permissions of one user"
SELECT permission, table_name, column_name, grant_option, origin
FROM permissions('analyst')
WHERE permission = 'SELECT';
```

| Column | Type | Description |
| -------------- | ------- | ----------------------------------------------------------------------- |
| `permission` | STRING | Permission name |
| `table_name` | STRING | Table scope, or `NULL` for database-level permissions |
| `column_name` | STRING | Column scope, or `NULL` for table- and database-level permissions |
| `grant_option` | BOOLEAN | Whether the principal can grant this permission at this scope to others |
| `origin` | STRING | `G` for granted access, `I` for implicit designated-timestamp access |

`G` includes both direct and inherited permissions; it does not distinguish
between them. You can inspect your own permissions without `USER DETAILS`.
Inspecting another entity generally requires `USER DETAILS`; users can also
inspect their own groups and service accounts they can assume. `permissions()`
does not show all entities in a single result. It cannot be used in a
materialized or live view.

<span id="active-permissions-and-grants" />

## active_permissions

`active_permissions()` returns the effective permissions of every persisted
user, group, and service account, including permissions users inherit from
their groups. Use it to find who can access a table or column. It takes no
arguments and returns these columns:

| Column | Type | Description |
| -------------- | ------- | ------------------------------------------------------------------------------ |
| `entity_name` | STRING | Name of the user, group, or service account |
| `entity_type` | STRING | `User`, `Group`, or `Service Account` |
| `permission` | STRING | Permission name, such as `SELECT` or `CREATE TABLE` |
| `table_name` | STRING | Table name for a table or column scope; `NULL` for a database-level permission |
| `column_name` | STRING | Column name for a column scope; `NULL` for a table or database scope |
| `grant_option` | BOOLEAN | Whether the entity can grant this permission at this scope to others |

Each row is one scope at which the entity holds a permission:

- `table_name` is `NULL`: database-wide. A database-level `SELECT` applies to
all tables, including tables created later.
- `table_name` is set and `column_name` is `NULL`: the whole table.
- Both are set: that column only.

An entity can have rows for the same permission at more than one scope. For
example, a table-wide `SELECT` without the grant option can sit next to column
rows that carry the grant option for specific columns. Access is limited to
specific columns only when the entity has no row for that permission on the
whole table or database. When filtering for access to a particular table,
include both its table name and `NULL`.

Each user's rows combine its direct permissions with permissions inherited
from its groups. Groups and service accounts have their own rows. The result
also includes implicit access to a table's designated timestamp column when a
principal has `SELECT` or `UPDATE` on another column. For a principal with
`DATABASE ADMIN`, it expands the effective database permissions.

## active_grants

`active_grants()` returns the permissions granted directly to each persisted
user, group, and service account. Use it to audit who was granted what. It
takes no arguments and returns the same columns and scope rows as
[`active_permissions()`](#active_permissions).

Unlike `active_permissions()`, it does not repeat a group's grants under its
members or include implicit designated timestamp permissions. Results reflect the **current, normalized ACL scopes**,
not the original `GRANT` statements: for example, revoking access to a single
column can turn a table-wide grant into column-level rows. To inspect one
principal instead, use [`permissions()`](#permissions) or
[`SHOW PERMISSIONS`](/docs/query/sql/show/#show-permissions).

:::note

`active_permissions()` and `active_grants()` both require `LIST USERS` and
`USER DETAILS` permissions. The built-in
admin can call them without explicit grants unless it has assumed a service
account, in which case the assumed account needs both permissions. They return
an empty result if ACL is disabled. They do not list external SSO/OIDC
identities, which get their access through the groups they are mapped to, or
the built-in admin, which has no persisted ACL entry. A disabled user can still
appear with its retained permissions, so a row does not necessarily mean the
entity can connect. Check the `enabled` column of
[`SHOW USERS`](/docs/query/sql/show/#show-users) or
[`SHOW SERVICE ACCOUNTS`](/docs/query/sql/show/#show-service-accounts), and the
[endpoint permissions](/docs/security/rbac/#endpoint-permissions) such as
`PGWIRE` or `HTTP`. Neither function can be used in a materialized or live view.

:::

## Examples

The examples use the following entities and grants on the `trades` table, whose
designated timestamp column is `timestamp`. The result tables show the output
for this setup.

```questdb-sql title="Example setup"
CREATE GROUP trading_team;
CREATE USER analyst WITH PASSWORD 'pwd';
CREATE USER risk_manager WITH PASSWORD 'pwd';
CREATE SERVICE ACCOUNT report_svc;
ADD USER analyst TO trading_team;

GRANT CREATE TABLE TO trading_team;
GRANT SELECT ON trades(symbol, price) TO trading_team WITH GRANT OPTION;
GRANT SELECT ON trades TO analyst;
GRANT SELECT ON ALL TABLES TO risk_manager;
GRANT SELECT ON trades(symbol, price) TO report_svc;
```

### Compare effective permissions with direct grants

`active_permissions()` shows everything `analyst` can do, including what it
inherits from `trading_team`:

```questdb-sql title="Effective permissions of one user"
SELECT permission, table_name, column_name, grant_option
FROM active_permissions()
WHERE entity_name = 'analyst'
ORDER BY permission, table_name, column_name;
```

| permission | table_name | column_name | grant_option |
| ------------ | ---------- | ----------- | ------------ |
| CREATE TABLE | NULL | NULL | false |
| SELECT | trades | NULL | false |
| SELECT | trades | price | true |
| SELECT | trades | symbol | true |

`analyst` can read the whole `trades` table through its own table-wide grant.
The `price` and `symbol` rows come from the group and carry its grant option,
so `analyst` can grant `SELECT` on those two columns, but not on the whole
table. `CREATE TABLE` is also inherited from the group.

`active_grants()` shows only what was granted to `analyst` directly:

```questdb-sql title="Direct grants of one user"
SELECT permission, table_name, column_name, grant_option
FROM active_grants()
WHERE entity_name = 'analyst';
```

| permission | table_name | column_name | grant_option |
| ---------- | ---------- | ----------- | ------------ |
| SELECT | trades | NULL | false |

### Find who can read a table

List every entity with effective `SELECT` permission on `trades`, including
database-wide access:

```questdb-sql title="Entities that can read trades"
SELECT entity_name, entity_type, table_name, column_name
FROM active_permissions()
WHERE permission = 'SELECT'
AND (table_name = 'trades' OR table_name IS NULL)
ORDER BY entity_type, entity_name, table_name, column_name;
```

| entity_name | entity_type | table_name | column_name |
| ------------ | --------------- | ---------- | ----------- |
| trading_team | Group | trades | price |
| trading_team | Group | trades | symbol |
| trading_team | Group | trades | timestamp |
| report_svc | Service Account | trades | price |
| report_svc | Service Account | trades | symbol |
| report_svc | Service Account | trades | timestamp |
| analyst | User | trades | NULL |
| analyst | User | trades | price |
| analyst | User | trades | symbol |
| risk_manager | User | NULL | NULL |

- `risk_manager` can read every table, including `trades`.
- `analyst` can read the whole table. Its column rows record the grant option
shown in the previous example.
- `trading_team` and `report_svc` can read `symbol` and `price`, plus the
designated timestamp column, which comes with column-level `SELECT`.
- Keep group rows when auditing: a group cannot log in, but its members can,
including external SSO/OIDC users mapped to it, who are not listed
individually.

To check access to a single column, also accept table-wide rows, and use
`DISTINCT` to get one row per entity:

```questdb-sql title="Entities that can read trades.price"
SELECT DISTINCT entity_name, entity_type
FROM active_permissions()
WHERE permission = 'SELECT'
AND (table_name = 'trades' OR table_name IS NULL)
AND (column_name = 'price' OR column_name IS NULL)
ORDER BY entity_type, entity_name;
```

| entity_name | entity_type |
| ------------ | --------------- |
| trading_team | Group |
| report_svc | Service Account |
| analyst | User |
| risk_manager | User |

These queries do not show every way to reach the data:

- **Assumed service accounts.** A user who can assume a service account gets
its permissions after
[`ASSUME SERVICE ACCOUNT`](/docs/security/rbac/#service-account-assumption),
but those permissions appear only under the service account's name. Use
[`SHOW SERVICE ACCOUNTS userName`](/docs/query/sql/show/#show-service-accounts)
to list the accounts a user or group can assume.
- **Views.** `SELECT` on a view over `trades` lets the grantee read the view's
rows without any grant on `trades`. See
[row-level access with views](/docs/security/rbac/#row-level-access-with-views).

### Find direct recipients of a permission

Find who was directly granted `CREATE TABLE`. Members who inherit a group's
permission are not repeated, so `analyst` does not appear. Use
`active_permissions()` to see effective access.

```questdb-sql title="Direct recipients of CREATE TABLE"
SELECT entity_name, entity_type, grant_option
FROM active_grants()
WHERE permission = 'CREATE TABLE'
ORDER BY entity_type, entity_name;
```

| entity_name | entity_type | grant_option |
| ------------ | ----------- | ------------ |
| trading_team | Group | false |

For a table-scoped permission such as `SELECT`, also filter by scope, including
database-wide grants:

```questdb-sql title="Direct SELECT grants on trades"
SELECT entity_name, entity_type, table_name, column_name, grant_option
FROM active_grants()
WHERE permission = 'SELECT'
AND (table_name = 'trades' OR table_name IS NULL)
ORDER BY entity_type, entity_name, table_name, column_name;
```

| entity_name | entity_type | table_name | column_name | grant_option |
| ------------ | --------------- | ---------- | ----------- | ------------ |
| trading_team | Group | trades | price | true |
| trading_team | Group | trades | symbol | true |
| report_svc | Service Account | trades | price | false |
| report_svc | Service Account | trades | symbol | false |
| analyst | User | trades | NULL | false |
| risk_manager | User | NULL | NULL | false |

Unlike `active_permissions()`, this does not include the implicit designated
timestamp rows.

### Find who can delegate SELECT on a table

Filter effective permissions by `grant_option` to find entities that can grant
`SELECT` on all or part of `trades` to others:

```questdb-sql title="Entities that can grant SELECT on trades"
SELECT entity_name, entity_type, table_name, column_name
FROM active_permissions()
WHERE permission = 'SELECT'
AND grant_option
AND (table_name = 'trades' OR table_name IS NULL)
ORDER BY entity_type, entity_name, table_name, column_name;
```

| entity_name | entity_type | table_name | column_name |
| ------------ | ----------- | ---------- | ----------- |
| trading_team | Group | trades | price |
| trading_team | Group | trades | symbol |
| analyst | User | trades | price |
| analyst | User | trades | symbol |

Both can grant `SELECT` on `price` and `symbol` only. No one in this setup can
grant `SELECT` on the whole table.
Loading
Loading