From 751c0357fac735e1deb7e339e86d0668ae1a58f8 Mon Sep 17 00:00:00 2001 From: glasstiger Date: Fri, 23 Oct 2026 12:16:23 +0100 Subject: [PATCH 1/4] docs: document Enterprise ACL inspection functions Document the changes from questdb/questdb-enterprise#1222, shipped in QuestDB Enterprise 4.1.0: - Add the Access control functions reference page, covering active_grants() and active_permissions() alongside all_permissions() and permissions(): result columns, scope semantics, inheritance, the implicit designated timestamp permission, DATABASE ADMIN expansion, required permissions, and limitations, with verified examples. - Document that Enterprise administrative functions are rejected in materialized view and live view queries, with the errors returned. - Document how a read-only replica refreshes replicated access lists, and the error returned when a refresh does not finish in 30 seconds. - Link the new functions from RBAC, SHOW, and GRANT, and correct the Enterprise quick start, which described all_permissions() as listing applied permissions. --- documentation/changelog.mdx | 4 + documentation/concepts/live-views.md | 5 + documentation/concepts/materialized-views.md | 5 + .../getting-started/enterprise-quick-start.md | 10 +- .../query/functions/access-control.md | 283 ++++++++++++++++++ documentation/query/sql/acl/grant.md | 3 + documentation/query/sql/create-live-view.md | 5 + documentation/query/sql/create-mat-view.md | 3 + documentation/query/sql/show.md | 7 +- documentation/security/rbac.md | 34 ++- documentation/sidebars.js | 1 + 11 files changed, 356 insertions(+), 4 deletions(-) create mode 100644 documentation/query/functions/access-control.md diff --git a/documentation/changelog.mdx b/documentation/changelog.mdx index 1ef8ca0b12..0b280c71ce 100644 --- a/documentation/changelog.mdx +++ b/documentation/changelog.mdx @@ -14,6 +14,7 @@ This page tracks significant updates to the QuestDB documentation. - [Databento connector](/docs/connect/connectors/databento/) - New page for the QuestDB Enterprise Databento connector, which streams Live feeds and runs Historical backfills into QuestDB. Covers installation, TOML configuration, table mappings, storage policies, candles from a materialized view, a session VWAP live view, live dashboards in a Web Console notebook or Grafana, checkpointed recovery, and Prometheus metrics - [ALTER LIVE VIEW RESUME WAL](/docs/query/sql/alter-live-view-resume-wal/) - New reference page for resuming a live view whose own WAL was suspended after an error, and how to find suspended live views through `wal_tables()` +- [Access control functions](/docs/query/functions/access-control/) - New reference page for the QuestDB Enterprise ACL table functions. `active_permissions()` and `active_grants()`, new in Enterprise 4.1.0, list the effective permissions and the direct grants of every user, group, and service account, with examples for auditing who can read a table. The page also covers `all_permissions()` and `permissions()` ### Reference @@ -35,6 +36,9 @@ This page tracks significant updates to the QuestDB documentation. - [EMA and VWEMA](/docs/query/functions/window-functions/reference/#avg) - Corrected the frame behavior: both reject a frame clause instead of ignoring it - [ALTER TABLE RESUME WAL](/docs/query/sql/alter-table-resume-wal/), [Monitoring and alerting](/docs/operations/monitoring-alerting/), and [Write-ahead log](/docs/concepts/write-ahead-log/) - Suspended materialized views and live views now point to their own `RESUME WAL` commands, since `ALTER TABLE` fails on them; [`wal_tables()`](/docs/query/functions/meta/#wal_tables) now lists live views - [ALTER MATERIALIZED VIEW RESUME WAL](/docs/query/sql/alter-mat-view-resume-wal/) - Corrected the Enterprise permission to `RESUME WAL` +- [Materialized views](/docs/concepts/materialized-views/#query-constraints) and [live views](/docs/query/sql/create-live-view/#query-constraints) - Documented that the Enterprise administrative functions, such as `permissions()`, `backups()`, and `reload_tls()`, are rejected in materialized and live view queries, and the error they return +- [RBAC](/docs/security/rbac/#replicas) - Documented how a read-only replica reloads replicated access lists, and the error a statement fails with when a reload does not finish within 30 seconds +- [Enterprise quick start](/docs/getting-started/enterprise-quick-start/) - Corrected the guidance for listing granted permissions: `all_permissions()` lists the available permissions, and `active_grants()` lists the grants - [Kubernetes Operator](/docs/enterprise-kubernetes-operator/releases/) - Updated for Operator 0.3.1, with upgrade notes on the removed convenience ClusterRoles and on applying the new CRDs first when `crd.enable=false`, the synced [API reference](/docs/enterprise-kubernetes-operator/reference/api/), and QuestDB Enterprise 4.0.1 as the tested image - [Kubernetes Operator permissions](/docs/enterprise-kubernetes-operator/installation/#operator-permissions) - Documented the cluster-wide RBAC the Operator is granted, which Secrets it reads and creates, why it never deletes a Secret, and that it has no object store or cloud permissions - [Kubernetes Operator backups left by departed instances](/docs/enterprise-kubernetes-operator/operations/backup-restore/#backups-left-by-departed-instances) - On Operator-managed clusters, backup prefixes of demoted or scaled-down instances are never pruned automatically. Covers when they are still needed as a seed or restore point, how to identify the safe ones to delete, and why age-based bucket lifecycle rules are risky diff --git a/documentation/concepts/live-views.md b/documentation/concepts/live-views.md index ce4b0c32b9..30d2edad8a 100644 --- a/documentation/concepts/live-views.md +++ b/documentation/concepts/live-views.md @@ -529,6 +529,11 @@ in [Base table lifecycle](#base-table-lifecycle). - **Deterministic queries only.** Non-deterministic functions such as `now()`, `sysdate()`, `systimestamp()`, and `rnd_*()` are rejected in the projection, the `WHERE` filter, and window-function arguments. +- **No administrative functions.** Enterprise functions that check the + caller's permissions, such as `permissions()` and `backup_instance_name()`, + are rejected, since a live view refreshes in the background. See + [query constraints](/docs/query/sql/create-live-view/#query-constraints) for + the full list. - **No TTL on the view.** Live-view disk growth is unbounded in this version. Base-table retention does not shrink the view, because `DROP PARTITION`, `TRUNCATE`, and TTL eviction on the base table leave the view's rows in place. diff --git a/documentation/concepts/materialized-views.md b/documentation/concepts/materialized-views.md index d0bd267710..7c40292d28 100644 --- a/documentation/concepts/materialized-views.md +++ b/documentation/concepts/materialized-views.md @@ -539,6 +539,11 @@ Materialized view queries: - Must use `SAMPLE BY` or `GROUP BY` with a designated timestamp column - Must not use `FROM-TO`, `FILL`, or `ALIGN TO FIRST OBSERVATION` - Must not use non-deterministic functions (`now()`, `rnd_uuid4()`) +- Must not use Enterprise administrative functions, which check the + permissions of the calling session and so cannot run in a background + refresh: `active_grants()`, `active_permissions()`, `backup_instance_name()`, + `backup_status()`, `backups()`, `permissions()`, `reload_tick_calendars()`, + and `reload_tls()` - Must use join conditions compatible with incremental refresh - When the base table uses [deduplication](/docs/concepts/deduplication/), non-aggregate columns must be a subset of the `DEDUP` keys diff --git a/documentation/getting-started/enterprise-quick-start.md b/documentation/getting-started/enterprise-quick-start.md index 6718948896..b4675f0fcd 100644 --- a/documentation/getting-started/enterprise-quick-start.md +++ b/documentation/getting-started/enterprise-quick-start.md @@ -186,7 +186,15 @@ 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()`. +To check the grants, query +[`active_grants()`](/docs/query/functions/access-control/#active_grants), +which lists the direct grants of every user, group, and service account. It is +available since QuestDB Enterprise 4.1.0: + +```questdb-sql +SELECT * FROM active_grants() +WHERE entity_name IN ('readonly', 'user1', 'user2'); +``` - For the full role-based access control docs, including group management, see the [RBAC operations guide](/docs/security/rbac/). diff --git a/documentation/query/functions/access-control.md b/documentation/query/functions/access-control.md new file mode 100644 index 0000000000..019983c5d6 --- /dev/null +++ b/documentation/query/functions/access-control.md @@ -0,0 +1,283 @@ +--- +title: Access control functions +sidebar_label: Access control +description: + Query QuestDB Enterprise permissions with SQL, from one entity's permissions + to the effective permissions and direct grants of every user, group, and + service account. +--- + +import { EnterpriseNote } from "@site/src/components/EnterpriseNote" + + + Access control functions are available in QuestDB Enterprise. + `active_grants()` and `active_permissions()` require version 4.1.0 or later. + + +These table functions return QuestDB Enterprise +[access control](/docs/security/rbac/) data as rows, so you can audit +permissions with SQL instead of running `SHOW PERMISSIONS` for one entity at a +time. Use them to find who can read a table or column, who can grant a +permission to others, or what a group passes on to its members. + +| Function | Returns | Required permissions | +| -------- | ------- | -------------------- | +| [`active_grants()`](#active_grants) | Direct grants of every user, group, and service account | `LIST USERS` and `USER DETAILS` | +| [`active_permissions()`](#active_permissions) | Effective permissions of every user, group, and service account, including those inherited from groups | `LIST USERS` and `USER DETAILS` | +| [`all_permissions()`](#all_permissions) | Every permission and the levels it can be granted at | None | +| [`permissions([entityName])`](#permissions) | Effective permissions of the current user or of a named entity | `USER DETAILS` for other entities | + +## active_grants + +`active_grants()` lists the permissions granted directly to each user, group, +and service account. Its rows match the `GRANT` statements in force, including +the permissions an entity received as the owner of a table it created. Use it +to review what has been granted, or to find the grant to revoke: a permission +that a user inherits from a group must be revoked from the group. + +It takes no arguments. To include the permissions each user inherits from its +groups, use [`active_permissions()`](#active_permissions). + +### Result columns + +- `entity_name` (`STRING`): name of the user, group, or service account. +- `entity_type` (`STRING`): `User`, `Group`, or `Service Account`. +- `permission` (`STRING`): permission name, as listed by + [`all_permissions()`](#all_permissions). +- `table_name` (`STRING`): table the permission is granted on. `null` for a + permission granted at database level: a database permission such as + `CREATE TABLE`, or a grant on all tables such as + `GRANT SELECT ON ALL TABLES`. +- `column_name` (`STRING`): column the permission is granted on. `null` for a + permission granted on a whole table or at database level. +- `grant_option` (`BOOLEAN`): `true` if the entity can grant the permission at + this scope to others. + +Each combination of entity, permission, table, and column appears once, and +grants on tables or columns that do not exist yet are included. A permission +appears on two scopes when its grant option is narrower than the permission. +After `GRANT SELECT ON ALL TABLES TO analyst`, then +`GRANT SELECT ON fx_trades TO analyst WITH GRANT OPTION`, `analyst` has a +database-level `SELECT` row with `grant_option` set to `false`, and a `SELECT` +row on `fx_trades` with `true`. + +### Required permissions + +`active_grants()` and `active_permissions()` list every entity, so they require +both `LIST USERS` and `USER DETAILS`, even to read your own rows. +`DATABASE ADMIN` includes both. A caller missing either gets +`Access denied for [LIST USERS]` or +`Access denied for [USER DETAILS]`. To check your own permissions +without them, use [`permissions()`](#permissions) or +[`SHOW PERMISSIONS`](/docs/query/sql/show/#show-permissions). + +### Limitations + +These apply to both `active_grants()` and `active_permissions()`: + +- The built-in admin and external (SSO/OIDC) users are not listed, as neither + has a stored access list. An external user gets the permissions of the + QuestDB groups [mapped](/docs/security/oidc/#mapping-user-permissions) to its + identity provider groups, so audit the rows of those groups. +- Group memberships and `ASSUME SERVICE ACCOUNT` grants are not listed, nor + the permissions a user gains by assuming a service account. Use + [`SHOW GROUPS userName`](/docs/query/sql/show/#show-groups) and + [`SHOW SERVICE ACCOUNTS userName`](/docs/query/sql/show/#show-service-accounts) + to see them. +- When access control is disabled with + [`acl.enabled=false`](/docs/configuration/iam/#aclenabled), the functions + return no rows. +- The functions cannot be used in a materialized view or a live view, which + refresh in the background without a session whose permissions could be + checked. Creating such a view fails with an error such as + `administrative function cannot be used in materialized view: active_grants`. + A regular view can call them, and checks the permissions of the user who + queries it. + +### Examples + +The examples on this page use these entities. `fx_orders` does not exist yet: + +```questdb-sql title="Example entities and grants" +CREATE TABLE fx_trades ( + timestamp TIMESTAMP_NS, + symbol SYMBOL, + ecn SYMBOL, + side SYMBOL, + price DOUBLE, + quantity DOUBLE +) TIMESTAMP(timestamp) PARTITION BY HOUR; + +CREATE GROUP fx_desk; +CREATE USER fx_trader; +ADD USER fx_trader TO fx_desk; +CREATE USER quant; +CREATE USER risk_manager; +CREATE SERVICE ACCOUNT fx_ingest; + +GRANT SELECT ON fx_trades TO fx_desk; +GRANT CREATE TABLE TO fx_trader; +GRANT SELECT ON fx_trades(symbol, price) TO quant; +GRANT SELECT ON ALL TABLES TO risk_manager WITH GRANT OPTION; +GRANT INSERT ON fx_trades, fx_orders TO fx_ingest; +``` + +```questdb-sql title="Direct grants of every entity" +SELECT * FROM active_grants() +ORDER BY entity_name, permission, table_name, column_name; +``` + +| entity_name | entity_type | permission | table_name | column_name | grant_option | +| ------------ | --------------- | ------------ | ---------- | ----------- | ------------ | +| fx_desk | Group | SELECT | fx_trades | | false | +| fx_ingest | Service Account | INSERT | fx_orders | | false | +| fx_ingest | Service Account | INSERT | fx_trades | | false | +| fx_trader | User | CREATE TABLE | | | false | +| quant | User | SELECT | fx_trades | price | false | +| quant | User | SELECT | fx_trades | symbol | false | +| risk_manager | User | SELECT | | | true | + +Each `GRANT` produces a row for every permission and every table or column it +names, and the database-level grant to `risk_manager` has no table name. + +```questdb-sql title="Grants that let the holder pass a permission on" +SELECT entity_name, permission, table_name, column_name +FROM active_grants() +WHERE grant_option; +``` + +| entity_name | permission | table_name | column_name | +| ------------ | ---------- | ---------- | ----------- | +| risk_manager | SELECT | | | + +## active_permissions + +`active_permissions()` lists the effective permissions of each user, group, +and service account: everything the entity is allowed to do, whether or not it +was granted directly. Use it to answer access questions across the database, +such as who can read a table or a column. + +It takes no arguments and returns the same [columns](#result-columns) as +`active_grants()`, with the same [required permissions](#required-permissions) +and [limitations](#limitations). In addition to the direct grants, it reports: + +- For each user, the permissions inherited from its groups. These rows repeat + the group's rows under the user's name. +- The [implicit permission](/docs/security/rbac/#implicit-permissions) on the + designated timestamp. An entity that holds `SELECT` or `UPDATE` on some + columns of a table holds the same permission on the table's designated + timestamp column, without the grant option. +- The permissions that `DATABASE ADMIN` includes: one database-level row per + permission, plus a table or column row only where it adds a grant option. + +### Examples + +These examples use the [entities above](#examples). + +```questdb-sql title="Effective permissions of every entity" +SELECT * FROM active_permissions() +ORDER BY entity_name, permission, table_name, column_name; +``` + +| entity_name | entity_type | permission | table_name | column_name | grant_option | +| ------------ | --------------- | ------------ | ---------- | ----------- | ------------ | +| fx_desk | Group | SELECT | fx_trades | | false | +| fx_ingest | Service Account | INSERT | fx_orders | | false | +| fx_ingest | Service Account | INSERT | fx_trades | | false | +| fx_trader | User | CREATE TABLE | | | false | +| fx_trader | User | SELECT | fx_trades | | false | +| quant | User | SELECT | fx_trades | price | false | +| quant | User | SELECT | fx_trades | symbol | false | +| quant | User | SELECT | fx_trades | timestamp | false | +| risk_manager | User | SELECT | | | true | + +Two rows are not direct grants. `fx_trader` inherits `SELECT` on `fx_trades` +from `fx_desk`, and `quant` holds `SELECT` on the designated timestamp because +it can read other columns of the table. + +To find who can read a table, include the database-level rows, where +`table_name` is `null`: + +```questdb-sql title="Who can read fx_trades" +SELECT entity_name, table_name, column_name, grant_option +FROM active_permissions() +WHERE permission = 'SELECT' + AND (table_name = 'fx_trades' OR table_name IS NULL) +ORDER BY entity_name, column_name; +``` + +| entity_name | table_name | column_name | grant_option | +| ------------ | ---------- | ----------- | ------------ | +| fx_desk | fx_trades | | false | +| fx_trader | fx_trades | | false | +| quant | fx_trades | price | false | +| quant | fx_trades | symbol | false | +| quant | fx_trades | timestamp | false | +| risk_manager | | | true | + +`quant` can read three columns of `fx_trades`, and `risk_manager` can read +every table and grant that access to others. + +## all_permissions + +`all_permissions()` lists every permission that can be granted, with the levels +it can be granted at. Use it to look up permission names for +[`GRANT`](/docs/query/sql/acl/grant/) and +[`REVOKE`](/docs/query/sql/acl/revoke/). It lists the permissions that exist, +not the ones granted: for those, use [`active_grants()`](#active_grants). Any +user can call it. + +It takes no arguments and returns: + +- `permission` (`STRING`): permission name. +- `level` (`STRING`): levels at which the permission can be granted: + `Database`, `Database|Table`, or `Database|Table|Column`. + +```questdb-sql title="Levels of three permissions" +SELECT * FROM all_permissions() +WHERE permission IN ('CREATE TABLE', 'INSERT', 'SELECT'); +``` + +| permission | level | +| ------------ | --------------------------------- | +| SELECT | Database|Table|Column | +| INSERT | Database|Table | +| CREATE TABLE | Database | + +## permissions + +`permissions()` lists the effective permissions of the current user, and +`permissions(entityName)` those of the named user, group, or service account. +It returns the same rows as +[`SHOW PERMISSIONS`](/docs/query/sql/show/#show-permissions), as a table +function that you can join or use in a subquery. + +It takes an optional argument: + +- `entityName` (optional, `STRING`): user, group, or service account to + inspect. Default: the current user. + +It returns these columns: + +- `permission`, `table_name`, `column_name`, and `grant_option`, as described + in [Result columns](#result-columns). +- `origin` (`STRING`): `G` for a granted permission, whether direct or + inherited from a group, and `I` for an + [implicit permission](/docs/security/rbac/#implicit-permissions) on the + designated timestamp column. + +Unlike `active_permissions()`, it omits grants on tables or columns that do not +exist yet. Inspecting an entity other than yourself, one of your groups, or a +service account you can assume requires `USER DETAILS`. Like +`active_permissions()`, it cannot be used in a materialized view or a live +view. + +```questdb-sql title="Permissions of quant" +SELECT * FROM permissions('quant'); +``` + +| permission | table_name | column_name | grant_option | origin | +| ---------- | ---------- | ----------- | ------------ | ------ | +| SELECT | fx_trades | symbol | false | G | +| SELECT | fx_trades | price | false | G | +| SELECT | fx_trades | timestamp | false | I | diff --git a/documentation/query/sql/acl/grant.md b/documentation/query/sql/acl/grant.md index 3a2e17b45c..0cd5f3606b 100644 --- a/documentation/query/sql/acl/grant.md +++ b/documentation/query/sql/acl/grant.md @@ -302,6 +302,9 @@ Such permissions do not show on `SHOW PERMISSIONS` output. | permission | table_name | column_name | grant_option | origin | | ---------- | ---------- | ----------- | ------------ | ------ | +To see them before the table or column exists, query +[`active_grants()`](/docs/query/functions/access-control/#active_grants). + However, when the table is created, then the applicable permissions appear: ```questdb-sql diff --git a/documentation/query/sql/create-live-view.md b/documentation/query/sql/create-live-view.md index 17e09e2ecd..0dcafd1c56 100644 --- a/documentation/query/sql/create-live-view.md +++ b/documentation/query/sql/create-live-view.md @@ -265,6 +265,10 @@ The view query is validated at creation time and must: `WHERE` predicates are supported. - Not use non-deterministic functions such as `now()`, `sysdate()`, `systimestamp()`, or `rnd_*()`. +- Not use Enterprise administrative functions, which check the permissions of + the calling session: `active_grants()`, `active_permissions()`, + `backup_instance_name()`, `backup_status()`, `backups()`, `permissions()`, + `reload_tick_calendars()`, and `reload_tls()`. - Not read another live view. ## Examples @@ -463,6 +467,7 @@ the query. | `wildcard column select is not allowed in live view queries` | The top-level projection contains `*` | | `live view select cannot filter on the designated timestamp yet` | The `WHERE` clause filters the designated timestamp | | `non-deterministic function cannot be used in live view` | The query uses `now()`, `rnd_*()`, or a similar non-deterministic function | +| `administrative function cannot be used in live view: ` | The query calls an Enterprise administrative function, such as `permissions()` or `reload_tls()` | ### Window errors diff --git a/documentation/query/sql/create-mat-view.md b/documentation/query/sql/create-mat-view.md index 0dd9739535..26abd97565 100644 --- a/documentation/query/sql/create-mat-view.md +++ b/documentation/query/sql/create-mat-view.md @@ -326,6 +326,8 @@ Materialized view queries must: - Use `SAMPLE BY` or `GROUP BY` with designated timestamp - Not use `FROM-TO`, `FILL`, or `ALIGN TO FIRST OBSERVATION` - Not use non-deterministic functions (`now()`, `rnd_*`) +- Not use Enterprise administrative functions, such as `permissions()` or + `backups()` See [query constraints](/docs/concepts/materialized-views/#query-constraints) for the full list. @@ -390,6 +392,7 @@ GRANT DROP MATERIALIZED VIEW ON trades_hourly TO user1; | `materialized view already exists` | View exists and `IF NOT EXISTS` not specified | | `base table does not exist` | Referenced table doesn't exist | | `query is not supported` | Query doesn't meet constraints (missing SAMPLE BY, uses FILL, etc.) | +| `administrative function cannot be used in materialized view: ` | The query calls an Enterprise administrative function, such as `active_permissions()` or `backups()` | | `permission denied` | Missing required permission (Enterprise) | ## See also diff --git a/documentation/query/sql/show.md b/documentation/query/sql/show.md index d25e03a2d5..b540680c1f 100644 --- a/documentation/query/sql/show.md +++ b/documentation/query/sql/show.md @@ -452,7 +452,12 @@ full column list, including `hasParquetGenerated`, `isParquet`, ### SHOW PERMISSIONS FOR CURRENT USER -_Enterprise only._ +_Enterprise only._ `SHOW PERMISSIONS` lists the effective permissions of one +user, group, or service account. The +[`permissions()`](/docs/query/functions/access-control/#permissions) table +function returns the same rows, and +[`active_permissions()`](/docs/query/functions/access-control/#active_permissions) +lists the permissions of every entity at once. ```questdb-sql SHOW PERMISSIONS; diff --git a/documentation/security/rbac.md b/documentation/security/rbac.md index 62dd034475..961323a8f5 100644 --- a/documentation/security/rbac.md +++ b/documentation/security/rbac.md @@ -511,6 +511,24 @@ ASSUME SERVICE ACCOUNT ingest_app; EXIT SERVICE ACCOUNT; ``` +### Access control on replicas {#replicas} + +Users, groups, service accounts, and their permissions are managed on the +primary. A read-only [replica](/docs/high-availability/overview/) rejects +statements such as `CREATE USER` and `GRANT`, and receives the changes through +replication instead. Before it authorizes a statement, the replica reloads its +access lists if new changes have arrived. + +A statement that finds another session already reloading waits for that reload +for up to 30 seconds. If the access lists are still out of date after that, the +statement fails instead of being authorized against outdated permissions: + +```text +timed out waiting for the access lists to refresh [timeout=30000ms] +``` + +Retry the statement. + ## User management reference {#user-management} ### Creating and removing principals @@ -583,11 +601,22 @@ JWK Token false REST Token true ``` +To audit permissions across all users, groups, and service accounts in one +query, use the +[access control functions](/docs/query/functions/access-control/): + +```questdb-sql +SELECT * FROM active_permissions(); -- Effective, including group permissions +SELECT * FROM active_grants(); -- Direct grants only +``` + :::note Viewing other users' information requires `LIST USERS` (to list all) or `USER DETAILS` (to see details) permissions. Users can always view their own -information without these permissions. +information without these permissions. `active_permissions()` and +`active_grants()` list the permissions of every entity, so they require both, +even to read your own rows. ::: @@ -798,7 +827,8 @@ again at startup. ## Permissions reference {#permissions} -Use `all_permissions()` to see all available permissions: +Use [`all_permissions()`](/docs/query/functions/access-control/#all_permissions) +to list all available permissions and the levels they can be granted at: ```questdb-sql SELECT * FROM all_permissions(); diff --git a/documentation/sidebars.js b/documentation/sidebars.js index 3f1b5f0e73..0a2b35300e 100644 --- a/documentation/sidebars.js +++ b/documentation/sidebars.js @@ -497,6 +497,7 @@ module.exports = { type: "category", label: "Functions", items: [ + "query/functions/access-control", "query/functions/aggregation", "query/functions/array", "query/functions/binary", From e60afdbd4d421c0eae1d6429d831a1a982f0927d Mon Sep 17 00:00:00 2001 From: glasstiger Date: Fri, 23 Oct 2026 13:29:02 +0100 Subject: [PATCH 2/4] address review comments --- .../query/functions/access-control.md | 109 ++++++++++++++---- documentation/query/sql/acl/grant.md | 4 +- 2 files changed, 88 insertions(+), 25 deletions(-) diff --git a/documentation/query/functions/access-control.md b/documentation/query/functions/access-control.md index 019983c5d6..954fb768f4 100644 --- a/documentation/query/functions/access-control.md +++ b/documentation/query/functions/access-control.md @@ -54,12 +54,12 @@ groups, use [`active_permissions()`](#active_permissions). this scope to others. Each combination of entity, permission, table, and column appears once, and -grants on tables or columns that do not exist yet are included. A permission -appears on two scopes when its grant option is narrower than the permission. -After `GRANT SELECT ON ALL TABLES TO analyst`, then -`GRANT SELECT ON fx_trades TO analyst WITH GRANT OPTION`, `analyst` has a -database-level `SELECT` row with `grant_option` set to `false`, and a `SELECT` -row on `fx_trades` with `true`. +grants on tables or columns that do not exist yet are included. One permission +can still have two rows when its grant option covers less than the permission +does. In the [examples](#examples), `analyst` holds `SELECT` on all tables and +the grant option on `fx_trades` only, so it has a database-level `SELECT` row +with `grant_option` set to `false`, and a `SELECT` row on `fx_trades` with +`true`. ### Required permissions @@ -84,6 +84,12 @@ These apply to both `active_grants()` and `active_permissions()`: [`SHOW GROUPS userName`](/docs/query/sql/show/#show-groups) and [`SHOW SERVICE ACCOUNTS userName`](/docs/query/sql/show/#show-service-accounts) to see them. +- Access through a view is not shown on the tables the view reads. A view + reads them with the permissions of its + [definer](/docs/concepts/views/#definer-security-model-enterprise), so a user + with `SELECT` on a view over `fx_trades` can read `fx_trades` data without any + permission on `fx_trades`. To audit who can read a table, also check who holds + `SELECT` on the views that read it. - When access control is disabled with [`acl.enabled=false`](/docs/configuration/iam/#aclenabled), the functions return no rows. @@ -91,8 +97,8 @@ These apply to both `active_grants()` and `active_permissions()`: refresh in the background without a session whose permissions could be checked. Creating such a view fails with an error such as `administrative function cannot be used in materialized view: active_grants`. - A regular view can call them, and checks the permissions of the user who - queries it. + A regular view can call them. Unlike the tables it reads, the functions check + the permissions of the user who queries the view, not those of its definer. ### Examples @@ -113,12 +119,15 @@ CREATE USER fx_trader; ADD USER fx_trader TO fx_desk; CREATE USER quant; CREATE USER risk_manager; +CREATE USER analyst; CREATE SERVICE ACCOUNT fx_ingest; GRANT SELECT ON fx_trades TO fx_desk; GRANT CREATE TABLE TO fx_trader; GRANT SELECT ON fx_trades(symbol, price) TO quant; GRANT SELECT ON ALL TABLES TO risk_manager WITH GRANT OPTION; +GRANT SELECT ON ALL TABLES TO analyst; +GRANT SELECT ON fx_trades TO analyst WITH GRANT OPTION; GRANT INSERT ON fx_trades, fx_orders TO fx_ingest; ``` @@ -129,6 +138,8 @@ ORDER BY entity_name, permission, table_name, column_name; | entity_name | entity_type | permission | table_name | column_name | grant_option | | ------------ | --------------- | ------------ | ---------- | ----------- | ------------ | +| analyst | User | SELECT | | | false | +| analyst | User | SELECT | fx_trades | | true | | fx_desk | Group | SELECT | fx_trades | | false | | fx_ingest | Service Account | INSERT | fx_orders | | false | | fx_ingest | Service Account | INSERT | fx_trades | | false | @@ -137,17 +148,20 @@ ORDER BY entity_name, permission, table_name, column_name; | quant | User | SELECT | fx_trades | symbol | false | | risk_manager | User | SELECT | | | true | -Each `GRANT` produces a row for every permission and every table or column it -names, and the database-level grant to `risk_manager` has no table name. +In this example, each `GRANT` produces a row for every permission and every +table or column it names, and the database-level grants to `analyst` and +`risk_manager` have no table name. ```questdb-sql title="Grants that let the holder pass a permission on" SELECT entity_name, permission, table_name, column_name FROM active_grants() -WHERE grant_option; +WHERE grant_option +ORDER BY entity_name; ``` | entity_name | permission | table_name | column_name | | ------------ | ---------- | ---------- | ----------- | +| analyst | SELECT | fx_trades | | | risk_manager | SELECT | | | ## active_permissions @@ -161,14 +175,20 @@ It takes no arguments and returns the same [columns](#result-columns) as `active_grants()`, with the same [required permissions](#required-permissions) and [limitations](#limitations). In addition to the direct grants, it reports: -- For each user, the permissions inherited from its groups. These rows repeat - the group's rows under the user's name. +- For each user, the permissions inherited from its groups, merged with the + user's own permissions. A broader permission absorbs a narrower one unless + the narrower one adds the grant option, and rows on the same scope merge into + one row, which has the grant option if any of them has it. So a direct grant + can be missing from `active_permissions()` when a group grants more. - The [implicit permission](/docs/security/rbac/#implicit-permissions) on the designated timestamp. An entity that holds `SELECT` or `UPDATE` on some columns of a table holds the same permission on the table's designated timestamp column, without the grant option. -- The permissions that `DATABASE ADMIN` includes: one database-level row per - permission, plus a table or column row only where it adds a grant option. +- The permissions that `DATABASE ADMIN` includes: a database-level row for + every permission that [`all_permissions()`](#all_permissions) lists, + `DATABASE ADMIN` included, with the grant option of the `DATABASE ADMIN` + grant. A table or column grant of the same entity keeps its own row only + where it adds the grant option. ### Examples @@ -181,6 +201,8 @@ ORDER BY entity_name, permission, table_name, column_name; | entity_name | entity_type | permission | table_name | column_name | grant_option | | ------------ | --------------- | ------------ | ---------- | ----------- | ------------ | +| analyst | User | SELECT | | | false | +| analyst | User | SELECT | fx_trades | | true | | fx_desk | Group | SELECT | fx_trades | | false | | fx_ingest | Service Account | INSERT | fx_orders | | false | | fx_ingest | Service Account | INSERT | fx_trades | | false | @@ -193,7 +215,29 @@ ORDER BY entity_name, permission, table_name, column_name; Two rows are not direct grants. `fx_trader` inherits `SELECT` on `fx_trades` from `fx_desk`, and `quant` holds `SELECT` on the designated timestamp because -it can read other columns of the table. +it can read other columns of the table. To list such rows, subtract +`active_grants()` from `active_permissions()`: + +```questdb-sql title="Effective permissions that are not direct grants" +SELECT entity_name, permission, table_name, column_name, grant_option +FROM active_permissions() +EXCEPT +SELECT entity_name, permission, table_name, column_name, grant_option +FROM active_grants() +ORDER BY entity_name; +``` + +| entity_name | permission | table_name | column_name | grant_option | +| ----------- | ---------- | ---------- | ----------- | ------------ | +| fx_trader | SELECT | fx_trades | | false | +| quant | SELECT | fx_trades | timestamp | false | + +Each row comes from a group, from the implicit permission on a designated +timestamp, or from `DATABASE ADMIN`. Comparing `grant_option` as well catches +a grant option that a user inherits on a scope it also holds directly. To find +the group a row comes from, list the user's groups with +[`SHOW GROUPS userName`](/docs/query/sql/show/#show-groups), then look up their +rows in `active_grants()`. To find who can read a table, include the database-level rows, where `table_name` is `null`: @@ -203,11 +247,13 @@ SELECT entity_name, table_name, column_name, grant_option FROM active_permissions() WHERE permission = 'SELECT' AND (table_name = 'fx_trades' OR table_name IS NULL) -ORDER BY entity_name, column_name; +ORDER BY entity_name, table_name, column_name; ``` | entity_name | table_name | column_name | grant_option | | ------------ | ---------- | ----------- | ------------ | +| analyst | | | false | +| analyst | fx_trades | | true | | fx_desk | fx_trades | | false | | fx_trader | fx_trades | | false | | quant | fx_trades | price | false | @@ -215,8 +261,11 @@ ORDER BY entity_name, column_name; | quant | fx_trades | timestamp | false | | risk_manager | | | true | -`quant` can read three columns of `fx_trades`, and `risk_manager` can read -every table and grant that access to others. +`analyst` and `risk_manager` can read every table, and `quant` can read three +columns of `fx_trades`. `risk_manager` can grant that access to others, and +`analyst` can grant `SELECT` on `fx_trades` only. The result leaves out access +that these functions do not show, such as reading `fx_trades` through a view. +See [limitations](#limitations). ## all_permissions @@ -267,10 +316,12 @@ It returns these columns: designated timestamp column. Unlike `active_permissions()`, it omits grants on tables or columns that do not -exist yet. Inspecting an entity other than yourself, one of your groups, or a -service account you can assume requires `USER DETAILS`. Like -`active_permissions()`, it cannot be used in a materialized view or a live -view. +exist yet, and a table or column row that only adds the grant option to a +broader permission. To check who can grant a permission, use +[`active_permissions()`](#active_permissions). Inspecting an entity other than +yourself, one of your groups, or a service account you can assume requires +`USER DETAILS`. Like `active_permissions()`, it cannot be used in a +materialized view or a live view. ```questdb-sql title="Permissions of quant" SELECT * FROM permissions('quant'); @@ -281,3 +332,15 @@ SELECT * FROM permissions('quant'); | SELECT | fx_trades | symbol | false | G | | SELECT | fx_trades | price | false | G | | SELECT | fx_trades | timestamp | false | I | + +```questdb-sql title="Permissions of analyst" +SELECT * FROM permissions('analyst'); +``` + +| permission | table_name | column_name | grant_option | origin | +| ---------- | ---------- | ----------- | ------------ | ------ | +| SELECT | | | false | G | + +`analyst` can grant `SELECT` on `fx_trades`, but `permissions('analyst')` does +not show it. `active_permissions()` lists that grant option as a separate row +on `fx_trades`. diff --git a/documentation/query/sql/acl/grant.md b/documentation/query/sql/acl/grant.md index 0cd5f3606b..38368585f6 100644 --- a/documentation/query/sql/acl/grant.md +++ b/documentation/query/sql/acl/grant.md @@ -260,8 +260,8 @@ GRANT UPDATE ON products(id) TO john; When granting permissions on the table or column level, sometimes it might seem like there is no effect when cross-checking with the `SHOW permissions` command. If QuestDB detects that the permission is already granted on a higher level, it -optimizes and removes any child permissions. Doing so keeps the access list -model simple and permission checks faster. +optimizes and removes any child permissions that do not add the grant option. +Doing so keeps the access list model simple and permission checks faster. For example, granting the same permission on the database and table level shows will show the permission on database level only: From a916bd25d8e77fdbcb5ba8ad5edc4c1eb39e9065 Mon Sep 17 00:00:00 2001 From: glasstiger Date: Fri, 23 Oct 2026 14:34:36 +0100 Subject: [PATCH 3/4] address review comments --- documentation/concepts/views.md | 5 +++++ documentation/query/functions/access-control.md | 14 +++++++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/documentation/concepts/views.md b/documentation/concepts/views.md index 5ef4b9be97..b7b9a6d1ff 100644 --- a/documentation/concepts/views.md +++ b/documentation/concepts/views.md @@ -382,6 +382,11 @@ SELECT * FROM sensitive_sales; -- Access denied! The view's definer permissions are preserved even if the creator's account is later disabled or deleted. +Access control functions such as `active_grants()` and `active_permissions()` +are an exception: in a view, they check the permissions of the user who queries +the view, not those of its definer. See +[access control functions](/docs/query/functions/access-control/#limitations). + ### No column-level permissions on views Unlike tables, views do **not** support column-level permissions. You can only diff --git a/documentation/query/functions/access-control.md b/documentation/query/functions/access-control.md index 954fb768f4..02c5a3cdcb 100644 --- a/documentation/query/functions/access-control.md +++ b/documentation/query/functions/access-control.md @@ -79,6 +79,14 @@ These apply to both `active_grants()` and `active_permissions()`: has a stored access list. An external user gets the permissions of the QuestDB groups [mapped](/docs/security/oidc/#mapping-user-permissions) to its identity provider groups, so audit the rows of those groups. +- A row shows a permission, not that the entity can connect. Disabled users + and service accounts are listed with their permissions: check the `enabled` + column of [`SHOW USERS`](/docs/query/sql/show/#show-users) and + [`SHOW SERVICE ACCOUNTS`](/docs/query/sql/show/#show-service-accounts). A + user or service account also needs an + [endpoint permission](/docs/security/rbac/#endpoint-permissions) such as + `HTTP` or `PGWIRE` to connect. Endpoint permissions appear as database-level + rows. - Group memberships and `ASSUME SERVICE ACCOUNT` grants are not listed, nor the permissions a user gains by assuming a service account. Use [`SHOW GROUPS userName`](/docs/query/sql/show/#show-groups) and @@ -148,9 +156,9 @@ ORDER BY entity_name, permission, table_name, column_name; | quant | User | SELECT | fx_trades | symbol | false | | risk_manager | User | SELECT | | | true | -In this example, each `GRANT` produces a row for every permission and every -table or column it names, and the database-level grants to `analyst` and -`risk_manager` have no table name. +The database-level grants to `analyst` and `risk_manager` have no table name, +`fx_ingest` has a row for `fx_orders` although the table does not exist yet, +and `quant` has one row per granted column. ```questdb-sql title="Grants that let the holder pass a permission on" SELECT entity_name, permission, table_name, column_name From b75e97f32c56b852bf89fba309ad8ff9c3684307 Mon Sep 17 00:00:00 2001 From: glasstiger Date: Sat, 24 Oct 2026 17:44:34 +0100 Subject: [PATCH 4/4] address review comments --- documentation/query/functions/access-control.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/documentation/query/functions/access-control.md b/documentation/query/functions/access-control.md index 02c5a3cdcb..7be2bf1d6f 100644 --- a/documentation/query/functions/access-control.md +++ b/documentation/query/functions/access-control.md @@ -194,9 +194,11 @@ and [limitations](#limitations). In addition to the direct grants, it reports: timestamp column, without the grant option. - The permissions that `DATABASE ADMIN` includes: a database-level row for every permission that [`all_permissions()`](#all_permissions) lists, - `DATABASE ADMIN` included, with the grant option of the `DATABASE ADMIN` - grant. A table or column grant of the same entity keeps its own row only - where it adds the grant option. + `DATABASE ADMIN` included. If `DATABASE ADMIN` was granted with + `WITH GRANT OPTION`, every expanded row has the grant option. Otherwise, + separately granted database-level grant options are preserved. A table or + column grant of the same entity keeps its own row only where it adds the + grant option. ### Examples