Skip to content

Commit 3822df6

Browse files
committed
chore: release 0.14.0 - and show a table-restricted key only its tables
allowed_tables now covers the schema too: GET /connections/<name>/schema and MCP's list_tables list only the key's tables (a foreign key into another shows as none), and table_ddl answers a forbidden table with the same 404 as a missing one. Docs and guides 16, 25 and 40 updated. Release: version 0.14.0; CHANGELOG's Unreleased becomes 0.14.0 with "Upgrading from 0.13"; SQLite and PostgreSQL upgrade fixtures for 0.14.0; BACKLOG sequence updated. The legacy-JSON-import deprecation test is SQLite-only, as the import is.
1 parent 63111e2 commit 3822df6

19 files changed

Lines changed: 137 additions & 29 deletions

‎BACKLOG.md‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,10 @@ JWKS support.
4545

4646
**Suggested sequence:**
4747
1. **0.13:** #61, #62, #69, #65, plus the #59 storage decision. (Released as 0.13.0 on 2026-10-04.)
48-
2. **1.0.0-rc1:** freeze; invite external users to upgrade real stores and report back.
49-
3. **1.0.0.**
48+
2. **0.14:** #64, #66, #67, #68, #71 and the how-to guides. (Released as 0.14.0 on 2026-10-04.)
49+
3. **#70**, the deployment-shape decision - the last item before the freeze.
50+
4. **1.0.0-rc1:** freeze; invite external users to upgrade real stores and report back.
51+
5. **1.0.0.**
5052

5153
## 1. Per-key API permissions
5254

@@ -2039,7 +2041,7 @@ breaks its own promise.
20392041

20402042
## 65. Upgrade guarantee: every 0.x store upgrades or refuses clearly, tested in CI
20412043

2042-
**Status: shipped.** PostgreSQL stores too: `<version>.postgres.tar.gz` (0.12.0, 0.13.0) are `pg_dump`
2044+
**Status: shipped.** PostgreSQL stores too: `<version>.postgres.tar.gz` (0.12.0, 0.13.0, 0.14.0) are `pg_dump`
20432045
output of the same estate, restored and started in the PostgreSQL CI job (`generate.py --postgres URL`). `tests/fixtures/stores/`
20442046
holds a home built by each of 0.7.1, 0.8.0, 0.9.0, 0.10.0, 0.11.0 and 0.12.0 through its own API
20452047
(`generate.py`); `tests/test_upgrades.py` starts the current code on each and checks connections, query versions

