diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c72411..55ec955 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ All notable changes to this project are documented here. The format follows Keep a Changelog; versions follow semantic versioning. +## [1.0.0-RC.3] - Unreleased + +### Changed +- **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 `_`. + +### Deprecated +- `LEGACY_TABLE_NAME` on each entity, `ShareAudienceType.LEGACY_NAME` and `SqlWorkbenchQBitConfig.LEGACY_*` hold the pre-RC.3 names, for migrating stored references only. + +### Upgrade from RC.2 +- Follow [UPGRADE.md](UPGRADE.md): grant the renamed permissions and rename stored table names (audit, saved views and reports). No DDL. + ## [1.0.0-RC.2] - Unreleased The workbench's data becomes standard QQQ tables, secured by QQQ's own mechanisms instead of being hidden ([#3](https://github.com/QRun-IO/qbit-sql-workbench/issues/3)). Requires QQQ `4.1.0-RC.4` or later ([qqq#1073](https://github.com/QRun-IO/qqq/issues/1073)). diff --git a/README.md b/README.md index 20a7914..fc083dd 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ You write SQL and markdown in notebook cells, run them against any RDBMS backend - **Read-only by default.** A default session runs one statement inside a database `READ ONLY` transaction, and a statement classifier refuses anything that isn't a read. - **Gated write mode.** Writes need a separate permission and two steps. First a dry run executes in a transaction that is rolled back, so you see the affected-row counts. You then confirm with a single-use token bound to that exact SQL. A row guard, a hard cap, a drift check, and statement timeouts apply. -- **Everything is audited.** The `sqlWorkbenchExecution` table records who ran what, when, on which backend, in which mode, how long it took, how many rows it returned or affected, and any error. SQL text and results never go to the logs. +- **Everything is audited.** The `sql_workbench_execution` table records who ran what, when, on which backend, in which mode, how long it took, how many rows it returned or affected, and any error. SQL text and results never go to the logs. - **Notebooks.** Notebooks are personal by default and can be shared, read-only, with users, groups, or everyone (the shared library). They have named `:parameters` (always bound, never spliced), version history, and pinned result snapshots. - **Standard QQQ tables.** Notebooks, cells, versions, pinned results, shares, and the execution audit are ordinary QQQ tables in the app, with the usual query, record, edit, export, audit, and developer-mode screens. Record security locks decide who sees and changes which rows, for every entry point. @@ -43,8 +43,8 @@ new SqlWorkbenchQBitProducer().withQBitConfig(config).produce(qInstance); The producer adds the following to the instance: -- **Tables:** `sqlWorkbenchNotebook`, `sqlWorkbenchCell`, `sqlWorkbenchNotebookVersion`, `sqlWorkbenchResultSnapshot`, `sqlWorkbenchNotebookShare`, `sqlWorkbenchExecution`, each with record security locks, per-table permissions, and table customizers ([Permissions](#permissions)). -- **Security key types** `sqlWorkbenchUser` (all-access key `sqlWorkbenchAllAccess`) and `sqlWorkbenchAudience`. +- **Tables:** `sql_workbench_notebook`, `sql_workbench_cell`, `sql_workbench_notebook_version`, `sql_workbench_result_snapshot`, `sql_workbench_notebook_share`, `sql_workbench_execution`, each with record security locks, per-table permissions, and table customizers ([Permissions](#permissions)). +- **Security key types** `sql_workbench_user` (all-access key `sql_workbench_all_access`) and `sql_workbench_audience`. - **Joins** from a notebook to its cells, versions, pinned results, and shares, with child-list widgets on the notebook's record view. - **Possible value sources** over the notebook table and the share audience types. - **Record actions** (processes on a table, never in the nav): Share Notebook, Duplicate Notebook, Restore Version. @@ -85,10 +85,10 @@ Everything is denied by default. Two standard QQQ layers apply: | Role | Grant | What it allows | |---|---|---| -| User | `config.getUserPermissionNames()`: `sqlWorkbench.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. | -| Writer | `config.getWritePermissionName()` (`sqlWorkbench.write`) | Write mode (preview and commit) and EXPLAIN ANALYZE. | -| Sharer | `config.getSharePermissionNames()`: `sqlWorkbench.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 `sqlWorkbenchAdmin.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. | +| 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. | +| 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. | The names are configurable (`accessPermissionBaseName`, `writePermissionName`, `sharePermissionName`, `adminPermissionBaseName`). Table permissions follow the table names, including any `tableNamePrefix`. QQQ 4.1 does not list `write` and `share` as available permissions, so grant them where you grant roles, for example in an authentication customizer. @@ -96,9 +96,9 @@ The names are configurable (`accessPermissionBaseName`, `writePermissionName`, ` | Key | Values | Used by | |---|---|---| -| `sqlWorkbenchUser` | the user's id (`QUser.idReference`) | owner locks: a notebook's `ownerUserId` (its children reach it through the notebook) and an execution's `userId` | -| `sqlWorkbenchAudience` | `user:`, `group:` for each group passed, and `all` | share locks (read only): a share row's `audienceKey` | -| `sqlWorkbenchAllAccess` | `true` for holders of the admin permission | every row | +| `sql_workbench_user` | the user's id (`QUser.idReference`) | owner locks: a notebook's `ownerUserId` (its children reach it through the notebook) and an execution's `userId` | +| `sql_workbench_audience` | `user:`, `group:` for each group passed, and `all` | share locks (read only): a share row's `audienceKey` | +| `sql_workbench_all_access` | `true` for holders of the admin permission | every row | A session without the access permission gets no keys, so it finds no workbench rows at all, even through code paths that skip table permissions. If the host never calls `applyToSession`, the workbench endpoints add the user and `all` keys themselves, but group shares need the host to pass the groups. @@ -204,7 +204,7 @@ A checklist for adding the workbench to a QQQ application. Each row points to th | Navigation | The app lists the widget and the six tables. Its processes are record actions on the tables; keep them out of the nav. If your app adds explicit table menus or nav-coverage tests, include the six tables. | | Turning writes off | `withWriteModeEnabled(false)` disables write mode for everyone. | | Known limitations | Raw SQL bypasses QQQ security, audits, listeners and automations ([Safety model](#safety-model)). H2 and generic backends have no enforced read-only transaction. Only PostgreSQL DDL ships. Shares are read-only (no shared editing). | -| Upgrading | From RC.1: run the share-table DDL and backfill, grant the new permission lists (the tables no longer rely on `sqlWorkbenchAdmin.hasAccess` alone), call `applyToSession`, and use QQQ `4.1.0-RC.4` or later. | +| Upgrading | From RC.2: the QQQ names became snake_case (`sql_workbench_*`; no DDL). See [UPGRADE.md](UPGRADE.md) for permissions and stored table names. From RC.1: also run the share-table DDL and backfill, grant the new permission lists (the tables no longer rely on the admin `hasAccess` permission alone), call `applyToSession`, and use QQQ `4.1.0-RC.4` or later. | Nothing in the qbit is specific to one host application. diff --git a/UPGRADE.md b/UPGRADE.md new file mode 100644 index 0000000..fe202f8 --- /dev/null +++ b/UPGRADE.md @@ -0,0 +1,56 @@ +# Upgrading + +## To 1.0.0-RC.3: snake_case names (metadata only) + +The owner naming rule now applies to every QRun-IO qbit: table names are lowercase snake_case, prefixed with the qbit's name. In 1.0.0-RC.3 the SQL Workbench's QQQ names change to match. **The database does not change**: the physical tables were already `sql_workbench_*` (`docs/ddl/postgres.sql`), so no DDL runs. What changes is the names QQQ uses, and anything your app stored under the old names. + +| Kind | Before (RC.1, RC.2) | From RC.3 | +|---|---|---| +| Tables (and the notebook possible-value source) | `sqlWorkbenchNotebook`, `sqlWorkbenchCell`, `sqlWorkbenchNotebookVersion`, `sqlWorkbenchResultSnapshot`, `sqlWorkbenchNotebookShare`, `sqlWorkbenchExecution` | `sql_workbench_notebook`, `sql_workbench_cell`, `sql_workbench_notebook_version`, `sql_workbench_result_snapshot`, `sql_workbench_notebook_share`, `sql_workbench_execution` | +| Enum possible-value source | `sqlWorkbenchShareAudienceType` | `sql_workbench_share_audience_type` | +| Permissions | `sqlWorkbench.hasAccess`, `sqlWorkbench.write`, `sqlWorkbench.share`, `sqlWorkbenchAdmin.hasAccess`, `sqlWorkbenchNotebook.read` etc. | `sql_workbench.hasAccess`, `sql_workbench.write`, `sql_workbench.share`, `sql_workbench_admin.hasAccess`, `sql_workbench_notebook.read` etc. | +| Security key types / all-access key | `sqlWorkbenchUser` (`sqlWorkbenchAllAccess`), `sqlWorkbenchAudience` | `sql_workbench_user` (`sql_workbench_all_access`), `sql_workbench_audience` | +| Joins | `sqlWorkbenchNotebookJoinSqlWorkbenchCell` etc. | `sql_workbench_notebook_join_sql_workbench_cell` etc. | + +Unchanged: the `sqlWorkbench` app, the `sqlWorkbenchWidget` widget, the record-action process names, the REST routes, field names, and Java class names. + +A `tableNamePrefix` (or QBit namespace) is now normalized to lowercase snake_case and joined with one `_`: `ns` and `ns_` give `ns_sql_workbench_notebook`, and `myNs` gives `my_ns_sql_workbench_notebook`. Prefixed tables follow the same rename. + +The old names stay available as `@Deprecated` constants for migration scripts only: `SqlWorkbenchNotebook.LEGACY_TABLE_NAME` (and the same on each entity), `ShareAudienceType.LEGACY_NAME`, and `SqlWorkbenchQBitConfig.LEGACY_*`. They will be removed in 1.1. + +### Steps + +1. **Backend table names.** If your host mapped the old camelCase names to `sql_workbench_*` (for example with `inferBackendName`), nothing else is needed: the new names are already the backend names, and the mapping becomes a no-op. If your backend tables were really named after the camelCase QQQ names, rename them (`ALTER TABLE "sqlWorkbenchNotebook" RENAME TO sql_workbench_notebook;` and so on) or set the backend table names explicitly. +2. **Permissions.** Grant the new names. If you set the permission names through the config getters (`getUserPermissionNames()`, `getSharePermissionNames()`, `getAdminPermissionNames()`), this is automatic. If roles live in an identity provider or a permissions table, rename them there. If you pinned the old names in config (`withAccessPermissionBaseName("sqlWorkbench")`), you can keep them, but the table permissions still change. +3. **Security keys.** `SqlWorkbenchSecurity.applyToSession` sets the new key types. Custom code that put `sqlWorkbenchUser` or `sqlWorkbenchAudience` values in a session must use the new names (`config.getUserSecurityKeyTypeName()` and so on). +4. **Stored references.** Rename the table names your app stored. A Postgres example, with the usual QQQ table and column names (adjust to your app; run inside one transaction, after a backup): + +```sql +BEGIN; +CREATE TEMP TABLE swb_rename (old_name TEXT, new_name TEXT) ON COMMIT DROP; +INSERT INTO swb_rename VALUES + ('sqlWorkbenchNotebook', 'sql_workbench_notebook'), + ('sqlWorkbenchCell', 'sql_workbench_cell'), + ('sqlWorkbenchNotebookVersion', 'sql_workbench_notebook_version'), + ('sqlWorkbenchResultSnapshot', 'sql_workbench_result_snapshot'), + ('sqlWorkbenchNotebookShare', 'sql_workbench_notebook_share'), + ('sqlWorkbenchExecution', 'sql_workbench_execution'); + +-- audit: QQQ's audit_table holds the QQQ table name +UPDATE audit_table a SET name = r.new_name FROM swb_rename r WHERE a.name = r.old_name; + +-- saved views and saved reports +UPDATE saved_view v SET table_name = r.new_name FROM swb_rename r WHERE v.table_name = r.old_name; +UPDATE saved_report s SET table_name = r.new_name FROM swb_rename r WHERE s.table_name = r.old_name; + +-- any app table that stores QQQ table names (for example a search index or a shared-record table) +-- UPDATE quick_search_index q SET table_name = r.new_name FROM swb_rename r WHERE q.table_name = r.old_name; +COMMIT; +``` + + Saved views and reports can also name columns of joined tables (`sqlWorkbenchCell.title`) inside their JSON filter or column lists; search those columns for `sqlWorkbench` and replace the table part. Rebuild any search index whose document ids embed table names. +5. **Frontends.** Links to `/app/sqlWorkbench/sqlWorkbenchNotebook/...` become `/app/sqlWorkbench/sql_workbench_notebook/...`. + +## To 1.0.0-RC.2 + +See the "Upgrade from RC.1" section of [CHANGELOG.md](CHANGELOG.md). diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 85ecfc0..7e394d0 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -40,7 +40,7 @@ SQL Workbench is a super-user SQL notebook for any QQQ application. Users write ┌──────────────────────────────────┐ ┌────────────────────────────────────────────┐ │ /app/sqlWorkbench (AppHome) │ GET metaData │ QQQ middleware (Javalin 7) │ │ └ WidgetRenderer │◄───────────────►│ /qqq/v1/metaData (app + widget, filtered │ - │ case 'SQL_WORKBENCH' │ │ by sqlWorkbench.hasAccess)│ + │ case 'SQL_WORKBENCH' │ │ by sql_workbench.hasAccess)│ │ └ SqlWorkbenchWidget (lazy) │ │ │ │ ├ NotebookList │ /qqq/v1/ │ SqlWorkbenchRouteProvider │ │ ├ NotebookEditor │ sql-workbench/*│ └ SqlWorkbenchApi (session, perms, JSON) │ @@ -78,7 +78,7 @@ SQL Workbench is a super-user SQL notebook for any QQQ application. Users write ### 2.2 Frontend (Next UI) -The feature is advertised through **app presence**, the same as ESB. `/qqq/v1/metaData` publishes only an allow-list of supplemental metadata, so a flag would not reach the UI. The qbit's app `sqlWorkbench` carries one widget of type `SQL_WORKBENCH`. Both are gated by `sqlWorkbench.hasAccess`, so users without access never see either one. The generic `/app/[slug]` route renders `AppHome`, then `ConnectedWidget`, then `WidgetRenderer`, which gains: +The feature is advertised through **app presence**, the same as ESB. `/qqq/v1/metaData` publishes only an allow-list of supplemental metadata, so a flag would not reach the UI. The qbit's app `sqlWorkbench` carries one widget of type `SQL_WORKBENCH`. Both are gated by `sql_workbench.hasAccess`, so users without access never see either one. The generic `/app/[slug]` route renders `AppHome`, then `ConnectedWidget`, then `WidgetRenderer`, which gains: ```tsx case 'SQL_WORKBENCH': return // React.lazy @@ -109,16 +109,16 @@ Six tables on the host-chosen backend (`config.backendName`). They are standard ```mermaid erDiagram - sqlWorkbenchNotebook ||--o{ sqlWorkbenchCell : "cells (ordered)" - sqlWorkbenchNotebook ||--o{ sqlWorkbenchNotebookVersion : "versions (append-only)" - sqlWorkbenchNotebook ||--o{ sqlWorkbenchResultSnapshot : "pinned results" - sqlWorkbenchNotebook ||--o{ sqlWorkbenchNotebookShare : "shared with (read)" - sqlWorkbenchExecution }o..o| sqlWorkbenchNotebook : "ran from (no FK)" + sql_workbench_notebook ||--o{ sql_workbench_cell : "cells (ordered)" + sql_workbench_notebook ||--o{ sql_workbench_notebook_version : "versions (append-only)" + sql_workbench_notebook ||--o{ sql_workbench_result_snapshot : "pinned results" + sql_workbench_notebook ||--o{ sql_workbench_notebook_share : "shared with (read)" + sql_workbench_execution }o..o| sql_workbench_notebook : "ran from (no FK)" ``` Names can be prefixed (`tableNamePrefix`) or customized (`tableMetaDataCustomizer`, for example snake_case backend names). The Postgres DDL is in [`docs/ddl/postgres.sql`](ddl/postgres.sql). Liquibase users can drop it into a formatted-SQL changeset. -### `sqlWorkbenchNotebook` +### `sql_workbench_notebook` | field | type | notes | |---|---|---| | id | INTEGER PK | | @@ -132,7 +132,7 @@ erDiagram | currentVersion | INTEGER | incremented on every content change, from the notebook UI or the tables | | createDate, modifyDate | DATE_TIME | | -### `sqlWorkbenchCell` +### `sql_workbench_cell` | field | type | notes | |---|---|---| | id | INTEGER PK | | @@ -145,15 +145,15 @@ erDiagram | backendName | STRING(100) | null means the notebook default | | createDate, modifyDate | DATE_TIME | | -### `sqlWorkbenchNotebookVersion` +### `sql_workbench_notebook_version` Append-only history. Each save writes the full notebook plus cells as JSON: a notebook-UI save writes one version; a table edit of the notebook or one of its cells writes one version per change (`NotebookVersioning`, as the system user, after QQQ has authorized the change). `id, notebookId, versionNumber, contentJson (TEXT), savedByUserId, savedByName, comment, createDate`. Unique key: `(notebookId, versionNumber)`. Retention: `notebookVersionRetention` (default 100 per notebook, oldest pruned on save). -### `sqlWorkbenchResultSnapshot` +### `sql_workbench_result_snapshot` Pinned results. Created only by pinning a cached result in the notebook UI (so the data comes from a real run); through the table only `label` can change, and the owner can delete them. `id, notebookId, cellKey, executionUuid, label, backendName, sqlText (TEXT), resultJson (TEXT: columns plus up to snapshotMaxRows rows), rowCount, truncated, createdByUserId, createDate`. -### `sqlWorkbenchNotebookShare` +### `sql_workbench_notebook_share` Who, besides the owner, may read a notebook (read only; no shared editing). | field | type | notes | |---|---|---| @@ -167,7 +167,7 @@ Who, besides the owner, may read a notebook (read only; no shared editing). Publishing in the notebook UI inserts (or deletes) the `ALL` share; the share customizer keeps the notebook's `visibility` in step. -### `sqlWorkbenchExecution` (audit, append-only) +### `sql_workbench_execution` (audit, append-only) | field | type | notes | |---|---|---| | id | INTEGER PK | | @@ -197,7 +197,7 @@ The audit table has its insert, edit, and delete capabilities disabled in the QQ ## 4. Execution engine ### 4.1 Pipeline (every run) -1. **Authorize.** The session must have `sqlWorkbench.hasAccess` (403 otherwise). Write and EXPLAIN ANALYZE also need `sqlWorkbench.write` and `config.writeModeEnabled`. +1. **Authorize.** The session must have `sql_workbench.hasAccess` (403 otherwise). Write and EXPLAIN ANALYZE also need `sql_workbench.write` and `config.writeModeEnabled`. 2. **Resolve the target.** The backend must be an `RDBMSBackendMetaData` in the instance and allowed by `allowedBackendNames` / `excludedBackendNames` (404 otherwise). The dialect comes from the vendor. 3. **Validate.** Check the SQL length (`maxSqlLength`, default 100 KB), then split and classify with `SqlClassifier`. Rules: - `FORBIDDEN` statements are rejected in every mode. On MySQL this includes any statement with an executable comment (`/*! ... */`, `/*M! ... */`) or optimizer hint (`/*+ ... */`): MySQL runs their contents, so the scanner keeps them as tokens instead of skipping them, and fails closed. MySQL `--` comments need trailing whitespace (`1--1` is arithmetic). @@ -233,7 +233,7 @@ There is no session-level `SET` leakage: user SQL can never run `SET` / `RESET` > **Defense in depth.** The READ ONLY transaction is the primary control on Postgres, MySQL, and SQLite. On H2 the classifier is the only control, so production should use Postgres or MySQL with `readOnlyCredentials` (a role with `SELECT` grants only). A READ ONLY transaction does not stop side-effecting *functions* that are not writes, such as `pg_terminate_backend()` or `pg_sleep()`. Only a least-privilege role does. That is why `readOnlyCredentials` is recommended for production. ### 4.3 Write mode -Write mode needs `sqlWorkbench.write`. It is a two-step protocol: +Write mode needs `sql_workbench.write`. It is a two-step protocol: 1. **Preview**: `POST /execute {mode: "WRITE"}` with no `confirmToken`. - Classify the statements and reject `FORBIDDEN` ones. @@ -302,7 +302,7 @@ The response is `{executionUuid, dialect, analyze, plan: PlanNode, raw: string}` SQL errors are results, not HTTP errors: `/execute` and `/explain` return 200 with `status` FAILED, TIMED_OUT, or CANCELLED plus `error` and `sqlState`. -All endpoints need an authenticated QQQ session (cookie or Authorization header, via `ExecutorSessionUtils.setupSession`) and `sqlWorkbench.hasAccess`. Errors are `{"error": "..."}` with 400, 401, 403, 404, 409 (a re-used `executionUuid`), or 500, through `QJavalinUtils.handleException`. Execution UUIDs are case-insensitive on every path (`/execute`, `/executions/{uuid}/rows`, `/export`, `/cancel`, snapshots); the server stores and returns them lower-case. Bodies are JSON. Request and response bodies are never logged. +All endpoints need an authenticated QQQ session (cookie or Authorization header, via `ExecutorSessionUtils.setupSession`) and `sql_workbench.hasAccess`. Errors are `{"error": "..."}` with 400, 401, 403, 404, 409 (a re-used `executionUuid`), or 500, through `QJavalinUtils.handleException`. Execution UUIDs are case-insensitive on every path (`/execute`, `/executions/{uuid}/rows`, `/export`, `/cancel`, snapshots); the server stores and returns them lower-case. Bodies are JSON. Request and response bodies are never logged. ```yaml GET /capabilities @@ -334,7 +334,7 @@ POST /notebooks body: Notebook -> Notebook (version 1) GET /notebooks/{id} -> Notebook (with cells, canEdit) PUT /notebooks/{id} body: Notebook -> Notebook (cells replaced, new version) DELETE /notebooks/{id} -> { deleted: true } -POST /notebooks/{id}/publish body: { shared: bool } (needs sqlWorkbench.share) +POST /notebooks/{id}/publish body: { shared: bool } (needs sql_workbench.share) POST /notebooks/{id}/duplicate -> Notebook (personal copy) GET /notebooks/{id}/versions -> { versions: [{versionNumber, savedByName, comment, createDate}] } GET /notebooks/{id}/versions/{n} -> { versionNumber, notebook: Notebook } @@ -349,7 +349,7 @@ DELETE /notebooks/{id}/snapshots/{snapshotId} Visibility rules: - A user can read their own notebooks and all `SHARED` ones. - Only the owner can edit, delete, snapshot, or restore. -- Publishing or unpublishing needs `sqlWorkbench.share` **and** ownership. +- Publishing or unpublishing needs `sql_workbench.share` **and** ownership. - Anyone with access can duplicate a readable notebook. --- @@ -358,7 +358,7 @@ Visibility rules: | Concern | Control | |---|---| -| Who | Deny by default. Every endpoint needs `sqlWorkbench.hasAccess` (the app and widget permission base name `sqlWorkbench`, at `HAS_ACCESS_PERMISSION`). Write mode and EXPLAIN ANALYZE need `sqlWorkbench.write`. Sharing (including publishing to the library) needs `sqlWorkbench.share` plus insert/edit/delete on the share table. The tables use QQQ's per-table `READ_INSERT_EDIT_DELETE_PERMISSIONS`; `SqlWorkbenchQBitConfig.getUserPermissionNames / getSharePermissionNames / getAdminPermissionNames` list what to grant per role. `sqlWorkbenchAdmin.hasAccess` grants the all-access key. The names are configurable. | +| Who | Deny by default. Every endpoint needs `sql_workbench.hasAccess` (the app and widget permission base name `sqlWorkbench`, at `HAS_ACCESS_PERMISSION`). Write mode and EXPLAIN ANALYZE need `sql_workbench.write`. Sharing (including publishing to the library) needs `sql_workbench.share` plus insert/edit/delete on the share table. The tables use QQQ's per-table `READ_INSERT_EDIT_DELETE_PERMISSIONS`; `SqlWorkbenchQBitConfig.getUserPermissionNames / getSharePermissionNames / getAdminPermissionNames` list what to grant per role. `sql_workbench_admin.hasAccess` grants the all-access key. The names are configurable. | | What can run | Read-only transactions by default (§4.2); classifier as defense in depth; `FORBIDDEN` statements in every mode; one statement per read-only run. | | Writes | Separate permission, two-step preview then commit with a server-side single-use token bound to the user, SQL, and backend, a dry run with rollback, a row guard with acknowledgement, a hard cap, a drift check, and a statement timeout. | | Credentials | Never returned or logged. Backend metadata goes out only as `{name, label, vendor, dialect}`. `readOnlyCredentials` support `${env.X}` / `${prop.X}` interpolation (`QMetaDataVariableInterpreter`), and the config's `toString` redacts them. | @@ -366,7 +366,7 @@ Visibility rules: | Session and CSRF | QQQ middleware session handling. The cookie session is `HttpOnly`; there is no extra CSRF token, matching the rest of `/qqq/v1` (the JSON-only POSTs need `Content-Type: application/json`, which a cross-site form cannot send without a CORS preflight). The route provider rejects non-JSON POST bodies. | | Isolation between users | Result cache, cancellation, history, and write tokens are keyed by the user's `idReference`. Another user's execution UUID returns 404. | | QQQ semantics | **Raw SQL bypasses QQQ**: no record security locks, table/field permissions, hidden fields, audit of record changes, or customizers/automations. Rows changed in write mode do **not** trigger QQQ audits, record-change listeners, ESB events, or Quick Search indexing. This is documented in the UI (a banner in write mode) and in the README. Use QQQ processes for business changes. | -| Which rows | Record security locks, enforced by QQQ's core actions and therefore for every entry point (table UI, `/qqq/v1` table API, exports, PVS lookups, processes, the workbench endpoints). Two key types: `sqlWorkbenchUser` (the session's user id; all-access key `sqlWorkbenchAllAccess` for admins) and `sqlWorkbenchAudience` (`user:`, `group:` per group, `all`). A notebook's lock is OR(owner lock on `ownerUserId`, read-scoped share lock on `sqlWorkbenchNotebookShare.audienceKey` through the share-to-notebook join); its cells, versions and pinned results lock through their notebook (one and two joins); a share row is readable by the notebook's owner or the audience it names; an execution by its `userId`. Writes evaluate only the write-scoped locks, so being shared with is read-only. The RDBMS module de-duplicates rows that match through several shares (`SELECT DISTINCT`, `COUNT(DISTINCT)`). Hosts call `SqlWorkbenchSecurity.applyToSession` from their authentication customizer; sessions without the access permission get no keys and see no rows. | +| Which rows | Record security locks, enforced by QQQ's core actions and therefore for every entry point (table UI, `/qqq/v1` table API, exports, PVS lookups, processes, the workbench endpoints). Two key types: `sql_workbench_user` (the session's user id; all-access key `sql_workbench_all_access` for admins) and `sql_workbench_audience` (`user:`, `group:` per group, `all`). A notebook's lock is OR(owner lock on `ownerUserId`, read-scoped share lock on `sql_workbench_notebook_share.audienceKey` through the share-to-notebook join); its cells, versions and pinned results lock through their notebook (one and two joins); a share row is readable by the notebook's owner or the audience it names; an execution by its `userId`. Writes evaluate only the write-scoped locks, so being shared with is read-only. The RDBMS module de-duplicates rows that match through several shares (`SELECT DISTINCT`, `COUNT(DISTINCT)`). Hosts call `SqlWorkbenchSecurity.applyToSession` from their authentication customizer; sessions without the access permission get no keys and see no rows. | | Invariants | Table customizers, shared by the notebook UI and the tables: the owner is the inserting session (only the system user may insert for someone else); `visibility`, `currentVersion` and dates are system-managed; cells are validated (type, length, key) and appended in order, never move notebooks, and every change writes a version; shares compute `audienceKey`, need the share permission, and keep `visibility` in step; pinned results are only created by pinning and only relabeled; versions and the audit are append-only for everyone but the system user. `NotebookService` runs as the caller through the same actions (flagging a whole-notebook save so it is versioned once) and keeps its explicit owner / share checks as defense in depth; only version bookkeeping and cascades run as the system user, after QQQ authorized the change. | | QQQ dependency | Partial updates of child rows (a cell's content, a pinned result's label) pass the joined write locks only with QQQ `4.1.0-RC.4`+ (qqq#1073: locks are validated against the stored row overlaid with the written values). | @@ -386,9 +386,9 @@ Visibility rules: | `useBackendReadOnlyReplica` | true | use the backend's `readOnlyBackendMetaData` for read-only sessions | | `writeModeEnabled` | true | a global kill switch for write mode | | `accessPermissionBaseName` | `sqlWorkbench` | `.hasAccess` | -| `writePermissionName` | `sqlWorkbench.write` | | -| `sharePermissionName` | `sqlWorkbench.share` | | -| `adminPermissionBaseName` | `sqlWorkbenchAdmin` | `.hasAccess` grants the all-access security key (every user's rows); must differ from `accessPermissionBaseName` | +| `writePermissionName` | `sql_workbench.write` | | +| `sharePermissionName` | `sql_workbench.share` | | +| `adminPermissionBaseName` | `sql_workbench_admin` | `.hasAccess` grants the all-access security key (every user's rows); must differ from `accessPermissionBaseName` | | `statementTimeoutSeconds` | 30 | read-only and EXPLAIN | | `writeStatementTimeoutSeconds` | 60 | preview and commit | | `maxRows` | 10000 | rows kept per result | @@ -432,7 +432,7 @@ Visibility rules: │ │ + SQL cell + Markdown cell │ └────────────────────────┴───────────────────────────────────────────────────────────────────┘ -Write mode (user has sqlWorkbench.write; toggled per cell): +Write mode (user has sql_workbench.write; toggled per cell): ┌ Confirm write on "postgres" ───────────────────────────────────────────┐ │ ⚠ Raw SQL bypasses QQQ security, audits, automations and indexing. │ │ 1. UPDATE UPDATE web_user SET active = false WHERE … 1,204 rows │ @@ -500,7 +500,7 @@ Plan tab: | 5 (later) | Saved-query sharing beyond the library, scheduled notebooks, per-backend permissions (`sqlWorkbench.backend.`), result diffing, a core QQQ `availablePermissions` hook for `write` / `share` | backlog | ## 11. Open questions -1. **Permission discovery.** `sqlWorkbench.write` and `sqlWorkbench.share` are plain session permissions. QQQ 4.1 lists available permissions only from tables, processes, apps, reports, and widgets, so these two do not appear in role-management UIs (for example qbit-user-role-permissions). Options: a core hook for custom permissions, or hidden marker widgets. Deferred. -2. **Per-backend authorization.** Should write access be limited per backend (for example `sqlWorkbench.write.`)? 1.0 uses one global write permission (the owner will refine later). +1. **Permission discovery.** `sql_workbench.write` and `sql_workbench.share` are plain session permissions. QQQ 4.1 lists available permissions only from tables, processes, apps, reports, and widgets, so these two do not appear in role-management UIs (for example qbit-user-role-permissions). Options: a core hook for custom permissions, or hidden marker widgets. Deferred. +2. **Per-backend authorization.** Should write access be limited per backend (for example `sql_workbench.write.`)? 1.0 uses one global write permission (the owner will refine later). 3. **Audit retention and redaction.** The audit stores parameter values verbatim. Should hosts be able to redact, for example, `password`-like parameter names? 1.0 stores them as is. 4. **H2 in production.** H2 has no enforced read-only transaction. Should the workbench refuse H2 targets in production unless read-only credentials are configured? 1.0 allows it, with a warning in capabilities (`readOnlyEnforced: false`). diff --git a/src/main/java/com/kingsrook/qbits/sqlworkbench/SqlWorkbenchQBitConfig.java b/src/main/java/com/kingsrook/qbits/sqlworkbench/SqlWorkbenchQBitConfig.java index bf87649..2fc7a24 100644 --- a/src/main/java/com/kingsrook/qbits/sqlworkbench/SqlWorkbenchQBitConfig.java +++ b/src/main/java/com/kingsrook/qbits/sqlworkbench/SqlWorkbenchQBitConfig.java @@ -22,6 +22,12 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.kingsrook.qbits.sqlworkbench.model.SqlWorkbenchCell; +import com.kingsrook.qbits.sqlworkbench.model.SqlWorkbenchExecution; +import com.kingsrook.qbits.sqlworkbench.model.SqlWorkbenchNotebook; +import com.kingsrook.qbits.sqlworkbench.model.SqlWorkbenchNotebookShare; +import com.kingsrook.qbits.sqlworkbench.model.SqlWorkbenchNotebookVersion; +import com.kingsrook.qbits.sqlworkbench.model.SqlWorkbenchResultSnapshot; import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel; @@ -41,8 +47,8 @@ import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaDat ** excludedBackendNames over the instance's RDBMS backends, optional ** readOnlyCredentials / writeCredentials per backend, and ** useBackendReadOnlyReplica. Permissions: accessPermissionBaseName - ** (=> "sqlWorkbench.hasAccess"), writePermissionName, sharePermissionName, - ** and adminPermissionBaseName (=> "sqlWorkbenchAdmin.hasAccess", which + ** (=> "sql_workbench.hasAccess"), writePermissionName, sharePermissionName, + ** and adminPermissionBaseName (=> "sql_workbench_admin.hasAccess", which ** grants the all-access security key: every user's notebooks and the whole ** audit). The tables use QQQ's per-table read / insert / edit / delete ** permissions; getUserPermissionNames, getSharePermissionNames and @@ -55,10 +61,34 @@ import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaDat *******************************************************************************/ public class SqlWorkbenchQBitConfig implements QBitConfig { - public static final String DEFAULT_ACCESS_PERMISSION_BASE_NAME = "sqlWorkbench"; - public static final String DEFAULT_WRITE_PERMISSION_NAME = "sqlWorkbench.write"; - public static final String DEFAULT_SHARE_PERMISSION_NAME = "sqlWorkbench.share"; - public static final String DEFAULT_ADMIN_PERMISSION_BASE_NAME = "sqlWorkbenchAdmin"; + public static final String DEFAULT_ACCESS_PERMISSION_BASE_NAME = "sql_workbench"; + public static final String DEFAULT_WRITE_PERMISSION_NAME = "sql_workbench.write"; + public static final String DEFAULT_SHARE_PERMISSION_NAME = "sql_workbench.share"; + public static final String DEFAULT_ADMIN_PERMISSION_BASE_NAME = "sql_workbench_admin"; + + public static final String USER_SECURITY_KEY_TYPE_NAME = "sql_workbench_user"; + public static final String AUDIENCE_SECURITY_KEY_TYPE_NAME = "sql_workbench_audience"; + public static final String ALL_ACCESS_SECURITY_KEY_NAME = "sql_workbench_all_access"; + + /////////////////////////////////////////////////////////////////////////// + // names before 1.0.0-RC.3, when the owner rule (lowercase snake_case, // + // prefixed with the qbit name) was applied; kept only so hosts can // + // migrate stored references (see UPGRADE.md) // + /////////////////////////////////////////////////////////////////////////// + @Deprecated(since = "1.0.0-RC.3") + public static final String LEGACY_ACCESS_PERMISSION_BASE_NAME = "sqlWorkbench"; + @Deprecated(since = "1.0.0-RC.3") + public static final String LEGACY_WRITE_PERMISSION_NAME = "sqlWorkbench.write"; + @Deprecated(since = "1.0.0-RC.3") + public static final String LEGACY_SHARE_PERMISSION_NAME = "sqlWorkbench.share"; + @Deprecated(since = "1.0.0-RC.3") + public static final String LEGACY_ADMIN_PERMISSION_BASE_NAME = "sqlWorkbenchAdmin"; + @Deprecated(since = "1.0.0-RC.3") + public static final String LEGACY_USER_SECURITY_KEY_TYPE_NAME = "sqlWorkbenchUser"; + @Deprecated(since = "1.0.0-RC.3") + public static final String LEGACY_AUDIENCE_SECURITY_KEY_TYPE_NAME = "sqlWorkbenchAudience"; + @Deprecated(since = "1.0.0-RC.3") + public static final String LEGACY_ALL_ACCESS_SECURITY_KEY_NAME = "sqlWorkbenchAllAccess"; private String backendName; private String tableNamePrefix; @@ -222,11 +252,29 @@ public class SqlWorkbenchQBitConfig implements QBitConfig /******************************************************************************* - ** Apply the table name prefix, if any. + ** Apply the table name prefix, if any. The prefix is normalized to + ** lowercase snake_case and joined with one "_", so a prefix of "ns", + ** "ns_" or "myNs" gives "ns_sql_workbench_notebook" or + ** "my_ns_sql_workbench_notebook". *******************************************************************************/ public String applyPrefix(String name) { - return (StringUtils.hasContent(tableNamePrefix) ? tableNamePrefix + "_" + name : name); + if(!StringUtils.hasContent(tableNamePrefix)) + { + return (name); + } + return (normalizePrefix(tableNamePrefix) + "_" + name); + } + + + + /******************************************************************************* + ** Lowercase snake_case form of a prefix, without leading or trailing "_". + *******************************************************************************/ + static String normalizePrefix(String prefix) + { + String snake = prefix.trim().replaceAll("([a-z0-9])([A-Z])", "$1_$2").replaceAll("[^A-Za-z0-9]+", "_").toLowerCase(); + return (snake.replaceAll("^_+", "").replaceAll("_+$", "")); } @@ -298,7 +346,7 @@ public class SqlWorkbenchQBitConfig implements QBitConfig @JsonIgnore public String getUserSecurityKeyTypeName() { - return (applyPrefix("sqlWorkbenchUser")); + return (applyPrefix(USER_SECURITY_KEY_TYPE_NAME)); } @@ -310,7 +358,7 @@ public class SqlWorkbenchQBitConfig implements QBitConfig @JsonIgnore public String getAudienceSecurityKeyTypeName() { - return (applyPrefix("sqlWorkbenchAudience")); + return (applyPrefix(AUDIENCE_SECURITY_KEY_TYPE_NAME)); } @@ -321,7 +369,7 @@ public class SqlWorkbenchQBitConfig implements QBitConfig @JsonIgnore public String getAllAccessSecurityKeyName() { - return (applyPrefix("sqlWorkbenchAllAccess")); + return (applyPrefix(ALL_ACCESS_SECURITY_KEY_NAME)); } @@ -338,12 +386,12 @@ public class SqlWorkbenchQBitConfig implements QBitConfig { List names = new ArrayList<>(); names.add(getAccessPermissionName()); - addTablePermissions(names, "sqlWorkbenchNotebook", "read", "insert", "edit", "delete"); - addTablePermissions(names, "sqlWorkbenchCell", "read", "insert", "edit", "delete"); - addTablePermissions(names, "sqlWorkbenchNotebookVersion", "read"); - addTablePermissions(names, "sqlWorkbenchResultSnapshot", "read", "edit", "delete"); - addTablePermissions(names, "sqlWorkbenchNotebookShare", "read"); - addTablePermissions(names, "sqlWorkbenchExecution", "read"); + 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, SqlWorkbenchNotebookShare.TABLE_NAME, "read"); + addTablePermissions(names, SqlWorkbenchExecution.TABLE_NAME, "read"); return (names); } @@ -359,7 +407,7 @@ public class SqlWorkbenchQBitConfig implements QBitConfig { List names = new ArrayList<>(); names.add(sharePermissionName); - addTablePermissions(names, "sqlWorkbenchNotebookShare", "insert", "edit", "delete"); + addTablePermissions(names, SqlWorkbenchNotebookShare.TABLE_NAME, "insert", "edit", "delete"); return (names); } diff --git a/src/main/java/com/kingsrook/qbits/sqlworkbench/SqlWorkbenchSecurity.java b/src/main/java/com/kingsrook/qbits/sqlworkbench/SqlWorkbenchSecurity.java index 6ae3c72..15a4b1f 100644 --- a/src/main/java/com/kingsrook/qbits/sqlworkbench/SqlWorkbenchSecurity.java +++ b/src/main/java/com/kingsrook/qbits/sqlworkbench/SqlWorkbenchSecurity.java @@ -39,10 +39,10 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils; ** The workbench's record-security keys. ** ** Two security key types guard the qbit's tables: - ** - sqlWorkbenchUser: the session's user id. Owner locks (a notebook's + ** - sql_workbench_user: the session's user id. Owner locks (a notebook's ** ownerUserId, an execution's userId) match it. Its all-access key, - ** sqlWorkbenchAllAccess, is set for workbench admins. - ** - sqlWorkbenchAudience: the session's audience keys: "user:", + ** sql_workbench_all_access, is set for workbench admins. + ** - sql_workbench_audience: the session's audience keys: "user:", ** "group:" per group, and "all". Share locks (a share row's ** audienceKey) match it, for reads only. ** diff --git a/src/main/java/com/kingsrook/qbits/sqlworkbench/metadata/SqlWorkbenchCellTableMetaDataProducer.java b/src/main/java/com/kingsrook/qbits/sqlworkbench/metadata/SqlWorkbenchCellTableMetaDataProducer.java index 43e84c9..61f3229 100644 --- a/src/main/java/com/kingsrook/qbits/sqlworkbench/metadata/SqlWorkbenchCellTableMetaDataProducer.java +++ b/src/main/java/com/kingsrook/qbits/sqlworkbench/metadata/SqlWorkbenchCellTableMetaDataProducer.java @@ -25,7 +25,7 @@ import com.kingsrook.qbits.sqlworkbench.SqlWorkbenchQBitConfig; /******************************************************************************* - ** Produces the sqlWorkbenchCell table. + ** Produces the sql_workbench_cell table. *******************************************************************************/ public class SqlWorkbenchCellTableMetaDataProducer extends QBitComponentMetaDataProducer { diff --git a/src/main/java/com/kingsrook/qbits/sqlworkbench/metadata/SqlWorkbenchExecutionTableMetaDataProducer.java b/src/main/java/com/kingsrook/qbits/sqlworkbench/metadata/SqlWorkbenchExecutionTableMetaDataProducer.java index 3981582..7dea964 100644 --- a/src/main/java/com/kingsrook/qbits/sqlworkbench/metadata/SqlWorkbenchExecutionTableMetaDataProducer.java +++ b/src/main/java/com/kingsrook/qbits/sqlworkbench/metadata/SqlWorkbenchExecutionTableMetaDataProducer.java @@ -25,7 +25,7 @@ import com.kingsrook.qbits.sqlworkbench.SqlWorkbenchQBitConfig; /******************************************************************************* - ** Produces the sqlWorkbenchExecution (audit) table. + ** Produces the sql_workbench_execution (audit) table. *******************************************************************************/ public class SqlWorkbenchExecutionTableMetaDataProducer extends QBitComponentMetaDataProducer { diff --git a/src/main/java/com/kingsrook/qbits/sqlworkbench/metadata/SqlWorkbenchNotebookShareTableMetaDataProducer.java b/src/main/java/com/kingsrook/qbits/sqlworkbench/metadata/SqlWorkbenchNotebookShareTableMetaDataProducer.java index c666f6f..ef0cd43 100644 --- a/src/main/java/com/kingsrook/qbits/sqlworkbench/metadata/SqlWorkbenchNotebookShareTableMetaDataProducer.java +++ b/src/main/java/com/kingsrook/qbits/sqlworkbench/metadata/SqlWorkbenchNotebookShareTableMetaDataProducer.java @@ -25,7 +25,7 @@ import com.kingsrook.qbits.sqlworkbench.SqlWorkbenchQBitConfig; /******************************************************************************* - ** Produces the sqlWorkbenchNotebookShare table. + ** Produces the sql_workbench_notebook_share table. *******************************************************************************/ public class SqlWorkbenchNotebookShareTableMetaDataProducer extends QBitComponentMetaDataProducer { diff --git a/src/main/java/com/kingsrook/qbits/sqlworkbench/metadata/SqlWorkbenchNotebookTableMetaDataProducer.java b/src/main/java/com/kingsrook/qbits/sqlworkbench/metadata/SqlWorkbenchNotebookTableMetaDataProducer.java index d0be1f7..6f7cc61 100644 --- a/src/main/java/com/kingsrook/qbits/sqlworkbench/metadata/SqlWorkbenchNotebookTableMetaDataProducer.java +++ b/src/main/java/com/kingsrook/qbits/sqlworkbench/metadata/SqlWorkbenchNotebookTableMetaDataProducer.java @@ -25,7 +25,7 @@ import com.kingsrook.qbits.sqlworkbench.SqlWorkbenchQBitConfig; /******************************************************************************* - ** Produces the sqlWorkbenchNotebook table. + ** Produces the sql_workbench_notebook table. *******************************************************************************/ public class SqlWorkbenchNotebookTableMetaDataProducer extends QBitComponentMetaDataProducer { diff --git a/src/main/java/com/kingsrook/qbits/sqlworkbench/metadata/SqlWorkbenchNotebookVersionTableMetaDataProducer.java b/src/main/java/com/kingsrook/qbits/sqlworkbench/metadata/SqlWorkbenchNotebookVersionTableMetaDataProducer.java index 66164d2..f62458d 100644 --- a/src/main/java/com/kingsrook/qbits/sqlworkbench/metadata/SqlWorkbenchNotebookVersionTableMetaDataProducer.java +++ b/src/main/java/com/kingsrook/qbits/sqlworkbench/metadata/SqlWorkbenchNotebookVersionTableMetaDataProducer.java @@ -25,7 +25,7 @@ import com.kingsrook.qbits.sqlworkbench.SqlWorkbenchQBitConfig; /******************************************************************************* - ** Produces the sqlWorkbenchNotebookVersion table. + ** Produces the sql_workbench_notebook_version table. *******************************************************************************/ public class SqlWorkbenchNotebookVersionTableMetaDataProducer extends QBitComponentMetaDataProducer { diff --git a/src/main/java/com/kingsrook/qbits/sqlworkbench/metadata/SqlWorkbenchResultSnapshotTableMetaDataProducer.java b/src/main/java/com/kingsrook/qbits/sqlworkbench/metadata/SqlWorkbenchResultSnapshotTableMetaDataProducer.java index c8555f5..c8ef000 100644 --- a/src/main/java/com/kingsrook/qbits/sqlworkbench/metadata/SqlWorkbenchResultSnapshotTableMetaDataProducer.java +++ b/src/main/java/com/kingsrook/qbits/sqlworkbench/metadata/SqlWorkbenchResultSnapshotTableMetaDataProducer.java @@ -25,7 +25,7 @@ import com.kingsrook.qbits.sqlworkbench.SqlWorkbenchQBitConfig; /******************************************************************************* - ** Produces the sqlWorkbenchResultSnapshot table. + ** Produces the sql_workbench_result_snapshot table. *******************************************************************************/ public class SqlWorkbenchResultSnapshotTableMetaDataProducer extends QBitComponentMetaDataProducer { diff --git a/src/main/java/com/kingsrook/qbits/sqlworkbench/metadata/SqlWorkbenchTableMetaDataHelper.java b/src/main/java/com/kingsrook/qbits/sqlworkbench/metadata/SqlWorkbenchTableMetaDataHelper.java index 38a96ed..c446f03 100644 --- a/src/main/java/com/kingsrook/qbits/sqlworkbench/metadata/SqlWorkbenchTableMetaDataHelper.java +++ b/src/main/java/com/kingsrook/qbits/sqlworkbench/metadata/SqlWorkbenchTableMetaDataHelper.java @@ -72,9 +72,9 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey; ** - Record security locks, enforced by the core actions and so for every ** entry point (table UI, /qqq/v1 table API, processes, PVS lookups, ** exports): a notebook (and its cells, versions and snapshots) is visible - ** to its owner (sqlWorkbenchUser = ownerUserId) OR to an audience it is + ** to its owner (sql_workbench_user = ownerUserId) OR to an audience it is ** shared with (a share row whose audienceKey is one of the session's - ** sqlWorkbenchAudience values); only the owner can change it. An + ** sql_workbench_audience values); only the owner can change it. An ** execution is visible to its user only. Workbench admins hold the ** all-access key and see every row. ** - Append-only: versions and the execution audit have no insert / edit / @@ -86,16 +86,16 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey; *******************************************************************************/ public class SqlWorkbenchTableMetaDataHelper { - public static final String JOIN_NOTEBOOK_CELL = "sqlWorkbenchNotebookJoinSqlWorkbenchCell"; - public static final String JOIN_NOTEBOOK_VERSION = "sqlWorkbenchNotebookJoinSqlWorkbenchNotebookVersion"; - public static final String JOIN_NOTEBOOK_SNAPSHOT = "sqlWorkbenchNotebookJoinSqlWorkbenchResultSnapshot"; - public static final String JOIN_NOTEBOOK_SHARE = "sqlWorkbenchNotebookJoinSqlWorkbenchNotebookShare"; - public static final String JOIN_SHARE_NOTEBOOK = "sqlWorkbenchNotebookShareJoinSqlWorkbenchNotebook"; + public static final String JOIN_NOTEBOOK_CELL = "sql_workbench_notebook_join_sql_workbench_cell"; + public static final String JOIN_NOTEBOOK_VERSION = "sql_workbench_notebook_join_sql_workbench_notebook_version"; + public static final String JOIN_NOTEBOOK_SNAPSHOT = "sql_workbench_notebook_join_sql_workbench_result_snapshot"; + public static final String JOIN_NOTEBOOK_SHARE = "sql_workbench_notebook_join_sql_workbench_notebook_share"; + public static final String JOIN_SHARE_NOTEBOOK = "sql_workbench_notebook_share_join_sql_workbench_notebook"; /******************************************************************************* - ** sqlWorkbenchNotebook + ** sql_workbench_notebook *******************************************************************************/ public static QTableMetaData buildNotebookTable(SqlWorkbenchQBitConfig config) throws QException { @@ -125,7 +125,7 @@ public class SqlWorkbenchTableMetaDataHelper /******************************************************************************* - ** sqlWorkbenchCell + ** sql_workbench_cell *******************************************************************************/ public static QTableMetaData buildCellTable(SqlWorkbenchQBitConfig config) throws QException { @@ -148,7 +148,7 @@ public class SqlWorkbenchTableMetaDataHelper /******************************************************************************* - ** sqlWorkbenchNotebookVersion (append-only) + ** sql_workbench_notebook_version (append-only) *******************************************************************************/ public static QTableMetaData buildNotebookVersionTable(SqlWorkbenchQBitConfig config) throws QException { @@ -169,7 +169,7 @@ public class SqlWorkbenchTableMetaDataHelper /******************************************************************************* - ** sqlWorkbenchResultSnapshot (pinned from the notebook UI; label edit and + ** sql_workbench_result_snapshot (pinned from the notebook UI; label edit and ** delete through the table) *******************************************************************************/ public static QTableMetaData buildResultSnapshotTable(SqlWorkbenchQBitConfig config) throws QException @@ -191,7 +191,7 @@ public class SqlWorkbenchTableMetaDataHelper /******************************************************************************* - ** sqlWorkbenchNotebookShare: who, besides the owner, can read a notebook. + ** sql_workbench_notebook_share: who, besides the owner, can read a notebook. *******************************************************************************/ public static QTableMetaData buildNotebookShareTable(SqlWorkbenchQBitConfig config) throws QException { @@ -213,7 +213,7 @@ public class SqlWorkbenchTableMetaDataHelper /******************************************************************************* - ** sqlWorkbenchExecution (the audit; append-only) + ** sql_workbench_execution (the audit; append-only) *******************************************************************************/ public static QTableMetaData buildExecutionTable(SqlWorkbenchQBitConfig config) throws QException { @@ -356,7 +356,7 @@ public class SqlWorkbenchTableMetaDataHelper /******************************************************************************* - ** Owner lock: the session's sqlWorkbenchUser value must equal the field + ** Owner lock: the session's sql_workbench_user value must equal the field ** (read and write). *******************************************************************************/ static RecordSecurityLock ownerLock(SqlWorkbenchQBitConfig config, String fieldName, String... joinNameChain) @@ -376,7 +376,7 @@ public class SqlWorkbenchTableMetaDataHelper /******************************************************************************* ** Share lock: a share row's audienceKey is one of the session's - ** sqlWorkbenchAudience values (read only). + ** sql_workbench_audience values (read only). *******************************************************************************/ static RecordSecurityLock shareLock(SqlWorkbenchQBitConfig config, String fieldName, String... joinNameChain) { diff --git a/src/main/java/com/kingsrook/qbits/sqlworkbench/model/ShareAudienceType.java b/src/main/java/com/kingsrook/qbits/sqlworkbench/model/ShareAudienceType.java index a7165c2..d169843 100644 --- a/src/main/java/com/kingsrook/qbits/sqlworkbench/model/ShareAudienceType.java +++ b/src/main/java/com/kingsrook/qbits/sqlworkbench/model/ShareAudienceType.java @@ -30,7 +30,13 @@ public enum ShareAudienceType implements PossibleValueEnum GROUP("Group", "group"), ALL("Everyone (shared library)", "all"); - public static final String NAME = "sqlWorkbenchShareAudienceType"; + public static final String NAME = "sql_workbench_share_audience_type"; + + /******************************************************************************* + ** The possible-value source name before 1.0.0-RC.3. + *******************************************************************************/ + @Deprecated(since = "1.0.0-RC.3") + public static final String LEGACY_NAME = "sqlWorkbenchShareAudienceType"; private final String label; private final String keyPrefix; diff --git a/src/main/java/com/kingsrook/qbits/sqlworkbench/model/SqlWorkbenchCell.java b/src/main/java/com/kingsrook/qbits/sqlworkbench/model/SqlWorkbenchCell.java index 580c791..48b3d5e 100644 --- a/src/main/java/com/kingsrook/qbits/sqlworkbench/model/SqlWorkbenchCell.java +++ b/src/main/java/com/kingsrook/qbits/sqlworkbench/model/SqlWorkbenchCell.java @@ -29,7 +29,14 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior *******************************************************************************/ public class SqlWorkbenchCell extends QRecordEntity { - public static final String TABLE_NAME = "sqlWorkbenchCell"; + public static final String TABLE_NAME = "sql_workbench_cell"; + + /******************************************************************************* + ** The table name before 1.0.0-RC.3 (owner rule: lowercase snake_case, + ** prefixed with the qbit name). Kept for migrating stored references only. + *******************************************************************************/ + @Deprecated(since = "1.0.0-RC.3") + public static final String LEGACY_TABLE_NAME = "sqlWorkbenchCell"; @QField(isEditable = false, isPrimaryKey = true) private Integer id; diff --git a/src/main/java/com/kingsrook/qbits/sqlworkbench/model/SqlWorkbenchExecution.java b/src/main/java/com/kingsrook/qbits/sqlworkbench/model/SqlWorkbenchExecution.java index ba9cf10..57b25f3 100644 --- a/src/main/java/com/kingsrook/qbits/sqlworkbench/model/SqlWorkbenchExecution.java +++ b/src/main/java/com/kingsrook/qbits/sqlworkbench/model/SqlWorkbenchExecution.java @@ -31,7 +31,14 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior *******************************************************************************/ public class SqlWorkbenchExecution extends QRecordEntity { - public static final String TABLE_NAME = "sqlWorkbenchExecution"; + public static final String TABLE_NAME = "sql_workbench_execution"; + + /******************************************************************************* + ** The table name before 1.0.0-RC.3 (owner rule: lowercase snake_case, + ** prefixed with the qbit name). Kept for migrating stored references only. + *******************************************************************************/ + @Deprecated(since = "1.0.0-RC.3") + public static final String LEGACY_TABLE_NAME = "sqlWorkbenchExecution"; @QField(isEditable = false, isPrimaryKey = true) private Integer id; diff --git a/src/main/java/com/kingsrook/qbits/sqlworkbench/model/SqlWorkbenchNotebook.java b/src/main/java/com/kingsrook/qbits/sqlworkbench/model/SqlWorkbenchNotebook.java index b4120b1..24b3c7f 100644 --- a/src/main/java/com/kingsrook/qbits/sqlworkbench/model/SqlWorkbenchNotebook.java +++ b/src/main/java/com/kingsrook/qbits/sqlworkbench/model/SqlWorkbenchNotebook.java @@ -30,7 +30,14 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior *******************************************************************************/ public class SqlWorkbenchNotebook extends QRecordEntity { - public static final String TABLE_NAME = "sqlWorkbenchNotebook"; + public static final String TABLE_NAME = "sql_workbench_notebook"; + + /******************************************************************************* + ** The table name before 1.0.0-RC.3 (owner rule: lowercase snake_case, + ** prefixed with the qbit name). Kept for migrating stored references only. + *******************************************************************************/ + @Deprecated(since = "1.0.0-RC.3") + public static final String LEGACY_TABLE_NAME = "sqlWorkbenchNotebook"; @QField(isEditable = false, isPrimaryKey = true) private Integer id; diff --git a/src/main/java/com/kingsrook/qbits/sqlworkbench/model/SqlWorkbenchNotebookShare.java b/src/main/java/com/kingsrook/qbits/sqlworkbench/model/SqlWorkbenchNotebookShare.java index e0678d8..c858d3a 100644 --- a/src/main/java/com/kingsrook/qbits/sqlworkbench/model/SqlWorkbenchNotebookShare.java +++ b/src/main/java/com/kingsrook/qbits/sqlworkbench/model/SqlWorkbenchNotebookShare.java @@ -29,11 +29,18 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior ** everyone with workbench access (audienceType ALL - the shared library). ** audienceKey ("user:alice", "group:analysts", "all") is computed by the ** table customizer and is what the record security locks match against the - ** session's sqlWorkbenchAudience key values. + ** session's sql_workbench_audience key values. *******************************************************************************/ public class SqlWorkbenchNotebookShare extends QRecordEntity { - public static final String TABLE_NAME = "sqlWorkbenchNotebookShare"; + public static final String TABLE_NAME = "sql_workbench_notebook_share"; + + /******************************************************************************* + ** The table name before 1.0.0-RC.3 (owner rule: lowercase snake_case, + ** prefixed with the qbit name). Kept for migrating stored references only. + *******************************************************************************/ + @Deprecated(since = "1.0.0-RC.3") + public static final String LEGACY_TABLE_NAME = "sqlWorkbenchNotebookShare"; @QField(isEditable = false, isPrimaryKey = true) private Integer id; diff --git a/src/main/java/com/kingsrook/qbits/sqlworkbench/model/SqlWorkbenchNotebookVersion.java b/src/main/java/com/kingsrook/qbits/sqlworkbench/model/SqlWorkbenchNotebookVersion.java index 2a0db45..5e2665e 100644 --- a/src/main/java/com/kingsrook/qbits/sqlworkbench/model/SqlWorkbenchNotebookVersion.java +++ b/src/main/java/com/kingsrook/qbits/sqlworkbench/model/SqlWorkbenchNotebookVersion.java @@ -29,7 +29,14 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior *******************************************************************************/ public class SqlWorkbenchNotebookVersion extends QRecordEntity { - public static final String TABLE_NAME = "sqlWorkbenchNotebookVersion"; + public static final String TABLE_NAME = "sql_workbench_notebook_version"; + + /******************************************************************************* + ** The table name before 1.0.0-RC.3 (owner rule: lowercase snake_case, + ** prefixed with the qbit name). Kept for migrating stored references only. + *******************************************************************************/ + @Deprecated(since = "1.0.0-RC.3") + public static final String LEGACY_TABLE_NAME = "sqlWorkbenchNotebookVersion"; @QField(isEditable = false, isPrimaryKey = true) private Integer id; diff --git a/src/main/java/com/kingsrook/qbits/sqlworkbench/model/SqlWorkbenchResultSnapshot.java b/src/main/java/com/kingsrook/qbits/sqlworkbench/model/SqlWorkbenchResultSnapshot.java index a195a62..bf30969 100644 --- a/src/main/java/com/kingsrook/qbits/sqlworkbench/model/SqlWorkbenchResultSnapshot.java +++ b/src/main/java/com/kingsrook/qbits/sqlworkbench/model/SqlWorkbenchResultSnapshot.java @@ -30,7 +30,14 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior *******************************************************************************/ public class SqlWorkbenchResultSnapshot extends QRecordEntity { - public static final String TABLE_NAME = "sqlWorkbenchResultSnapshot"; + public static final String TABLE_NAME = "sql_workbench_result_snapshot"; + + /******************************************************************************* + ** The table name before 1.0.0-RC.3 (owner rule: lowercase snake_case, + ** prefixed with the qbit name). Kept for migrating stored references only. + *******************************************************************************/ + @Deprecated(since = "1.0.0-RC.3") + public static final String LEGACY_TABLE_NAME = "sqlWorkbenchResultSnapshot"; @QField(isEditable = false, isPrimaryKey = true) private Integer id; diff --git a/src/test/java/com/kingsrook/qbits/sqlworkbench/SqlWorkbenchQBitProducerTest.java b/src/test/java/com/kingsrook/qbits/sqlworkbench/SqlWorkbenchQBitProducerTest.java index bc0f3d3..e276df0 100644 --- a/src/test/java/com/kingsrook/qbits/sqlworkbench/SqlWorkbenchQBitProducerTest.java +++ b/src/test/java/com/kingsrook/qbits/sqlworkbench/SqlWorkbenchQBitProducerTest.java @@ -32,8 +32,13 @@ import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.Mem import com.kingsrook.qbits.sqlworkbench.metadata.SqlWorkbenchAppMetaDataProducer; import com.kingsrook.qbits.sqlworkbench.metadata.SqlWorkbenchWidgetMetaDataProducer; import com.kingsrook.qbits.sqlworkbench.metadata.SqlWorkbenchWidgetRenderer; +import com.kingsrook.qbits.sqlworkbench.model.ShareAudienceType; +import com.kingsrook.qbits.sqlworkbench.model.SqlWorkbenchCell; import com.kingsrook.qbits.sqlworkbench.model.SqlWorkbenchExecution; import com.kingsrook.qbits.sqlworkbench.model.SqlWorkbenchNotebook; +import com.kingsrook.qbits.sqlworkbench.model.SqlWorkbenchNotebookShare; +import com.kingsrook.qbits.sqlworkbench.model.SqlWorkbenchNotebookVersion; +import com.kingsrook.qbits.sqlworkbench.model.SqlWorkbenchResultSnapshot; import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSTableBackendDetails; import com.kingsrook.qqq.middleware.javalin.QJavalinMetaData; import org.junit.jupiter.api.Test; @@ -67,12 +72,12 @@ class SqlWorkbenchQBitProducerTest extends BaseSqlWorkbenchTest // with a record security lock, and none is hidden. // //////////////////////////////////////////////////////////////////////////// Map> capabilities = new java.util.LinkedHashMap<>(); - capabilities.put("sqlWorkbenchNotebook", List.of(true, true, true)); - capabilities.put("sqlWorkbenchCell", List.of(true, true, true)); - capabilities.put("sqlWorkbenchNotebookShare", List.of(true, true, true)); - capabilities.put("sqlWorkbenchResultSnapshot", List.of(false, true, true)); - capabilities.put("sqlWorkbenchNotebookVersion", List.of(false, false, false)); - capabilities.put("sqlWorkbenchExecution", List.of(false, false, false)); + capabilities.put("sql_workbench_notebook", List.of(true, true, true)); + capabilities.put("sql_workbench_cell", List.of(true, true, true)); + capabilities.put("sql_workbench_notebook_share", List.of(true, true, true)); + capabilities.put("sql_workbench_result_snapshot", List.of(false, true, true)); + capabilities.put("sql_workbench_notebook_version", List.of(false, false, false)); + capabilities.put("sql_workbench_execution", List.of(false, false, false)); for(Map.Entry> entry : capabilities.entrySet()) { String name = entry.getKey(); @@ -90,8 +95,8 @@ class SqlWorkbenchQBitProducerTest extends BaseSqlWorkbenchTest assertThat(((RDBMSTableBackendDetails) table.getBackendDetails()).getTableName()).startsWith("sql_workbench_"); assertThat(table.getSourceQBitName()).isNotBlank(); } - assertThat(qInstance.getSecurityKeyType("sqlWorkbenchUser").getAllAccessKeyName()).isEqualTo("sqlWorkbenchAllAccess"); - assertThat(qInstance.getSecurityKeyType("sqlWorkbenchAudience")).isNotNull(); + assertThat(qInstance.getSecurityKeyType("sql_workbench_user").getAllAccessKeyName()).isEqualTo("sql_workbench_all_access"); + assertThat(qInstance.getSecurityKeyType("sql_workbench_audience")).isNotNull(); //////////////////////////////////////////////////////////////////////// // role-management UIs list available permissions without a QContext // @@ -99,8 +104,8 @@ class SqlWorkbenchQBitProducerTest extends BaseSqlWorkbenchTest //////////////////////////////////////////////////////////////////////// QContext.clear(); assertThat(com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper.getAllAvailablePermissionNames(qInstance)) - .contains("sqlWorkbenchNotebook.read", "sqlWorkbenchNotebookShare.insert", "sqlWorkbench.hasAccess") - .doesNotContain("sqlWorkbenchExecution.delete", "sqlWorkbenchNotebookVersion.insert"); + .contains("sql_workbench_notebook.read", "sql_workbench_notebook_share.insert", "sql_workbench.hasAccess") + .doesNotContain("sql_workbench_execution.delete", "sql_workbench_notebook_version.insert"); loginAs(READER); QWidgetMetaData widget = (QWidgetMetaData) qInstance.getWidget(SqlWorkbenchWidgetMetaDataProducer.NAME); @@ -108,9 +113,9 @@ class SqlWorkbenchQBitProducerTest extends BaseSqlWorkbenchTest QAppMetaData app = qInstance.getApp(SqlWorkbenchAppMetaDataProducer.APP_NAME); assertThat(app.getWidgets()).containsExactly(SqlWorkbenchWidgetMetaDataProducer.NAME); - assertThat(app.getChildren()).extracting(c -> c.getName()).containsExactly("sqlWorkbenchNotebook", "sqlWorkbenchCell", "sqlWorkbenchNotebookShare", - "sqlWorkbenchResultSnapshot", "sqlWorkbenchNotebookVersion", "sqlWorkbenchExecution"); - assertThat(app.getPermissionRules().getPermissionBaseName()).isEqualTo("sqlWorkbench"); + assertThat(app.getChildren()).extracting(c -> c.getName()).containsExactly("sql_workbench_notebook", "sql_workbench_cell", "sql_workbench_notebook_share", + "sql_workbench_result_snapshot", "sql_workbench_notebook_version", "sql_workbench_execution"); + assertThat(app.getPermissionRules().getPermissionBaseName()).isEqualTo("sql_workbench"); /////////////////////////////////////////////////////////////////// // the qbit's own processes are record actions on its tables and // // never app children (the nav only lists tables and the widget) // @@ -137,8 +142,8 @@ class SqlWorkbenchQBitProducerTest extends BaseSqlWorkbenchTest SqlWorkbenchQBitConfig prefixed = new SqlWorkbenchQBitConfig().withBackendName("other").withRegisterRoutes(false); new SqlWorkbenchQBitProducer().withQBitConfig(prefixed).withNamespace("ns").produce(instance); - assertThat(instance.getTable("ns_sqlWorkbenchNotebook")).isNotNull(); - assertThat(instance.getTable("ns_sqlWorkbenchCell").getField("notebookId").getPossibleValueSourceName()).isEqualTo("ns_sqlWorkbenchNotebook"); + assertThat(instance.getTable("ns_sql_workbench_notebook")).isNotNull(); + assertThat(instance.getTable("ns_sql_workbench_cell").getField("notebookId").getPossibleValueSourceName()).isEqualTo("ns_sql_workbench_notebook"); assertThat(instance.getApp("ns_sqlWorkbench")).isNotNull(); assertThat(QJavalinMetaData.of(instance)).isNull(); @@ -153,6 +158,65 @@ class SqlWorkbenchQBitProducerTest extends BaseSqlWorkbenchTest + /******************************************************************************* + ** Owner naming rule (1.0.0-RC.3): table, possible-value source, permission + ** and security key names are lowercase snake_case prefixed with the qbit's + ** name; the pre-RC.3 names stay available only as deprecated constants. + *******************************************************************************/ + @Test + @SuppressWarnings("deprecation") + void testSnakeCaseNamesAndLegacyConstants() throws Exception + { + List tableNames = List.of(SqlWorkbenchNotebook.TABLE_NAME, SqlWorkbenchCell.TABLE_NAME, SqlWorkbenchNotebookVersion.TABLE_NAME, + SqlWorkbenchResultSnapshot.TABLE_NAME, SqlWorkbenchNotebookShare.TABLE_NAME, SqlWorkbenchExecution.TABLE_NAME); + assertThat(tableNames).containsExactly("sql_workbench_notebook", "sql_workbench_cell", "sql_workbench_notebook_version", + "sql_workbench_result_snapshot", "sql_workbench_notebook_share", "sql_workbench_execution"); + assertThat(tableNames).allSatisfy(name -> assertThat(name).matches("sql_workbench_[a-z_]+")); + assertThat(List.of(SqlWorkbenchNotebook.LEGACY_TABLE_NAME, SqlWorkbenchCell.LEGACY_TABLE_NAME, SqlWorkbenchNotebookVersion.LEGACY_TABLE_NAME, + SqlWorkbenchResultSnapshot.LEGACY_TABLE_NAME, SqlWorkbenchNotebookShare.LEGACY_TABLE_NAME, SqlWorkbenchExecution.LEGACY_TABLE_NAME)) + .containsExactly("sqlWorkbenchNotebook", "sqlWorkbenchCell", "sqlWorkbenchNotebookVersion", "sqlWorkbenchResultSnapshot", + "sqlWorkbenchNotebookShare", "sqlWorkbenchExecution"); + assertThat(ShareAudienceType.NAME).isEqualTo("sql_workbench_share_audience_type"); + assertThat(ShareAudienceType.LEGACY_NAME).isEqualTo("sqlWorkbenchShareAudienceType"); + + SqlWorkbenchQBitConfig config = new SqlWorkbenchQBitConfig(); + assertThat(config.getAccessPermissionName()).isEqualTo("sql_workbench.hasAccess"); + assertThat(config.getWritePermissionName()).isEqualTo("sql_workbench.write"); + assertThat(config.getSharePermissionName()).isEqualTo("sql_workbench.share"); + assertThat(config.getAdminPermissionNames()).contains("sql_workbench_admin.hasAccess"); + assertThat(config.getUserPermissionNames()).allSatisfy(name -> assertThat(name).startsWith("sql_workbench")); + assertThat(config.getUserSecurityKeyTypeName()).isEqualTo("sql_workbench_user"); + assertThat(config.getAudienceSecurityKeyTypeName()).isEqualTo("sql_workbench_audience"); + assertThat(config.getAllAccessSecurityKeyName()).isEqualTo("sql_workbench_all_access"); + assertThat(SqlWorkbenchQBitConfig.LEGACY_ACCESS_PERMISSION_BASE_NAME).isEqualTo("sqlWorkbench"); + assertThat(SqlWorkbenchQBitConfig.LEGACY_ADMIN_PERMISSION_BASE_NAME).isEqualTo("sqlWorkbenchAdmin"); + assertThat(SqlWorkbenchQBitConfig.LEGACY_USER_SECURITY_KEY_TYPE_NAME).isEqualTo("sqlWorkbenchUser"); + + //////////////////////////////////////////////////////////////////// + // prefixes are normalized, so a prefixed name stays snake_case // + //////////////////////////////////////////////////////////////////// + assertThat(config.withTableNamePrefix("ns").applyPrefix(SqlWorkbenchNotebook.TABLE_NAME)).isEqualTo("ns_sql_workbench_notebook"); + assertThat(config.withTableNamePrefix("ns_").applyPrefix(SqlWorkbenchNotebook.TABLE_NAME)).isEqualTo("ns_sql_workbench_notebook"); + assertThat(config.withTableNamePrefix("myNs").applyPrefix(SqlWorkbenchNotebook.TABLE_NAME)).isEqualTo("my_ns_sql_workbench_notebook"); + assertThat(config.withTableNamePrefix(null).applyPrefix(SqlWorkbenchNotebook.TABLE_NAME)).isEqualTo("sql_workbench_notebook"); + + ////////////////////////////////////////////////////////////////////// + // with no prefix, the default RDBMS backend table name equals the // + // QQQ table name: no inference or DDL is needed // + ////////////////////////////////////////////////////////////////////// + QInstance instance = new QInstance(); + instance.addBackend(defineH2Backend("plain")); + new SqlWorkbenchQBitProducer().withQBitConfig(new SqlWorkbenchQBitConfig().withBackendName("plain").withRegisterRoutes(false)).produce(instance); + for(String name : tableNames) + { + assertThat(instance.getTable(name)).as(name).isNotNull(); + } + assertThat(instance.getPossibleValueSource(ShareAudienceType.NAME)).isNotNull(); + assertThat(instance.getSecurityKeyType("sql_workbench_user")).isNotNull(); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -187,7 +251,7 @@ class SqlWorkbenchQBitProducerTest extends BaseSqlWorkbenchTest .anyMatch(e -> e.contains("maxRows")).anyMatch(e -> e.contains("writePermissionName")).anyMatch(e -> e.contains("defaultPageSize must not exceed")); assertThat(errors(new SqlWorkbenchQBitConfig().withBackendName("db").withMaxAffectedRowsWithoutAcknowledgement(10).withMaxAffectedRows(5), instance)) .anyMatch(e -> e.contains("maxAffectedRowsWithoutAcknowledgement must not exceed")); - assertThat(errors(new SqlWorkbenchQBitConfig().withBackendName("db").withAdminPermissionBaseName("sqlWorkbench"), instance)) + assertThat(errors(new SqlWorkbenchQBitConfig().withBackendName("db").withAdminPermissionBaseName("sql_workbench"), instance)) .anyMatch(e -> e.contains("adminPermissionBaseName must differ")); assertThat(errors(new SqlWorkbenchQBitConfig().withBackendName("db").withAdminPermissionBaseName(""), instance)) .anyMatch(e -> e.contains("adminPermissionBaseName is required")); @@ -211,8 +275,8 @@ class SqlWorkbenchQBitProducerTest extends BaseSqlWorkbenchTest .withWriteCredentials("db", new SqlWorkbenchCredentials().withUsername("rw").withPassword("secret2")); assertThat(config.toString()).doesNotContain("secret").doesNotContain("ro\"").contains("db"); assertThat(config.getReadOnlyCredentials().get("db").toString()).doesNotContain("secret").doesNotContain("ro,"); - assertThat(config.getAccessPermissionName()).isEqualTo("sqlWorkbench.hasAccess"); - assertThat(config.getAdminPermissionName()).isEqualTo("sqlWorkbenchAdmin.hasAccess"); + assertThat(config.getAccessPermissionName()).isEqualTo("sql_workbench.hasAccess"); + assertThat(config.getAdminPermissionName()).isEqualTo("sql_workbench_admin.hasAccess"); //////////////////////////////////////////////////////////////// // credentials never serialize - jdbcUrl can carry a password // @@ -220,7 +284,7 @@ class SqlWorkbenchQBitProducerTest extends BaseSqlWorkbenchTest String credentialsJson = com.kingsrook.qqq.backend.core.utils.JsonUtils.toJson(new SqlWorkbenchCredentials() .withUsername("ro").withPassword("secret").withJdbcUrl("jdbc:postgresql://db/app?password=urlsecret")); assertThat(credentialsJson).doesNotContain("secret").doesNotContain("urlsecret").doesNotContain("jdbc:"); - assertThat(config.getAdminPermissionRules().getPermissionBaseName()).isEqualTo("sqlWorkbenchAdmin"); + assertThat(config.getAdminPermissionRules().getPermissionBaseName()).isEqualTo("sql_workbench_admin"); assertThat(config.getDefaultBackendNameForTables()).isEqualTo("db"); assertThat(config.applyPrefix("t")).isEqualTo("t"); assertThat(config.withTableNamePrefix("p").applyPrefix("t")).isEqualTo("p_t");