Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
2 changes: 1 addition & 1 deletion backend/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

<groupId>com.dbaagent</groupId>
<artifactId>dba-agent-backend</artifactId>
<version>1.1.0</version>
<version>1.2.0</version>
<name>DBA Agent Backend</name>
<description>Backend service for DBA Agent with secure credential storage and schema analysis</description>

Expand Down
4 changes: 2 additions & 2 deletions docs/oss-ux/RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` `<version>` | `1.1.0` |
| Product / Git tag | `vMAJOR.MINOR.PATCH` | `v1.2.0` |
| Backend JAR | `backend/pom.xml` `<version>` | `1.2.0` |
| Frontend package | root `package.json` | `0.1.0` (internal) |
| MCP / CLI npm | `mcp/package.json` | `0.27.0` |

Expand Down
56 changes: 56 additions & 0 deletions docs/releases/RELEASE_NOTES-v1.2.0.md
Original file line number Diff line number Diff line change
@@ -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`.
183 changes: 183 additions & 0 deletions scripts/self-host/e2e-multischema-check.py
Original file line number Diff line number Diff line change
@@ -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())
Loading