diff --git a/documentation/changelog.mdx b/documentation/changelog.mdx
index 49b052e30..f99fa5783 100644
--- a/documentation/changelog.mdx
+++ b/documentation/changelog.mdx
@@ -22,6 +22,7 @@ This page tracks significant updates to the QuestDB documentation.
- [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()`
- [Audited views](/docs/security/audited-views/) - QuestDB Enterprise views created `WITH AUDIT` keep a best-effort audit trail of their reads in `sys.view_audit`: who read the view, when, and with which `AUDITED` parameter values, plus the [`view.audit.*` settings](/docs/configuration/audited-views/)
- [Declared value lists](/docs/query/sql/declare/#value-lists) - `DECLARE @symbols := ('BTC-USDT', 'ETH-USDT')` names the values of an `IN` filter once, including lists of bind variables and list parameters in views
+- [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
@@ -53,6 +54,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 ce4b0c32b..30d2edad8 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 d0bd26771..7c40292d2 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/concepts/views.md b/documentation/concepts/views.md
index 9b4d98011..235d8f556 100644
--- a/documentation/concepts/views.md
+++ b/documentation/concepts/views.md
@@ -408,6 +408,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/getting-started/enterprise-quick-start.md b/documentation/getting-started/enterprise-quick-start.md
index 53d5591ab..aed0c7d96 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 000000000..7be2bf1d6
--- /dev/null
+++ b/documentation/query/functions/access-control.md
@@ -0,0 +1,356 @@
+---
+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. 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
+
+`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.
+- 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
+ [`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.
+- 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. Unlike the tables it reads, the functions check
+ the permissions of the user who queries the view, not those of its definer.
+
+### 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 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;
+```
+
+```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 |
+| ------------ | --------------- | ------------ | ---------- | ----------- | ------------ |
+| 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 |
+| 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 |
+
+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 active_grants()
+WHERE grant_option
+ORDER BY entity_name;
+```
+
+| entity_name | permission | table_name | column_name |
+| ------------ | ---------- | ---------- | ----------- |
+| analyst | SELECT | fx_trades | |
+| 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, 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: a database-level row for
+ every permission that [`all_permissions()`](#all_permissions) lists,
+ `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
+
+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 |
+| ------------ | --------------- | ------------ | ---------- | ----------- | ------------ |
+| 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 |
+| 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 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`:
+
+```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, 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 |
+| quant | fx_trades | symbol | false |
+| quant | fx_trades | timestamp | false |
+| risk_manager | | | true |
+
+`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
+
+`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, 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');
+```
+
+| 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 |
+
+```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 3a2e17b45..38368585f 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:
@@ -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 17e09e2ec..0dcafd1c5 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 3dc80162a..7434cb710 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.
@@ -395,6 +397,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 eac94fb35..8cfc8cae0 100644
--- a/documentation/query/sql/show.md
+++ b/documentation/query/sql/show.md
@@ -469,7 +469,12 @@ new trailing columns.
### 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 52cefca0c..de626f5f9 100644
--- a/documentation/security/rbac.md
+++ b/documentation/security/rbac.md
@@ -528,6 +528,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
@@ -600,11 +618,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.
:::
@@ -817,7 +846,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 ed5efe484..e5bae7de7 100644
--- a/documentation/sidebars.js
+++ b/documentation/sidebars.js
@@ -541,6 +541,7 @@ module.exports = {
type: "category",
label: "Functions",
items: [
+ "query/functions/access-control",
"query/functions/aggregation",
"query/functions/array",
"query/functions/binary",