Skip to content

feat(connections): open a database from the terminal and DDEV (#1486)#1858

Merged
datlechin merged 3 commits into
mainfrom
feat/1486-open-db-from-terminal
Jul 13, 2026
Merged

feat(connections): open a database from the terminal and DDEV (#1486)#1858
datlechin merged 3 commits into
mainfrom
feat/1486-open-db-from-terminal

Conversation

@datlechin

@datlechin datlechin commented Jul 13, 2026

Copy link
Copy Markdown
Member

Closes #1486.

Opens a project database in TablePro from the shell, and makes ddev tablepro work.

What ships

  • ddev tablepro: submitted upstream as a DDEV global host command, feat: add ddev tablepro host command for TablePro ddev/ddev#8580. DDEV creates it automatically for anyone with TablePro in /Applications, so there is nothing to copy or install. It reads DDEV_HOST_DB_PORT on every run, because DDEV reassigns the database port on each ddev start, and calls open -b com.TablePro so TablePro wins even when TablePlus also owns the mysql:// scheme. Nothing in the app had to change for it to work.
  • tablepro command: install it from Settings > General > Command Line. It writes a two-line shim to /usr/local/bin that always opens TablePro. /usr/local/bin is root-owned on a stock macOS install and often absent entirely, so when TablePro cannot write there it asks macOS for an administrator password (osascript ... with administrator privileges) and creates the folder and the command. Cancelling the prompt writes nothing. If even that fails, Settings falls back to showing the exact sudo command.
  • Always Allow: the "Open External Database Connection?" alert used to appear on every single open, so ddev tablepro needed a click every time. The alert now offers Always Allow, and Settings > General > Trusted Links lists and revokes what you trusted.
  • Enviroment is accepted as an alias for the env URL parameter. That is the misspelling TablePlus and DDEV actually emit, and it was being silently dropped.

Security boundary

Always Allow is offered only for a database on your own machine (localhost, 127.0.0.1/8, ::1), and that check is enforced on read as well as write, so a poisoned defaults blob cannot resurrect trust for a remote host. A link to a remote host asks every time, with no way to silence it. This kills the "a web page makes TablePro connect to the attacker's server" case.

The trust key is database type + host + database + username + the URL's name parameter. It deliberately excludes the port, because DDEV hands out a new host port on every start and a port-bearing key would re-prompt forever. Because every DDEV project shares the same default credentials (db:db@127.0.0.1/db), name=ddev-<project> is what keeps one project's trust from covering another.

Trust only skips the initial connect alert. SQL from a link (?query=), a filter (?condition=), and a pre-connect script each still ask for their own confirmation, and a transient connection never carries a pre-connect script.

Notes for review

  • ExternalConnectionGate holds the decision (trusted? prompt? persist?) as a plain unit behind two protocol seams, so it is unit-tested without an NSAlert. ExternalConnectionAlertPrompt is the untestable UI half, matching how HostKeyStore/HostKeyVerifier are split.
  • The alert's Cancel button keeps the Escape key. The previous code overwrote Cancel's key equivalent with Return, which silently removed AppKit's automatic Escape binding. Enter is now bound to nothing, so a stray Enter still cannot connect.
  • The tablepro shim is generated from a string and written at install time rather than bundled as a Resource. A script inside Contents/MacOS would be fed to the unguarded strip -x loop in scripts/build-release.sh and break every release build.
  • The privileged install runs a shell command as root, so the command string is built through OSAScriptPrivilegedShell.quote (POSIX single-quote escaping) and then escaped again for the AppleScript string literal. Both layers are unit-tested, including a directory name that tries to inject a second command (x'; touch canary; echo ') and asserts the canary is never created. Nothing user-supplied reaches the string: the only interpolated values are the tool path and a fixed directory.

Tests

28 new tests across ExternalConnectionTrustStoreTests, ExternalConnectionGateTests, CommandLineToolInstallerTests, plus two parser tests. They cover the loopback lookalikes (127.evil.com, 0x7f.0.0.1), the poisoned-defaults case, port-independent trust across a DDEV restart, trust not leaking between projects, and the installer refusing to overwrite or delete a foreign file at /usr/local/bin/tablepro.

swiftlint --strict clean. Build and tests green.

Follow-ups, not in this PR

@mintlify

mintlify Bot commented Jul 13, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
TablePro 🟢 Ready View Preview Jul 13, 2026, 1:03 AM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f889db4fdd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

internal init(connection: DatabaseConnection, scopeName: String?) {
self.init(
databaseType: connection.type.rawValue,
host: connection.host,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Include tunnel identity in trust keys

Because the key and isLoopbackHost use only connection.host, SSH/tunneled URLs are treated as local whenever the database host on the remote side is 127.0.0.1. For example, after trusting a DDEV link like mysql://db:db@127.0.0.1/.../db?name=ddev-shop, a URL such as mysql+ssh://deploy:pw@evil.example/db:db@127.0.0.1/db?name=ddev-shop builds the same trust key while the actual SSH endpoint is stored separately in sshConfig, so ExternalConnectionGate can skip the confirmation for a remote connection. Include the tunnel/SSH identity in the trust decision or disallow Always Allow when a tunnel is enabled.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ca7f771a4d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

for octet in octets {
guard !octet.isEmpty,
octet.allSatisfy({ $0.isASCII && $0.isNumber }),
let value = Int(octet), value <= 255

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject non-canonical IPv4 before trusting

Because the loopback check parses octets with Int, hosts such as 0127.0.0.1 are classified as 127/8 and get the Always Allow path. In environments where the database client/system resolver accepts legacy IPv4 notation, that same host can resolve as octal 87.0.0.1, so a remote connection can be persisted as trusted and later skip the external-link confirmation. Reject IPv4 octets with leading zeros (or resolve/canonicalize before comparing) before allowing loopback trust.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 59ad55e2e8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

internal init(connection: DatabaseConnection, scopeName: String?) {
self.init(
databaseType: connection.type.rawValue,
host: connection.host,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Include MongoDB seed hosts in trust decisions

When a saved MongoDB connection has host == 127.0.0.1 but additionalFields["mongoHosts"] contains other seeds, this key records only connection.host, so key.isLoopbackHost offers and persists Always Allow. The driver later prefers additionalFields["mongoHosts"] over config.host (Plugins/MongoDBDriverPlugin/MongoDBPluginDriver.swift:58), so a URL matching the first seed can skip the external-link prompt while the actual connection includes remote hosts. Include the effective Mongo host list in the trust check/key, or suppress Always Allow for MongoDB seed-list connections.

Useful? React with 👍 / 👎.

@datlechin datlechin merged commit 9d16ed1 into main Jul 13, 2026
3 checks passed
@datlechin datlechin deleted the feat/1486-open-db-from-terminal branch July 13, 2026 01:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Open project DB from the terminal / DDEV (e.g. ddev tablepro)

1 participant