metabase / metabase/metabase

Model-sourced queries are authorized by the backend but unreachable in the UI when Create queries is "No"

Open
#79,438 1 comment 0 reactions 0 assignees View on GitHub
.Team/Graphy Priority:P2
Dominant language
Clojure
Stars
49.3k
Forks
6.8k
Avg merge
1d 13h
Merged PRs (30d)
653

Description

### Describe the bug

The query processor deliberately treats a **model as the permission boundary**: when a query's primary source is a card, the required permissions are *only* that card's collection read permissions. The underlying tables' `view-data` and `create-queries` are never consulted.

But a user in exactly that position cannot build such a query in the UI. `GET /api/database` omits the database entirely, so the notebook editor treats the query as non-editable. The frontend never sends the query, so there is no server-side error and nothing in the logs to diagnose.

Net effect: **the backend authorizes a capability that the product makes unreachable.** `POST /api/dataset` returns 202 with data for a model-sourced query, while the UI shows a permission error for the same query.

The only way to reach it is to grant `create-queries: query-builder` on at least one table of that database — deliberately sacrificing a table — which is plainly not an intended configuration.

Related: #74873 requests exactly this capability as a new feature ("There is no option that allows a table to be queried in the background through a model while remaining completely hidden from the UI for non-admin users"). This report shows the query processor **already implements** that middle ground; only database visibility blocks it.

### To Reproduce

Reproducible on the **Sample Database** alone.

1. As an admin, create a **model** from the Sample Database (e.g. from `Orders`) and save it into a collection, e.g. `Models`.
2. Create a group `ModelOnly` and add a non-admin user to it.
3. **Admin → Permissions → Data → Sample Database**:
- `ModelOnly` → View data: **Can view**, Create queries: **No**
- `All Users` → Create queries: **No** (otherwise the most-permissive rule overrides it)
4. **Admin → Permissions → Collections** → give `ModelOnly` **read** access to `Models`.
5. Sign in as that user and run the three API calls below (or try to build a new question from the model in the UI).

**1. The backend authorizes the model-sourced query — 202, rows returned:**

```
POST /api/dataset
{"database":,"type":"query","query":{"source-table":"card__","limit":1}}

→ 202, data returned
```

**2. Direct access to the underlying table is correctly denied — 403:**

```
POST /api/dataset
{"database":,"type":"query","query":{"source-table":,"limit":1}}

→ 403 {"error_type":"missing-required-permissions"}
```

Joining a raw table onto the model is also correctly denied with 403, so the boundary itself holds.

**3. But the database is invisible to the user:**

```
GET /api/database
→ the model's database is absent from the list
```

**4. And the card's own metadata shows the gap — `db_id` is set, but the `db` object is null:**

```
GET /api/table/card__/query_metadata
→ {"id":"card__", "db_id":, "db":null, ...}
```

Because the frontend metadata provider has no database entity for that id, `editable?` fails on its first condition and the notebook is disabled.

#### UI symptom

With **two** databases — the model's database configured as above, and any other database left at `Query builder` or higher — the "New Question" entry point stays visible (`can-create-queries` is computed across all databases). Picking the model then loads `GET /api/table/card__/query_metadata` with 200, after which the notebook is dead: **no `POST /api/dataset` is ever sent** and a permission error is shown.

With a single database the bug is the same, but the "New Question" entry disappears entirely, which obscures where the problem is.

### Expected behavior

The notebook opens with the model as its source, and the question can be built and run — matching the backend, which already authorizes exactly this query.

