Skip to content
Open
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
5 changes: 0 additions & 5 deletions legacy/phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -230,11 +230,6 @@ parameters:
count: 1
path: src/Command/Server/ServerStartCommand.php

-
message: "#^Parameter \\#1 \\$items of method Platformsh\\\\Cli\\\\Service\\\\QuestionHelper\\:\\:choose\\(\\) expects array\\<string, string\\>, array\\<int\\|string, string\\> given\\.$#"
count: 1
path: src/Command/SshKey/SshKeyDeleteCommand.php

-
message: "#^Call to an undefined method GuzzleHttp\\\\ClientInterface\\:\\:post\\(\\)\\.$#"
count: 1
Expand Down
18 changes: 14 additions & 4 deletions legacy/src/Command/SshKey/SshKeyAddCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Platformsh\Cli\Command\SshKey;

use GuzzleHttp\Exception\BadResponseException;
use Platformsh\Cli\Service\Io;
use Platformsh\Cli\Service\Api;
use Platformsh\Cli\Service\Config;
Expand Down Expand Up @@ -128,7 +129,16 @@ protected function execute(InputInterface $input, OutputInterface $output): int
}

// Add the new key.
$this->api->getClient()->addSshKey($publicKey, $input->getOption('name'));
try {
$this->api->addSshKey($publicKey, $input->getOption('name'));
} catch (BadResponseException $e) {
// The API rejects a key that is already registered, by anyone.
if ($e->getResponse()->getStatusCode() === 409) {
$this->stdErr->writeln('<error>This SSH key is already registered.</error>');
return 1;
}
throw $e;
}