‎CHANGELOG.md‎

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,24 @@ sooner, saying why. The list of what is deprecated now is `queryapigate/deprecat
4242

4343
## [Unreleased]
4444

45+
## [0.14.0] - 2026-10-04
46+
47+
The groundwork for 1.0's promise: a deprecation policy and an "experimental" label for what's outside it, a database
48+
support matrix with tiers, backups you can restore, Python 3.11 or newer - and all 40 how-to guides, whose writing
49+
found and fixed the access-control and packaging gaps listed under *Fixed*.
50+
51+
### Upgrading from 0.13
52+
Read these first.
53+
- **Breaking: Python 3.11 or newer.** On 3.9 or 3.10, pip keeps installing 0.13.0. The Docker image is unaffected.
54+
- **No store migration**: the store stays at schema 5. Back up first anyway ([how](https://github.com/AnanthaRajuC/QueryAPIGate/blob/main/how-to/33-back-up-and-restore.md)).
55+
- **Coming from 0.9 or older?** Start 0.14 once before going further: 0.15.0 may stop importing the pre-SQLite JSON
56+
files (*Deprecated*, below).
57+
- **Callers of `/execute_sql_from_file` or `/execute_sql_with_parameters_from_file`** should move to `/q/{name}`
58+
before 0.15.0. Each process logs the first such call with the key that made it.
59+
- **A key with `"allowed_ips": []` now works from no address** (it used to work from any). Find any with
60+
`GET /api/v1/api-keys` and set them to `null`, or to real addresses.
61+
- **A table-restricted key's schema shows only its tables** - a client that listed the rest no longer sees them.
62+
4563
### Breaking
4664
- **Python 3.11 or newer is required** (was 3.9). Python 3.9 reached its end of life in October 2025 and 3.10
4765
reaches its own this month, so neither is carried into 1.0. pip on 3.9 or 3.10 keeps installing 0.13.0, the last
@@ -84,11 +102,16 @@ sooner, saying why. The list of what is deprecated now is `queryapigate/deprecat
84102
/events`, `queryapigate events`), H2, JDBC and MongoDB connections, and alerts (`GET /api/v1/alerts`). Each is
85103
marked in its docs, with `x-experimental: true` in `/openapi.json`, with a tag in the Console (its Settings rows,
86104
the database-type picker, the Alerts screen), and with a warning in the log at startup while one is in use.
105+
87106
### Changed
88107
- **The one-command demo (`docker compose up`) seeds itself through the Management API** (`demo/seed.sh`) instead of
89108
copying the pre-SQLite JSON files that 0.14 deprecates.
90109

91110
### Fixed
111+
- **A table-restricted key sees only its tables in the schema.** `allowed_tables` refused statements on other
112+
tables, but the schema browser and MCP's `list_tables` still listed every table and column on the connection, and
113+
`table_ddl` showed any table's `CREATE TABLE`. Now the others are left out (and a foreign key pointing at one shows
114+
as none), and `table_ddl` answers a forbidden table with the same `404` as a missing one.
92115
- **An empty `allowed_ips` list allows no address.** `"allowed_ips": []` on a key or role used to mean "no
93116
restriction" - failing open, unlike an empty `allowed_tables` or `allowed_write_ops`, which allow nothing. `null`
94117
is still how to say "any address"; the Console sends `null` for an empty field, as before.
@@ -1533,7 +1556,8 @@ First public release, restructured from the original single-file application.
15331556
- JSON column order is preserved; Decimal, date and driver-specific number types serialise correctly.
15341557
- Concurrent saves can no longer lose a version.
15351558

1536-
[Unreleased]: https://github.com/AnanthaRajuC/QueryAPIGate/compare/v0.13.0...HEAD
1559+
[Unreleased]: https://github.com/AnanthaRajuC/QueryAPIGate/compare/v0.14.0...HEAD
1560+
[0.14.0]: https://github.com/AnanthaRajuC/QueryAPIGate/releases/tag/v0.14.0
15371561
[0.13.0]: https://github.com/AnanthaRajuC/QueryAPIGate/releases/tag/v0.13.0
15381562
[0.12.0]: https://github.com/AnanthaRajuC/QueryAPIGate/releases/tag/v0.12.0
15391563
[0.11.0]: https://github.com/AnanthaRajuC/QueryAPIGate/releases/tag/v0.11.0

‎documentation/API.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -790,6 +790,9 @@ connections, the dialects QueryAPIGate can actually parse for this - **a table-r
790790
type, never silently left unrestricted.** Table names are matched case-insensitively and bare (not
791791
schema-qualified), the same simplification `allowed_write_ops`'s keyword list already makes; a table-valued
792792
function (e.g. ClickHouse's `numbers(10)`) touches no real table, so this can't meaningfully restrict one.
793+
The key's view of the schema follows the same list: `GET /connections/<name>/schema` and MCP's `list_tables` leave
794+
other tables out (a foreign key pointing at one shows as `null`), and `table_ddl` answers a forbidden table with the
795+
same `404` as a table that doesn't exist.
793796

794797
Omitted (or `null`) means no restriction, exactly today's behaviour. `PATCH /api/v1/api-keys/<name>` with an
795798
explicit `{"allowed_tables": null}` clears an existing restriction, the same pattern the other grant fields

‎documentation/THREAT_MODEL.md‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -221,10 +221,6 @@ Being explicit about these is as important as everything above:
221221
- **Connection metadata (hosts, ports, usernames, database names) is visible to the admin key** -
222222
`GET /api/v1/connections` is admin-only, and passwords are always masked, but the rest is not treated as
223223
secret.
224-
- **`allowed_tables` limits what a statement touches, not what a key can see of the schema.** A key with a
225-
`connections` grant can list every table and column on that connection (the schema browser, `table_ddl`,
226-
MCP's `list_tables`) - names and structure, never rows of a forbidden table. Hide a table's existence with
227-
database permissions.
228224
- **Names can be probed.** A saved query a key can't reach answers `403` naming its connection; a name that
229225
doesn't exist answers `404`. Don't put anything sensitive in query or connection names.
230226
- **`/metrics` is intentionally unauthenticated**, on the assumption that a metrics scraper shouldn't need

‎how-to/16-restrict-a-key-to-tables.md‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ Saved queries are checked the same way, every time they run. `orders_by_status`
5454
Writes are checked too: if the key also has `allow_writes`, `UPDATE`/`DELETE`/`INSERT` are allowed only on listed
5555
tables.
5656

57-
## Where it isn't enforced
57+
## What else it covers, and where it stops
5858

5959
- **Database types:** supported for PostgreSQL, MySQL, SQLite, DuckDB and ClickHouse - the dialects the parser
6060
handles. On an `h2`, `jdbc` or `mongo` connection a table-restricted key is **refused on every query** (`403`
@@ -63,10 +63,9 @@ tables.
6363
- **Table names, not schemas:** entries are bare names matched case-insensitively. `orders` allows `orders` in any
6464
schema the connection can see. If two schemas both have an `orders` table, this can't tell them apart - use a
6565
database user that sees only one.
66-
- **Structure stays visible:** `allowed_tables` limits what a statement can read or change, not what the key can
67-
*see* of the schema. The schema browser (`GET /connections/shop/schema`), `table_ddl` and MCP's `list_tables` list
68-
every table and column on the connection, forbidden ones included - their names and columns, never their rows. If
69-
a table's existence is itself sensitive, put it out of the connection's reach with database permissions.
66+
- **The schema shows only the listed tables.** The schema browser (`GET /connections/shop/schema`) and MCP's
67+
`list_tables` leave the others out, and a foreign key pointing at one shows as none. `table_ddl` for a forbidden
68+
table answers `404` - *'salaries' is not a table on 'shop'* - exactly as for a table that doesn't exist.
7069
- **Table-valued functions** (ClickHouse's `numbers(10)`, DuckDB's `read_csv(...)`) touch no named table, so the
7170
list can't restrict them.
7271

‎how-to/25-mcp-ad-hoc-tools.md‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,9 @@ An agent can read the `code` and adjust - ask for different tables, drop the wri
8383

8484
## Keep in mind
8585

86-
- **`list_tables` shows every table on the connection**, including ones `allowed_tables` forbids - names and columns,
87-
never rows. The same is true of the saved-query tools listed from a `connections` grant: `salary_list` appears
88-
above but fails with `table_not_allowed` when called. If the agent shouldn't know a table exists, keep it out of
89-
the connection's reach with database permissions.
86+
- **`list_tables` shows only the tables `allowed_tables` permits** - for this key `customers`, `orders` and
87+
`products`, not `salaries`. Saved-query tools are listed by the key's grants alone, though: `salary_list` appears
88+
above, and fails with `table_not_allowed` when called. Keep a query the agent mustn't see out of its collections.
9089
- **Results are capped** at `QUERYAPIGATE_MCP_MAX_ROWS` rows (default 200) - see
9190
[Read structured MCP results properly](26-mcp-structured-results.md) for `truncated` and `page_size`.
9291
- **Everything is recorded.** Each `execute_sql` call is an ad-hoc run in run history with `"transport": "mcp"`, the

‎how-to/40-read-the-threat-model.md‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,6 @@ with `from_claim` parameters so each user only ever sees their own rows. List th
7777
These are by design or not yet addressed - plan around them rather than discover them:
7878

7979
- **The admin key has no limits** inside QueryAPIGate. Database grants are the backstop.
80-
- **`allowed_tables` hides rows, not structure.** A key with a `connections` grant can list every table and column on
81-
that connection, forbidden ones included ([guide 16](16-restrict-a-key-to-tables.md)).
8280
- **Names can be probed.** A saved query a key can't reach answers `403` naming its connection; a missing name answers
8381
`404`.
8482
- **The MCP server ignores `QUERYAPIGATE_TRUST_PROXY`**, so behind a proxy it can't tell MCP callers' addresses apart.

‎queryapigate/__init__.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""QueryAPIGate - expose SQL databases as a REST API."""
2-
__version__ = '0.13.0'
2+
__version__ = '0.14.0'
33

44
from .app import create_app # noqa: E402 (app imports __version__)
55

‎queryapigate/app.py‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -855,7 +855,7 @@ def connection_schema(name):
855855
database = request.args.get('database') or None
856856
if database and not g.permission.admin:
857857
raise ApiError('Only the admin key may browse a different database on this connection', 403, code='admin_only')
858-
return jsonify(schema.fetch_schema(name, database=database)), 200
858+
return jsonify(schema.fetch_schema(name, database=database, allowed_tables=g.permission.allowed_tables)), 200
859859

860860

861861
@bp.route('/connections/<name>/table_ddl', methods=['GET'])
@@ -869,7 +869,8 @@ def connection_table_ddl(name):
869869
database = request.args.get('database') or None
870870
if database and not g.permission.admin:
871871
raise ApiError('Only the admin key may browse a different database on this connection', 403, code='admin_only')
872-
return jsonify(schema.fetch_table_ddl(name, table, database=database)), 200
872+
return jsonify(schema.fetch_table_ddl(name, table, database=database,
873+
allowed_tables=g.permission.allowed_tables)), 200
873874

874875

875876
# --------------------------------------------------------------------------------------

‎queryapigate/mcp_server.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ def list_tables_tool(permission, arguments):
235235
if error is not None:
236236
return error
237237
try:
238-
result = schema.fetch_schema(connection_name, database=database)
238+
result = schema.fetch_schema(connection_name, database=database, allowed_tables=permission.allowed_tables)
239239
except ApiError as error:
240240
return _error(error.message, error.status, error.code)
241241
return {'content': [{'type': 'text', 'text': json.dumps(result)}], 'structuredContent': result}

0 commit comments

Comments
 (0)