Alternatively, if "Create queries: No" is intended to block model-sourced ad-hoc queries too (as [the docs](https://www.metabase.com/docs/latest/permissions/data) suggest: *"users cannot create new questions or modify existing ones through querying"*), then `POST /api/dataset` should not return 202 for them either. Either way the two layers should agree.

### Logs

No server-side error is produced. The server log for the failing UI action ends at:

```
GET /api/collection//items 200
GET /api/table/card__/query_metadata 200
(nothing further — no POST /api/dataset)
```

The only 403s in the session are unrelated (`GET /api/collection/root`, `GET /api/setting/version-info`).

#### Root cause

**Backend — the model is the permission boundary** (`src/metabase/query_permissions/impl.clj`, `legacy-mbql-required-perms`):

```clojure
;; if we are using a Card as our source, our perms are that Card's (i.e. that Card's Collection's) read perms
(if-let [source-card-id (... lib/primary-source-card-id)]
{:paths (source-card-read-perms source-card-id)}
;; otherwise ... calculate perms based on the Tables referenced in the query
...)
```

**Frontend — editability requires the database entity** (`src/metabase/lib/metadata.cljc`, `editable-stages?`):

```clojure
(and (when-let [{:keys [id]} (database query)] ; ← nil when the DB is not in metadata
(= (:database query) id))
(or (and source-table (table query source-table))
(and source-card (card query source-card)) ; ← never reached
...))
```

**The gap — database visibility ignores collection-based access** (`src/metabase/warehouses_rest/api.clj`, `dbs-list`):

```clojure
[:or (visible-filter-clause :model/Database :id user-info {:perms/create-queries :query-builder})
(visible-filter-clause :model/Database :id user-info {:perms/manage-database :yes})
(visible-filter-clause :model/Database :id user-info {:perms/manage-table-metadata :yes})]
```

There is no branch for "the user can query a model on this database", so the database disappears from the list.

Notably, the analogous problem **was** solved for the EE published-tables feature in `src/metabase/permissions/user.clj` (`query-creation-capabilities`):

```clojure
:can-create-queries (boolean
(or (some #(at-least-as-permissive? :perms/create-queries % :query-builder) create-query-perms)
(published-tables/user-has-any-published-table-permission?))) ; ← explicit branch
```

Collection-based access is explicitly folded into the capability computation there. Model-based access has no equivalent, in either `query-creation-capabilities` or `dbs-list`.

### Information about your Metabase installation

Browser info is omitted below because the repro was driven at the API level (curl); the bug is browser-independent.

```json
{
"metabase-info": {
"databases": ["h2", "duckdb"],
"run-mode": "prod",
"plan-alias": "",
"version": {
"date": "2026-07-29",
"tag": "v0.62.7",
"hash": "9a18e3c"
},
"settings": {
"report-timezone": null
},
"hosting-env": "unknown",
"application-database": "postgres"
}
}
```

OSS build, self-hosted via Docker, no premium token (`token-features` all disabled). The DuckDB community driver is in use, but nothing in this report is driver-specific — the repro above uses the Sample Database.

### Severity

Blocking for a common governance setup. Models are positioned as a curated semantic layer, but a group restricted to models cannot build anything from them. The only workaround is to grant `Query builder` on a throwaway table, which weakens the very boundary the configuration is meant to enforce.

### Additional context

**Workaround.** Set `create-queries` to **Granular** for the group and grant **Query builder** on a single empty anchor table, leaving every other table at **No**. The database then appears in `GET /api/database`, the notebook becomes editable, and model-sourced questions work as the backend already intended. All other tables remain correctly 403. Needing a sacrificial table for this is the clearest symptom that the intended path is missing.

**Possible resolutions.** Either direction removes the contradiction:

1. Treat model access as conferring database visibility — add a branch to `dbs-list` (and `query-creation-capabilities`) for users with collection read access to a model on that database, mirroring what published tables already do. This makes the existing backend behaviour reachable, and would also resolve point 4 of #74873 on the free tier.
2. Or make the backend consistent with the docs and reject ad-hoc model-sourced queries under `Create queries: No`.

Option 1 seems more aligned with how models are positioned. I have a patch for option 1 ready and will open a PR referencing this issue.

**Secondary issue — the failure is undiagnosable.** Regardless of which behaviour is correct, the current failure is a permission error rendered with **zero server requests** and no indication of which permission is missing. Nothing in the admin UI or the server logs points at `GET /api/database`. Identifying the cause required reading the backend source and diffing API responses; an admin working from the UI alone has no path to it. A message naming the missing permission, or at minimum a logged reason, would help.

Contributor guide

Open the contributing guide

Research direction

Start with dbs-list in src/metabase/warehouses_rest/api.clj and compare its database visibility checks with query-creation-capabilities in src/metabase/permissions/user.clj. Read editable-stages? in src/metabase/lib/metadata.cljc, then run the documented Sample Database API reproduction. Done means the selected behavior is consistent: model-sourced queries are either reachable in the notebook and remain authorized, or are rejected by the backend.

Written by the indexing model from the issue text.

Assessment

Tech stack
clojure
Domain
backend, database, frontend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.