ClickHouse / ClickHouse/ClickHouse

HTTP session does not pick up newly granted nested role privileges until reconnect

Open
#114,788 0 comments 0 reactions 0 assignees View on GitHub
comp-rbac external
Dominant language
C++
Stars
49.9k
Forks
9k
Avg merge
21h 32m
Merged PRs (30d)
515

Description

### Company or project name

Local reproducible test case

### Describe the bug

An existing HTTP session does not pick up privileges from a role granted to the user after the session was opened.

If the user opens an HTTP session first, then an administrator grants a role to that user, the same HTTP session still uses the old active role set and fails access checks. A new HTTP session immediately sees the newly granted role and the same query succeeds.

This is especially visible with `clusterAllReplicas(..., system.tables)` and a subquery reading an ordinary table. The old session fails with `ACCESS_DENIED` for the table used in the subquery, even though the role granted after session creation provides that privilege through a nested role.

Setting `push_external_roles_in_interserver_queries=0` does not help this scenario because the failure happens before remote execution: the current session does not refresh its active roles/privileges.

### How to reproduce

Tested on ClickHouse `26.7.3.19` official build.

Minimal topology: multi-replica cluster, HTTP interface enabled, access management enabled.

Create test objects and nested roles:

```sql
CREATE DATABASE role_cache_repro ON CLUSTER 'test_cluster';

CREATE TABLE role_cache_repro.test_table ON CLUSTER 'test_cluster'
(
name String
)
ENGINE = ReplicatedMergeTree('/clickhouse/tables/{uuid}/{shard}', '{replica}')
ORDER BY name;

INSERT INTO role_cache_repro.test_table VALUES ('role_cache_repro');

CREATE ROLE role_cache_main;
CREATE ROLE role_cache_subrole;

GRANT REMOTE ON *.* TO role_cache_subrole;
GRANT SELECT ON system.tables TO role_cache_subrole;
GRANT SELECT ON role_cache_repro.* TO role_cache_subrole;
GRANT role_cache_subrole TO role_cache_main;

CREATE USER role_cache_user IDENTIFIED BY 'password';
```

Open an HTTP session before granting the role:

```bash
curl -u role_cache_user:password \
'http://localhost:8123/?session_id=role-cache-session' \
--data-binary "SELECT currentUser(), currentRoles()"
```

Grant the role after the session already exists:

```sql
GRANT role_cache_main TO role_cache_user;
```

In the same HTTP session, roles are still stale:

```bash
curl -u role_cache_user:password \
'http://localhost:8123/?session_id=role-cache-session' \
--data-binary "SELECT currentUser(), currentRoles()"
```

Run the query in the same old HTTP session:

```bash
curl -u role_cache_user:password \
'http://localhost:8123/?session_id=role-cache-session' \
--data-binary "
SELECT
hostName(),
t.database,
t.name
FROM clusterAllReplicas('test_cluster', system.tables) AS t
WHERE t.database IN
(
SELECT name
FROM role_cache_repro.test_table
)
FORMAT TSVRaw"
```

Actual result in the old session:

```text
Code: 497. DB::Exception: role_cache_user: Not enough privileges. To execute this query, it's necessary to have the grant SELECT(name) ON role_cache_repro.test_table. (ACCESS_DENIED)
```

Open a new HTTP session and run the same query:

```bash
curl -u role_cache_user:password \
'http://localhost:8123/?session_id=role-cache-session-new' \
--data-binary "
SELECT
hostName(),
t.database,
t.name
FROM clusterAllReplicas('test_cluster', system.tables) AS t
WHERE t.database IN
(
SELECT name
FROM role_cache_repro.test_table
)
FORMAT TSVRaw"
```

The new session succeeds and returns rows from all replicas.

### Expected behavior

After a role is granted to a user, existing HTTP sessions should either:

1. refresh the active role/privilege set before subsequent queries, or
2. clearly document that HTTP sessions cache role grants until reconnect/session expiration.

The behavior is surprising because reconnecting immediately fixes the access check without any other access-control changes.

### Error message and/or stacktrace

From `system.query_log` for the old session:

```text
type: ExceptionBeforeStart
exception_code: 497
exception: Code: 497. DB::Exception: role_cache_user: Not enough privileges. To execute this query, it's necessary to have the grant SELECT(name) ON role_cache_repro.test_table. (ACCESS_DENIED)
used_privileges: ['SELECT ON system.tables','SHOW COLUMNS ON system.tables','READ ON REMOTE']
missing_privileges: ['SELECT(name) ON role_cache_repro.test_table']
```

For the new session, the same query finishes successfully and `used_privileges` includes:

```text
SELECT(name) ON role_cache_repro.test_table
SELECT(database, name) ON system.tables
READ ON REMOTE
```

### Additional context

The same result was observed with nested roles:

```sql
GRANT role_cache_subrole TO role_cache_main;
GRANT role_cache_main TO role_cache_user;
```

The issue is not fixed by adding:

```sql
SETTINGS push_external_roles_in_interserver_queries = 0
```

because the old-session failure is `ExceptionBeforeStart` on the initiator, before remote execution.

Contributor guide

Open the contributing guide

Research direction

Start with the HTTP interface reproduction using the documented session_id requests, then inspect system.query_log for the old session's ExceptionBeforeStart and missing privileges. The payload names no source files or tests; done means an existing session reflects the newly granted nested role, or the caching behavior is clearly documented.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp, sql
Domain
authorization, backend, databases
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.