Model-sourced queries are authorized by the backend but unreachable in the UI when Create queries is "No"
- 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
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