$this->stdErr->writeln(\sprintf(
'The SSH key <info>%s</info> has been successfully added to your %s account.',
Expand All @@ -152,14 +162,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int
/**
* Check whether the SSH key already exists in the user's account.
*
* @param string $fingerprint The public key fingerprint (as an MD5 hash).
* @param string $fingerprint The public key fingerprint (as a SHA-256 hash).
*
* @return bool
*/
protected function keyExistsByFingerprint(string $fingerprint): bool
{
foreach ($this->api->getClient()->getSshKeys() as $existingKey) {
if ($existingKey->fingerprint === $fingerprint) {
foreach ($this->api->getSshKeys() as $existingKey) {
if ($existingKey->sha256 === $fingerprint) {
return true;
}
}
Expand Down
11 changes: 5 additions & 6 deletions legacy/src/Command/SshKey/SshKeyDeleteCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ protected function configure(): void
InputArgument::OPTIONAL,
'The ID of the SSH key to delete',
);
$this->addExample('Delete the key 123', '123');
$this->addExample('Delete the key with the given ID', '01JX7Q8YV0N4W2S6TRK3M9BAEC');
$help = 'This command lets you delete SSH keys from your account.'
. "\n\n" . $this->certificateNotice($this->config);
$this->setHelp($help);
Expand All @@ -45,11 +45,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int
}
$options = [];
foreach ($keys as $key) {
$options[(string) $key->key_id] = sprintf('%s (%s)', $key->key_id, $key->title ?: $key->fingerprint);
$options[$key->id] = sprintf('%s (%s)', $key->id, $key->label ?: $key->sha256);
}
$id = $this->questionHelper->choose($options, 'Enter a number to choose a key to delete:', null, false);
}
if (empty($id) || !is_numeric($id)) {
if (empty($id)) {
$this->stdErr->writeln('<error>You must specify the ID of the SSH key to delete.</error>');
$this->stdErr->writeln('');
$this->stdErr->writeln(
Expand All @@ -59,15 +59,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int
return 1;
}

$key = $this->api->getClient()
->getSshKey((string) $id);
$key = $this->api->getSshKey((string) $id);
if (!$key) {
$this->stdErr->writeln("SSH key not found: <error>$id</error>");

return 1;
}

$key->delete();
$this->api->deleteSshKey($key->id);

$this->stdErr->writeln(sprintf(
'The SSH key <info>%s</info> has been deleted from your %s account.',
Expand Down
10 changes: 5 additions & 5 deletions legacy/src/Command/SshKey/SshKeyListCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ class SshKeyListCommand extends SshKeyCommandBase
/** @var array<string, string> */
private array $tableHeader = [
'id' => 'ID',
'title' => 'Title',
'fingerprint' => 'Fingerprint',
'label' => 'Label',
'sha256' => 'Fingerprint (SHA-256)',
'path' => 'Local path',
];
/** @var string[] */
private array $defaultColumns = ['id', 'title', 'path'];
private array $defaultColumns = ['id', 'label', 'path'];
public function __construct(private readonly Api $api, private readonly Config $config, private readonly SshKey $sshKey, private readonly Table $table)
{
parent::__construct();
Expand Down Expand Up @@ -52,8 +52,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$sshKeyService = $this->sshKey;
$rows = [];
foreach ($keys as $key) {
$row = ['id' => (string) $key->key_id, 'title' => $key->title, 'fingerprint' => $key->fingerprint];
$identity = $sshKeyService->findIdentityMatchingPublicKeys([$key->fingerprint]);
$row = ['id' => $key->id, 'label' => $key->label, 'sha256' => $key->sha256];
$identity = $sshKeyService->findIdentityMatchingPublicKeys([$key->sha256]);
$path = $identity ? $identity . '.pub' : '';
if (!$identity && !$table->formatIsMachineReadable()) {
$path = '<comment>Not found</comment>';
Expand Down
2 changes: 1 addition & 1 deletion legacy/src/Event/LoginRequiredEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public function getLoginOptionsCmdLine(): string
$args = [];
foreach ($this->getLoginOptions() as $option => $value) {
$args[] = $option;
$args[] = OsUtil::escapeShellArg(is_array($value) ? implode(',', $value) : $value);
$args[] = OsUtil::escapeShellArg(is_array($value) ? implode(',', $value) : (string) $value);
}
return implode(' ', $args);
}
Expand Down
44 changes: 44 additions & 0 deletions legacy/src/Model/SshKey.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

declare(strict_types=1);

namespace Platformsh\Cli\Model;

/**
* An SSH public key belonging to a user.
*
* This models the Auth API's representation, which replaced the older
* Accounts one. Notable differences: the ID is an opaque string (a ULID)
* rather than an integer, the label was called "title", and the fingerprint
* is a SHA-256 hash in OpenSSH format rather than an MD5 hash.
*/
readonly class SshKey
{
public function __construct(
public string $id,
public string $sha256,
public string $value,
public string $label,
public bool $active,
public string $userId,
public string $createdAt,
public string $updatedAt,
) {}

/**
* @param array<string, mixed> $data
*/
public static function fromData(array $data): self
{
return new self(
(string) ($data['id'] ?? ''),
(string) ($data['sha256'] ?? ''),
(string) ($data['value'] ?? ''),
(string) ($data['label'] ?? ''),
(bool) ($data['active'] ?? true),
(string) ($data['user_id'] ?? ''),
(string) ($data['created_at'] ?? ''),
(string) ($data['updated_at'] ?? ''),
);
}
}
124 changes: 101 additions & 23 deletions legacy/src/Service/Api.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\BadResponseException;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Uri;
use GuzzleHttp\Psr7\UriResolver;
use GuzzleHttp\Utils;
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
use League\OAuth2\Client\Token\AccessToken;
Expand All @@ -44,7 +46,7 @@
use Platformsh\Client\Model\Project;
use Platformsh\Client\Model\Ref\UserRef;
use Platformsh\Client\Model\ApiResourceBase as ApiResource;
use Platformsh\Client\Model\SshKey;
use Platformsh\Cli\Model\SshKey;
use Platformsh\Client\Model\Subscription;
use Platformsh\Client\Model\Team\TeamMember;
use Platformsh\Client\Model\Team\TeamProjectAccess;
Expand Down Expand Up @@ -911,50 +913,126 @@ public function getMyAccount(bool $reset = false): array
}

/**
* Get the current user's legacy account info, including SSH keys.
* Shortcut to return the ID of the current user.
*/
public function getMyUserId(bool $reset = false): string
{
$id = $this->getClient()->getMyUserId($reset);
if (!$id) {
throw new \RuntimeException('No user ID found for the current session.');
}
return $id;
}

/**
* Returns the base URL of the current user's SSH keys collection.
*/
private function sshKeysUrl(): string
{
return rtrim($this->config->getApiUrl(), '/') . '/users/' . rawurlencode($this->getMyUserId()) . '/ssh-keys';
}

/**
* Returns the cache key under which the current user's SSH keys are stored.
*/
private function sshKeysCacheKey(): string
{
return sprintf('%s:ssh-keys', $this->config->getSessionId());
}

/**
* Get the logged-in user's SSH keys.
*
* @param bool $reset
*
* @return array{'id': string, 'username': string, 'mail': string, 'display_name': string, 'ssh_keys': array<string, mixed>}
* @return SshKey[]
*/
private function getLegacyAccountInfo(bool $reset = false): array
public function getSshKeys(bool $reset = false): array
{
$cacheKey = sprintf('%s:my-account', $this->config->getSessionId());
$info = $this->cache->fetch($cacheKey);
if (!$reset && $info) {
$this->io->debug('Loaded account information from cache');
$cacheKey = $this->sshKeysCacheKey();
$items = $this->cache->fetch($cacheKey);
if ($reset || !is_array($items)) {
$items = [];
$url = $this->sshKeysUrl();
// The list is paginated, and the "next" link may be relative to the
// API base URL, so each one is resolved against the request URL.
while ($url !== null) {
$response = $this->getHttpClient()->request('GET', $url);
$data = (array) Utils::jsonDecode((string) $response->getBody(), true);
Comment on lines +960 to +961
foreach ($data['items'] ?? [] as $item) {
$items[] = $item;
}
$next = $data['_links']['next']['href'] ?? null;
$url = $next !== null
? (string) UriResolver::resolve(new Uri($url), new Uri((string) $next))
: null;
}
$this->cache->save($cacheKey, $items, $this->config->getInt('api.users_ttl'));
} else {
$info = $this->getClient()->getAccountInfo($reset);
$this->cache->save($cacheKey, $info, $this->config->getInt('api.users_ttl'));
$this->io->debug('Loaded SSH keys from cache');
}

return $info;
return array_map(fn(array $item): SshKey => SshKey::fromData($item), $items);
}

/**
* Shortcut to return the ID of the current user.
* Get a single SSH key belonging to the logged-in user.
*
* @param string $id The key's ID.
*
* @return SshKey|null The key, or null if it does not exist.
*/
public function getMyUserId(bool $reset = false): string
public function getSshKey(string $id): ?SshKey
{
$id = $this->getClient()->getMyUserId($reset);
if (!$id) {
throw new \RuntimeException('No user ID found for the current session.');
try {
$response = $this->getHttpClient()->request('GET', $this->sshKeysUrl() . '/' . rawurlencode($id));
} catch (BadResponseException $e) {
if ($e->getResponse()->getStatusCode() === 404) {
return null;
}
throw $e;
}
return $id;

return SshKey::fromData((array) Utils::jsonDecode((string) $response->getBody(), true));
}

/**
* Get the logged-in user's SSH keys.
* Add an SSH public key to the logged-in user's account.
*
* @param bool $reset
* @param string $value The public key, in OpenSSH format.
* @param string|null $label A human-readable label for the key.
*
* @return SshKey[]
* @return SshKey The newly created key.
*/
public function getSshKeys(bool $reset = false): array
public function addSshKey(string $value, ?string $label = null): SshKey
{
$payload = ['value' => $value];
if ($label !== null && $label !== '') {
$payload['label'] = $label;
}
$response = $this->getHttpClient()->request('POST', $this->sshKeysUrl(), ['json' => $payload]);
$this->clearSshKeysCache();

return SshKey::fromData((array) Utils::jsonDecode((string) $response->getBody(), true));
}

/**
* Delete an SSH key from the logged-in user's account.
*
* @param string $id The key's ID.
*/
public function deleteSshKey(string $id): void
{
$data = $this->getLegacyAccountInfo($reset);
$this->getHttpClient()->request('DELETE', $this->sshKeysUrl() . '/' . rawurlencode($id));
$this->clearSshKeysCache();
}

return SshKey::wrapCollection($data['ssh_keys'], rtrim($this->config->getApiUrl(), '/') . '/', $this->getHttpClient());
/**
* Clear the cached list of the logged-in user's SSH keys.
*/
public function clearSshKeysCache(): void
{
$this->cache->delete($this->sshKeysCacheKey());
}

/**
Expand Down
13 changes: 8 additions & 5 deletions legacy/src/Service/SshKey.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace Platformsh\Cli\Service;

use Platformsh\Client\Model\SshKey as SshKeyModel;
use Platformsh\Cli\Model\SshKey as SshKeyModel;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\OutputInterface;

Expand Down Expand Up @@ -109,7 +109,7 @@ private function listPublicKeys(bool $reset = false): array
}

/**
* Lists SSH key MD5 fingerprints in the user's account.
* Lists SSH key SHA-256 fingerprints in the user's account.
*
* @return string[]
*/
Expand All @@ -120,7 +120,7 @@ private function listAccountKeyFingerprints(): array
return [];
}

return \array_map(fn(SshKeyModel $sshKey) => $sshKey->fingerprint, $keys);
return \array_map(fn(SshKeyModel $sshKey) => $sshKey->sha256, $keys);
}

/**
Expand Down Expand Up @@ -158,7 +158,10 @@ public function findIdentityMatchingPublicKeys(array $fingerprints): ?string
}

/**
* Returns an MD5 hash of a public key that matches its server fingerprint.
* Returns the SHA-256 fingerprint of a public key, matching the API.
*
* The format is the one used by OpenSSH and reported by `ssh-keygen -l`:
* "SHA256:" followed by the unpadded base64 of the hash.
*
* @param string $filename An absolute path to the public key.
*
Expand All @@ -181,6 +184,6 @@ public function getPublicKeyFingerprint(string $filename): string
throw new \RuntimeException('Failed to base64-decode public key: ' . $filename);
}

return \md5($key);
return 'SHA256:' . \rtrim(\base64_encode(\hash('sha256', $key, true)), '=');
}
}
Loading