New ssh key api - #158
Conversation
A challenge asking only for a more recent login carries a max age, which was passed to the shell argument escaper as an integer where a string is required. The CLI exited with a TypeError instead of prompting the user to log in again. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
SSH keys are now managed at /users/{id}/ssh-keys. The new API identifies a key
by a string ID rather than a number, names its label "label" rather than
"title", and reports a SHA-256 fingerprint in OpenSSH format rather than an
MD5 one.
Fingerprints are computed the same way here, so a local key is still matched
to the one on your account. The ssh-key:list columns are renamed to match, and
ssh-key:add keeps its --name option. ssh-key:delete accepts the new IDs, which
the previous check for a numeric argument rejected. Adding a key that is
already registered is now reported rather than passing silently.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
The new SSH key API methods introduce inconsistent error handling (raw BadResponseException) compared to established ApiResponseException wrapping, which should be aligned before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR migrates the legacy CLI’s SSH key management from the older account/SSH-key representation to a newer SSH keys API, updating the CLI to use SHA-256 (OpenSSH-style) fingerprints and a new SSH key model.
Changes:
- Added a dedicated
Platformsh\Cli\Model\SshKeymodel and updated commands/services to use new fields (id,label,sha256). - Implemented new SSH key API methods in
Api(list/get/add/delete) including pagination handling and cache invalidation. - Updated fingerprint calculation to return OpenSSH-style
SHA256:fingerprints and added PHPUnit coverage for the fingerprint behavior.
File summaries
| File | Description |
|---|---|
| legacy/tests/Service/SshKeyTest.php | Adds tests for SHA-256/OpenSSH fingerprint generation and invalid key handling. |
| legacy/src/Service/SshKey.php | Switches account-key matching from MD5 to SHA-256/OpenSSH fingerprint format. |
| legacy/src/Service/Api.php | Adds SSH keys API endpoints (list/get/add/delete), pagination, and dedicated caching. |
| legacy/src/Model/SshKey.php | Introduces a CLI-level SSH key model for the new API representation. |
| legacy/src/Event/LoginRequiredEvent.php | Ensures login option values are consistently stringified for shell escaping. |
| legacy/src/Command/SshKey/SshKeyListCommand.php | Updates displayed/output columns to match new key fields (label, sha256). |
| legacy/src/Command/SshKey/SshKeyDeleteCommand.php | Updates deletion flow to use new key IDs and new API delete operation. |
| legacy/src/Command/SshKey/SshKeyAddCommand.php | Updates add flow to use new API and handles “already registered” (409) response. |
| legacy/phpstan-baseline.neon | Removes a baseline entry that is no longer applicable after key-ID typing changes. |
Review details
Suppressed comments (2)
legacy/src/Service/Api.php:994
- In getSshKey(), non-404 failures are rethrown as BadResponseException. For consistency with the rest of the API layer (and to preserve the richer error details formatting), it should rethrow ApiResponseException::create(...) instead.
if ($e->getResponse()->getStatusCode() === 404) {
return null;
}
throw $e;
}
legacy/src/Service/Api.php:1027
- deleteSshKey() currently lets BadResponseException bubble up directly. To keep error handling consistent with other direct HTTP calls, catch BadResponseException and rethrow ApiResponseException::create(...).
$this->getHttpClient()->request('DELETE', $this->sshKeysUrl() . '/' . rawurlencode($id));
$this->clearSshKeysCache();
- Files reviewed: 9/9 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| $response = $this->getHttpClient()->request('GET', $url); | ||
| $data = (array) Utils::jsonDecode((string) $response->getBody(), true); |
|
📋 PR Summary Migrates the legacy PHP CLI's SSH key handling from the old Accounts API (via the platformsh-client library) to the new Auth API SSH keys endpoints. A new Changes
|
There was a problem hiding this comment.
Note
Reviewed — No blocking findings · 🔵 3 minor points
🔍 Full review · 9 files reviewed
🔍 What this review checked
- The test vector's expected fingerprint SHA256:Zc8rf0C3ZFAVs8mnWl4r6jKmJN8kutsoBK2h4UkXPp8 is the correct unpadded base64 SHA-256 of that key blob (recomputed independently).
- No references to the old fields ($key->title, ->fingerprint, ->key_id) or to Platformsh\Client\Model\SshKey remain, and getLegacyAccountInfo has no leftover callers.
- addSshKey() and deleteSshKey() both invalidate the new session-scoped 'ssh-keys' cache key, and both commands re-warm it with getSshKeys(true).
- The removed phpstan-baseline entry is the only baseline entry referencing SshKey code, so no baseline entry is left unmatched.
- The is_array() cache check makes a cached empty key list a hit rather than a miss, unlike the old truthiness check.
Verification. The diff adds legacy/tests/Service/SshKeyTest.php covering only getPublicKeyFingerprint (valid and invalid key); nothing covers the new Api::getSshKeys pagination/caching, getSshKey 404 handling, addSshKey 409 handling or deleteSshKey. The legacy-php job in .github/workflows/ci.yml runs php-cs-fixer, phpstan (level 8 + baseline) and ./scripts/test/unit.sh over these files.
Review details
- Commit: 8672834
- Model: claude-opus-5
🔵 Minor points
legacy/src/Service/Api.php:988— The new SSH key methods let raw Guzzle exceptions escape, unlike every other direct HTTP call in this codebase (e.g.Api::getTasks()at line ~1914,TaskRunCommand,TeamUserAddCommand, which all dothrow ApiResponseException::create($e->getRequest(), $e->getResponse(), $e)).getSshKey()maps only 404 to null; combined with the removal of theis_numeric($id)guard in SshKeyDeleteCommand,ssh-key:delete <malformed-id>now sends the ID to the API and a 400/422 response surfaces as an unhandledGuzzleHttp\Exception\ClientExceptionwith a truncated-body message instead of "SSH key not found" or a formatted API error. The same applies togetSshKeys(),addSshKey()(non-409 errors) anddeleteSshKey().legacy/src/Service/Api.php:959— The pagination loop ingetSshKeys()has no page limit and no guard against a repeated URL: it follows_links.next.hrefunconditionally, so if the API returns anextlink on the last page that resolves to the URL just requested (a common pattern for APIs that always emitnext), the loop issues the same GET forever while appending duplicate items to$items, hanging the command and growing memory without bound.legacy/src/Model/SshKey.php:22—SshKey::$activeis parsed from the API but never read anywhere:Api::getSshKeys()returns inactive keys,SshKeyListCommandhas noactivecolumn, andService\SshKey::listAccountKeyFingerprints()feeds inactive keys' fingerprints intofindIdentityMatchingPublicKeys(). A user whose only account key is inactive gets it auto-selected as the SSH IdentityFile byselectIdentity()and is told byWelcomeCommand/SshDiagnosticsthat a local key matching the account exists, while SSH authentication fails with no explanation.
Review 1 of 10 for this pull request · View the full run
No description provided.