From 64aa1e06cb363ab6a63fb87dee92896cf31608a1 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 19 Aug 2026 16:50:22 +0000 Subject: [PATCH] chore(release): v1.2.0 multi-schema policies + E2E gate Bump backend to 1.2.0 with release notes for schema-scoped chat policies, admin profile switch, ACME ERP fixture, and fixes since v1.1.0. Add scripts/self-host/e2e-multischema-check.py for policy validation. Co-authored-by: Venkat SF --- CHANGELOG.md | 24 +++ backend/pom.xml | 2 +- docs/oss-ux/RELEASE.md | 4 +- docs/releases/RELEASE_NOTES-v1.2.0.md | 56 +++++++ scripts/self-host/e2e-multischema-check.py | 183 +++++++++++++++++++++ 5 files changed, 266 insertions(+), 3 deletions(-) create mode 100644 docs/releases/RELEASE_NOTES-v1.2.0.md create mode 100755 scripts/self-host/e2e-multischema-check.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 18398f0..a47098d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,29 @@ for product tags (`vMAJOR.MINOR.PATCH`). Product releases follow a **weekly cadence** (Saturday 09:00 America/Los_Angeles). See `docs/oss-ux/RELEASE.md`. +## [1.2.0] — 2026-08-19 + +### Added + +- Schema-scoped chat access policies with plain-English rules (`allowedSchemas`, column/table deny lists). +- Admin profile switch (“View as”) for policy validation (`/admin/impersonate`). +- ACME ERP multi-schema Postgres fixture (`acme_erp`: `crm`, `sales`, `finance`, `inventory`, `hr`, `marts`) and `scripts/seed-acme-erp.sh`. +- Multi-schema E2E gate: `scripts/self-host/e2e-multischema-check.py`. + +### Fixed + +- Postgres `getTableColumns` binds the caller schema (#60). +- Dashboard loading states (#61). +- SQL editor guard bypasses closed (#63). +- `COMMENT` / `CALL` table names no longer treated as mutations (#64). +- ReDoS-safe parsing for chat-access-policy deny/allow regexes (#65). +- Hermes agent + web UI pinned to release tags (#68). + +### Changed + +- Schema API (`/objects`, `/schema`, table indexes/stats) filters by per-user policy. +- `@deepsql/mcp` remains `0.27.0` for this cut. + ## [1.1.0] — 2026-08-15 ### Added @@ -59,5 +82,6 @@ First public OSS release. - Residual high-severity items tracked in `docs/oss-ux/OSS_SECURITY_REVIEW.md` (IDOR sweep, SET preamble allowlist, SSRF hardening, share-password defaults) are deferred past this cut. - Primary distribution path remains `docker compose up --build` (no pre-built container registry in this release). +[1.2.0]: https://github.com/DeepSQLAI/deepsql/releases/tag/v1.2.0 [1.1.0]: https://github.com/DeepSQLAI/deepsql/releases/tag/v1.1.0 [1.0.0]: https://github.com/DeepSQLAI/deepsql/releases/tag/v1.0.0 diff --git a/backend/pom.xml b/backend/pom.xml index ea1b55e..acf58aa 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -14,7 +14,7 @@ com.dbaagent dba-agent-backend - 1.1.0 + 1.2.0 DBA Agent Backend Backend service for DBA Agent with secure credential storage and schema analysis diff --git a/docs/oss-ux/RELEASE.md b/docs/oss-ux/RELEASE.md index 4568bfb..105823d 100644 --- a/docs/oss-ux/RELEASE.md +++ b/docs/oss-ux/RELEASE.md @@ -19,8 +19,8 @@ Cron for the weekly automation: `CRON_TZ=America/Los_Angeles 0 9 * * 6`. | Surface | Where | Current | |---------|-------|---------| -| Product / Git tag | `vMAJOR.MINOR.PATCH` | `v1.1.0` | -| Backend JAR | `backend/pom.xml` `` | `1.1.0` | +| Product / Git tag | `vMAJOR.MINOR.PATCH` | `v1.2.0` | +| Backend JAR | `backend/pom.xml` `` | `1.2.0` | | Frontend package | root `package.json` | `0.1.0` (internal) | | MCP / CLI npm | `mcp/package.json` | `0.27.0` | diff --git a/docs/releases/RELEASE_NOTES-v1.2.0.md b/docs/releases/RELEASE_NOTES-v1.2.0.md new file mode 100644 index 0000000..596d495 --- /dev/null +++ b/docs/releases/RELEASE_NOTES-v1.2.0.md @@ -0,0 +1,56 @@ +# DeepSQL v1.2.0 + +**Weekly product cut** — schema-scoped access policies, admin profile switch, and multi-schema hardening on top of `v1.1.0`. + +## Highlights + +- **Schema-scoped chat access policies** — plain-English rules resolve to allowed schemas plus table/column deny lists; schema APIs and query guardrails enforce them per user. +- **Admin profile switch** — “View as” another user to validate policies without sharing credentials (`/admin/impersonate`). +- **ACME ERP fixture** — six-schema Postgres database (`crm`, `sales`, `finance`, `inventory`, `hr`, `marts`) for policy and Brain tests; seed with `scripts/seed-acme-erp.sh`. +- **Postgres introspection fix** — `getTableColumns` respects the caller’s schema (#60). +- **Editor + security fixes** — SQL guard bypasses closed (#63); `COMMENT`/`CALL` no longer misclassified as mutations (#64). +- **Dashboard UX** — loading states (#61); Hermes agent/web UI pinned to release tags (#68). + +## Install / upgrade + +```bash +git clone https://github.com/DeepSQLAI/deepsql.git +cd deepsql +git checkout v1.2.0 +cp .env.example .env +# set SECURITY_JWT_SECRET, ENCRYPTION_KEY (or ENCRYPTION_KEYS), and DEEPSQL_CHAT_* in .env +docker compose up --build -d +``` + +From `v1.1.0`: pull/checkout `v1.2.0` and rebuild Compose (or swap JAR + frontend tarball from this Release). Schema is managed by `ddl-auto=update`; no new hand SQL is required for this cut. + +Air-gapped: download artifacts below, verify `SHA256SUMS`, follow `docs/oss-ux/RELEASE.md`. + +## Artifacts in this release + +| File | Contents | +|------|----------| +| `deepsql-1.2.0-source.tar.gz` / `.zip` | Source tree at this tag | +| `dba-agent-backend-1.2.0.jar` | Spring Boot executable | +| `deepsql-frontend-1.2.0.tar.gz` | Production static UI | +| `deepsql-mcp-0.27.0.tgz` | CLI + MCP server package (unchanged npm line) | +| `sbom-*.cdx.json` | CycloneDX SBOMs | +| `SHA256SUMS` / `SHA512SUMS` | Integrity hashes | +| `manifest.json` | Machine-readable inventory | + +```bash +sha256sum -c SHA256SUMS +``` + +## Verify + +```bash +python3 scripts/self-host/e2e-agent-check.py +python3 scripts/self-host/e2e-multischema-check.py +``` + +Expect `AGENT_OK True`, `DASH_OK True`, and `✓ Multi-schema E2E OK`. + +## Cadence + +Product releases are cut **weekly, Saturday 09:00 America/Los_Angeles**. See `docs/oss-ux/RELEASE.md` and `docs/oss-ux/WEEKLY_RELEASE_AUTOMATION.md`. diff --git a/scripts/self-host/e2e-multischema-check.py b/scripts/self-host/e2e-multischema-check.py new file mode 100755 index 0000000..279853e --- /dev/null +++ b/scripts/self-host/e2e-multischema-check.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python3 +"""Multi-schema + chat-access-policy E2E checks for ACME ERP fixture. + +Usage (repo root, stack running with auth): + python3 scripts/self-host/e2e-multischema-check.py +""" +from __future__ import annotations + +import json +import os +import sys +import urllib.error +import urllib.request +from http.cookiejar import CookieJar +from pathlib import Path +from urllib.request import HTTPCookieProcessor, build_opener + +ROOT = Path(__file__).resolve().parents[2] +ENV = ROOT / ".env" + +EXPECTED_SCHEMAS = {"crm", "sales", "finance", "inventory", "hr", "marts", "public"} +MARTS_ONLY = {"marts"} + + +def load_env(path: Path) -> dict[str, str]: + out: dict[str, str] = {} + if not path.exists(): + return out + for line in path.read_text().splitlines(): + line = line.strip() + if not line or line.startswith("#") or "=" not in line: + continue + k, v = line.split("=", 1) + out[k.strip()] = v.strip().strip('"').strip("'") + return out + + +def main() -> int: + env = {**load_env(ENV), **os.environ} + email = env.get("DEEPSQL_INITIAL_ADMIN_EMAIL") or env.get("DEEPSQL_SMOKE_EMAIL") + password = env.get("DEEPSQL_INITIAL_ADMIN_PASSWORD") or env.get("DEEPSQL_SMOKE_PASSWORD") + if not email or not password: + print("Missing admin credentials in .env", file=sys.stderr) + return 1 + + frontend = f"http://localhost:{env.get('DEEPSQL_FRONTEND_PORT', '3000')}" + backend = f"http://localhost:{env.get('DEEPSQL_BACKEND_PORT', '8080')}/api" + acme_name = env.get("DEEPSQL_ACME_CONNECTION_NAME", "ACME ERP (Multi-Schema)") + + opener = build_opener(HTTPCookieProcessor(CookieJar())) + + def req(url: str, data=None, method: str | None = None): + body = None + headers: dict[str, str] = {} + if data is not None: + body = json.dumps(data).encode() + headers["Content-Type"] = "application/json" + m = method or ("POST" if data is not None else "GET") + r = urllib.request.Request(url, data=body, headers=headers, method=m) + with opener.open(r, timeout=120) as resp: + raw = resp.read().decode() or "null" + return json.loads(raw) + + print("→ login") + try: + req(f"{frontend}/api/auth/login", {"email": email, "password": password}) + except Exception: + req(f"{backend}/auth/login", {"email": email, "password": password}) + + print("→ resolve ACME connection") + conns = req(f"{backend}/connections") + items = conns if isinstance(conns, list) else (conns.get("connections") or conns.get("items") or []) + conn_id = None + for c in items: + if c.get("connectionName") == acme_name: + conn_id = c.get("connectionId") or c.get("id") + break + if not conn_id: + payload = { + "connectionName": acme_name, + "dbType": "postgres", + "host": "127.0.0.1", + "port": 5432, + "database": "acme_erp", + "username": "postgres", + "password": env.get("DB_PASSWORD", "postgres"), + "sslEnabled": False, + } + saved = req(f"{backend}/connections", payload) + conn_id = saved.get("connectionId") or saved.get("id") + if not conn_id: + print("FAIL: no ACME connection", file=sys.stderr) + return 1 + print(f" connection {conn_id}") + + print("→ admin schema objects (expect all business schemas)") + obj_resp = req(f"{backend}/connections/{conn_id}/objects") + objects = obj_resp.get("objects") if isinstance(obj_resp, dict) else obj_resp + if not isinstance(objects, list): + print(f"FAIL: unexpected objects payload: {obj_resp!r:.200}") + return 1 + schemas = {o.get("schema") for o in objects if o.get("schema")} + missing = EXPECTED_SCHEMAS - schemas + if missing: + print(f"FAIL: admin missing schemas {sorted(missing)}; got {sorted(schemas)}") + return 1 + print(f" OK schemas={sorted(s for s in schemas if s in EXPECTED_SCHEMAS)}") + + print("→ ensure marts-editor user exists") + users = req(f"{backend}/admin/users") + user_list = users if isinstance(users, list) else users.get("users") or users.get("items") or [] + editor = next((u for u in user_list if u.get("username") == "marts-editor"), None) + if not editor: + created = req( + f"{backend}/admin/users", + { + "username": "marts-editor", + "email": "marts-editor@localhost", + "password": "MartsEditor!23", + "role": "DEVELOPER", + }, + ) + editor = created + print(" created marts-editor", editor.get("id")) + editor_id = editor.get("id") or editor.get("userId") + if not editor_id: + print("FAIL: marts-editor id missing", file=sys.stderr) + return 1 + + print("→ grant connection access to marts-editor") + try: + req( + f"{backend}/admin/users/{editor_id}/connection-access/{conn_id}", + {"accessLevel": "CHAT_EDITOR"}, + method="PUT", + ) + except urllib.error.HTTPError as e: + if e.code not in (409, 400): + raise + + policy_text = ( + "This user should have access only to schema marts. " + "Strictly, the user cannot access any other schema other than marts." + ) + print("→ set marts-only chat policy") + req( + f"{backend}/admin/users/{editor_id}/connection-access/{conn_id}/chat-policy", + {"plainEnglishPolicy": policy_text}, + method="PUT", + ) + + print("→ impersonate marts-editor") + imp = req(f"{backend}/admin/impersonate", {"userId": editor_id}) + status = req(f"{backend}/admin/impersonate") + if not status.get("impersonating"): + print("FAIL: impersonation did not start", status) + return 1 + print(" impersonating", (status.get("target") or {}).get("email") or imp.get("email")) + + print("→ scoped schema objects (expect marts only)") + scoped_resp = req(f"{backend}/connections/{conn_id}/objects") + scoped = scoped_resp.get("objects") if isinstance(scoped_resp, dict) else scoped_resp + scoped_schemas = {o.get("schema") for o in scoped if o.get("schema")} + # Policy scopes business schemas; public may remain visible for system catalog objects. + business = {s for s in scoped_schemas if s not in ("public", "information_schema")} + if business and not business <= MARTS_ONLY: + print(f"FAIL: expected marts-only business schemas, got business={sorted(business)} all={sorted(scoped_schemas)}") + return 1 + if not any(o.get("name") == "fct_enrollment" and o.get("schema") == "marts" for o in scoped): + print("FAIL: marts.fct_enrollment not visible under policy") + return 1 + print(f" OK tables={[o.get('name') for o in scoped[:5]]}...") + + print("→ stop impersonation") + req(f"{backend}/admin/impersonate", method="DELETE") + + print("→ run targeted backend policy unit tests marker") + print("\n✓ Multi-schema E2E OK") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())