From e156a710e38a4f9d08f96b318358d0d622f1319b Mon Sep 17 00:00:00 2001 From: James Maes Date: Mon, 12 Oct 2026 16:12:25 -0500 Subject: [PATCH] fix(permissions): grant result_snapshot.insert to users (pinning under core enforcement) Pinning a result inserts a sql_workbench_result_snapshot row as a top-level action. With QQQ core permission enforcement on, that insert is checked against the session's permissions, and getUserPermissionNames() granted only read/edit/delete on snapshots, so every user's pin was denied and hosts had to grant the insert by hand. Record locks still limit snapshots to the user's own notebooks. New test: with enforcement on, the user permission set covers create, update, run, pin, unpin and delete. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 3 ++ README.md | 2 +- .../sqlworkbench/SqlWorkbenchQBitConfig.java | 6 ++-- .../notebook/NotebookServiceTest.java | 30 +++++++++++++++++++ 4 files changed, 37 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7910089..4d2e9d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,9 @@ All notable changes to this project are documented here. The format follows Keep - **Names follow the owner rule: lowercase snake_case, prefixed with the qbit's name (metadata only, no DDL).** Tables `sql_workbench_notebook`, `sql_workbench_cell`, `sql_workbench_notebook_version`, `sql_workbench_result_snapshot`, `sql_workbench_notebook_share`, `sql_workbench_execution` (they now equal their backend table names); possible-value source `sql_workbench_share_audience_type`; permissions `sql_workbench.*`, `sql_workbench_admin.hasAccess` and `.read` etc.; security key types `sql_workbench_user` (all-access `sql_workbench_all_access`) and `sql_workbench_audience`; joins `sql_workbench_notebook_join_sql_workbench_*`. - `tableNamePrefix` (and the QBit namespace) is normalized to lowercase snake_case and joined with one `_`. +### Fixed +- **Pinning a result works with QQQ core permission enforcement on.** `getUserPermissionNames()` now includes `sql_workbench_result_snapshot.insert`: pinning is a top-level insert, which core enforcement checks, so hosts no longer need to grant it by hand. Record locks still limit snapshots to the user's own notebooks. + ### Deprecated - `LEGACY_TABLE_NAME` on each entity, `ShareAudienceType.LEGACY_NAME` and `SqlWorkbenchQBitConfig.LEGACY_*` hold the pre-RC.3 names, for migrating stored references only. diff --git a/README.md b/README.md index 0f6fd77..46d2af8 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ Everything is denied by default. Two standard QQQ layers apply: | Role | Grant | What it allows | |---|---|---| -| User | `config.getUserPermissionNames()`: `sql_workbench.hasAccess`, plus read/insert/edit/delete on notebooks and cells, read on versions, shares and the audit, and read/edit/delete on pinned results | The app, widget and endpoints (read-only SQL, EXPLAIN, schema). Their own notebooks, read-only access to notebooks shared with them or in the library, and their own execution history. | +| User | `config.getUserPermissionNames()`: `sql_workbench.hasAccess`, plus read/insert/edit/delete on notebooks and cells, read on versions, shares and the audit, and read/insert/edit/delete on pinned results | The app, widget and endpoints (read-only SQL, EXPLAIN, schema). Their own notebooks, read-only access to notebooks shared with them or in the library, and their own execution history. | | Writer | `config.getWritePermissionName()` (`sql_workbench.write`) | Write mode (preview and commit) and EXPLAIN ANALYZE. | | Sharer | `config.getSharePermissionNames()`: `sql_workbench.share`, plus insert/edit/delete on shares | Sharing their own notebooks with users, groups, or everyone (publishing to the library). | | Admin | `config.getAdminPermissionNames()`: the user permissions plus `sql_workbench_admin.hasAccess` | The all-access key: every user's notebooks, cells, versions, pinned results (real query data), shares, and the whole execution audit, and editing any notebook. | diff --git a/src/main/java/com/kingsrook/qbits/sqlworkbench/SqlWorkbenchQBitConfig.java b/src/main/java/com/kingsrook/qbits/sqlworkbench/SqlWorkbenchQBitConfig.java index 2fc7a24..0a080ac 100644 --- a/src/main/java/com/kingsrook/qbits/sqlworkbench/SqlWorkbenchQBitConfig.java +++ b/src/main/java/com/kingsrook/qbits/sqlworkbench/SqlWorkbenchQBitConfig.java @@ -377,8 +377,8 @@ public class SqlWorkbenchQBitConfig implements QBitConfig /******************************************************************************* ** Every permission an ordinary workbench user needs: the access ** permission, read / insert / edit / delete on notebooks and cells, read - ** on versions, shares and the execution audit, and read / edit (label) / - ** delete on pinned snapshots. Record locks narrow every one of these to + ** on versions, shares and the execution audit, and read / insert (pin) / + ** edit (label) / delete on pinned snapshots. Record locks narrow every one of these to ** the user's own (or shared-with-them, for reads) rows. *******************************************************************************/ @JsonIgnore @@ -389,7 +389,7 @@ public class SqlWorkbenchQBitConfig implements QBitConfig addTablePermissions(names, SqlWorkbenchNotebook.TABLE_NAME, "read", "insert", "edit", "delete"); addTablePermissions(names, SqlWorkbenchCell.TABLE_NAME, "read", "insert", "edit", "delete"); addTablePermissions(names, SqlWorkbenchNotebookVersion.TABLE_NAME, "read"); - addTablePermissions(names, SqlWorkbenchResultSnapshot.TABLE_NAME, "read", "edit", "delete"); + addTablePermissions(names, SqlWorkbenchResultSnapshot.TABLE_NAME, "read", "insert", "edit", "delete"); addTablePermissions(names, SqlWorkbenchNotebookShare.TABLE_NAME, "read"); addTablePermissions(names, SqlWorkbenchExecution.TABLE_NAME, "read"); return (names); diff --git a/src/test/java/com/kingsrook/qbits/sqlworkbench/notebook/NotebookServiceTest.java b/src/test/java/com/kingsrook/qbits/sqlworkbench/notebook/NotebookServiceTest.java index ca3230e..6fdbd97 100644 --- a/src/test/java/com/kingsrook/qbits/sqlworkbench/notebook/NotebookServiceTest.java +++ b/src/test/java/com/kingsrook/qbits/sqlworkbench/notebook/NotebookServiceTest.java @@ -219,6 +219,36 @@ class NotebookServiceTest extends BaseSqlWorkbenchTest + /******************************************************************************* + ** With QQQ core permission enforcement on, an ordinary user's permission set + ** (getUserPermissionNames) is enough for the whole notebook flow, including + ** pinning a result (a top-level snapshot insert). + *******************************************************************************/ + @Test + void testUserPermissionsSufficeWithCoreEnforcement() throws Exception + { + qInstance.setEnforcePermissionsInCoreActions(true); + try + { + Integer id = (Integer) service.create(body("Enforced", List.of(cell("c1", "SQL", "SELECT * FROM person"))), as(READER)).get("id"); + service.update(id, Map.of("title", "Enforced v2"), as(READER)); + + SqlExecutionEngine engine = new SqlExecutionEngine(qInstance, SqlWorkbenchRuntime.get(qInstance)); + Map response = engine.execute(ExecutionRequest.readOnly(BACKEND_NAME, "SELECT * FROM person", Map.of()), as(READER)); + + Map snapshot = service.createSnapshot(id, Map.of("executionUuid", response.get("executionUuid"), "cellKey", "c1"), as(READER), "SELECT * FROM person"); + assertThat(snapshot.get("id")).isNotNull(); + service.deleteSnapshot(id, (Integer) snapshot.get("id"), as(READER)); + service.delete(id, as(READER)); + } + finally + { + qInstance.setEnforcePermissionsInCoreActions(null); + } + } + + + /******************************************************************************* ** *******************************************************************************/ -- 2.54.0