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
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
BUGSNAG_KEY=abcd1234
AMPLITUDE_KEY=acbd1234
ACLI_VERSION=1.0.0
ACLI_DEVICE_CLIENT_ID=0oa1example2client3id
ACLI_AUTH_DOMAIN=example.acquia.com
ACLI_AUTH_SERVER_ID=ausExample1234
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ jobs:
touch .env
echo BUGSNAG_KEY=${{ secrets.BUGSNAG_KEY }} >> .env
echo AMPLITUDE_KEY=${{ secrets.AMPLITUDE_KEY }} >> .env
echo ACLI_DEVICE_CLIENT_ID=${{ secrets.ACLI_DEVICE_CLIENT_ID }} >> .env
echo ACLI_AUTH_DOMAIN=${{ secrets.ACLI_AUTH_DOMAIN }} >> .env
echo ACLI_AUTH_SERVER_ID=${{ secrets.ACLI_AUTH_SERVER_ID }} >> .env
echo ACLI_VERSION=${{ steps.acli-version.outputs.ACLI_VERSION }} >> .env
echo ACLI_BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") >> .env
- name: 'Extract additional API specs from secrets'
Expand Down
6 changes: 6 additions & 0 deletions config/prod/services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ services:
Acquia\Cli\ApiCredentialsInterface:
alias: Acquia\Cli\CloudApi\CloudCredentials

Acquia\Cli\CloudApi\DeviceTokenRefresher: ~

cloud.credentials:
class: Acquia\Cli\CloudApi\CloudCredentials

Expand Down Expand Up @@ -100,8 +102,10 @@ services:
secret: '@=service("cloud.credentials").getCloudSecret()'
accessToken: '@=service("cloud.credentials").getCloudAccessToken()'
accessTokenExpiry: '@=service("cloud.credentials").getCloudAccessTokenExpiry()'
deviceAccessToken: '@=service("cloud.credentials").getCloudDeviceAccessToken()'
$baseUri: '@=service("cloud.credentials").getBaseUri()'
$accountsUri: '@=service("cloud.credentials").getAccountsUri()'
$deviceTokenRefresher: '@Acquia\Cli\CloudApi\DeviceTokenRefresher'

# v3 (MEO) connector factory — same credentials as v2, prod gateway at
# https://api.acquia.com/v3. Override with ACLI_CLOUD_API_V3_BASE_URI for dev/stage.
Expand All @@ -113,8 +117,10 @@ services:
secret: '@=service("cloud.credentials").getCloudSecret()'
accessToken: '@=service("cloud.credentials").getCloudAccessToken()'
accessTokenExpiry: '@=service("cloud.credentials").getCloudAccessTokenExpiry()'
deviceAccessToken: '@=service("cloud.credentials").getCloudDeviceAccessToken()'
$baseUri: '@=service("cloud.credentials").getV3BaseUri()'
$accountsUri: '@=service("cloud.credentials").getAccountsUri()'
$deviceTokenRefresher: '@Acquia\Cli\CloudApi\DeviceTokenRefresher'

Acquia\Cli\CloudApi\V3ClientService:
arguments:
Expand Down
5 changes: 5 additions & 0 deletions src/AcsfApi/AcsfCredentials.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,9 @@ public function getBaseUri(): ?string

return null;
}

public function getCloudDeviceAccessToken(): ?string
{
return null;
}
}
2 changes: 2 additions & 0 deletions src/ApiCredentialsInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@ public function getBaseUri(): ?string;
public function getCloudKey(): ?string;

public function getCloudSecret(): ?string;

public function getCloudDeviceAccessToken(): ?string;
}
16 changes: 14 additions & 2 deletions src/CloudApi/AccessTokenConnector.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,31 @@ class AccessTokenConnector extends Connector
/**
* @param array<string> $config
*/
public function __construct(array $config, ?string $baseUri = null, ?string $urlAccessToken = null)
{
public function __construct(
array $config,
?string $baseUri = null,
?string $urlAccessToken = null,
private ?DeviceTokenRefresher $deviceTokenRefresher = null,
) {
$this->accessToken = new AccessToken(['access_token' => $config['access_token']]);
parent::__construct($config, $baseUri, $urlAccessToken);
}

public function createRequest(string $verb, string $path): RequestInterface
{
if ($file = getenv('ACLI_ACCESS_TOKEN_FILE')) {
// File token takes priority — skip device token refresh to avoid
// burning refresh token rotations for a token nothing uses.
if (!file_exists($file)) {
throw new AcquiaCliException('Access token file not found at {file}', ['file' => $file]);
}
$this->accessToken = new AccessToken(['access_token' => trim(file_get_contents($file), "\"\n")]);
} elseif ($this->deviceTokenRefresher !== null) {
$freshToken = $this->deviceTokenRefresher->getValidAccessToken();
if ($freshToken === null) {
throw new AcquiaCliException('Device token session expired. Run <options=bold>acli auth:login</> to re-authenticate.');
}
$this->accessToken = new AccessToken(['access_token' => $freshToken]);
}
return $this->provider->getAuthenticatedRequest(
$verb,
Expand Down
34 changes: 34 additions & 0 deletions src/CloudApi/AuthConfig.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

namespace Acquia\Cli\CloudApi;

/**
* Authorization-server configuration for the device code flow ,
* shared by login and token refresh.
*/
class AuthConfig
{
public function authServerId(): string
{
return getenv('ACLI_AUTH_SERVER_ID') ?: '';
}

public function clientId(): string
{
return getenv('ACLI_DEVICE_CLIENT_ID') ?: '';
}

public function domain(): string
{
return getenv('ACLI_AUTH_DOMAIN') ?: '';
}

public function isConfigured(): bool
{
return $this->clientId() !== ''
&& $this->domain() !== ''
&& $this->authServerId() !== '';
}
}
13 changes: 11 additions & 2 deletions src/CloudApi/ClientService.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
use Acquia\Cli\ApiCredentialsInterface;
use Acquia\Cli\Application;
use Acquia\Cli\ConnectorFactoryInterface;
use Acquia\Cli\Helpers\TelemetryHelper;
use AcquiaCloudApi\Connector\Client;
use AcquiaCloudApi\Connector\ConnectorInterface;
use loophp\phposinfo\OsInfo;

/**
* Factory producing Acquia Cloud Api clients.
Expand Down Expand Up @@ -54,7 +56,13 @@ public function getClient(): Client

protected function configureClient(Client $client): void
{
$userAgent = sprintf("acli/%s", $this->application->getVersion());
$provider = TelemetryHelper::getEnvironmentProvider();
$userAgent = sprintf(
'Acquia CLI (%s, %s%s)',
$this->application->getVersion(),
OsInfo::uuid(),
$provider !== null ? ', ' . $provider : '',
);
$customHeaders = [
'User-Agent' => [$userAgent],
];
Expand All @@ -77,7 +85,8 @@ protected function checkAuthentication(): bool
{
return (
$this->credentials->getCloudAccessToken() ||
($this->credentials->getCloudKey() && $this->credentials->getCloudSecret())
($this->credentials->getCloudKey() && $this->credentials->getCloudSecret()) ||
($this->credentials->getCloudDeviceAccessToken() !== null)
);
}
}
12 changes: 8 additions & 4 deletions src/CloudApi/CloudCredentials.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,15 @@

class CloudCredentials implements ApiCredentialsInterface
{
/**
* CloudCredentials constructor.
*/
public function __construct(private CloudDataStore $datastoreCloud)
public function __construct(
private CloudDataStore $datastoreCloud,
) {
}

public function getCloudDeviceAccessToken(): ?string
{
$stored = $this->datastoreCloud->get('device_token');
return $stored['access_token'] ?? null;
}

public function getCloudAccessToken(): ?string
Expand Down
17 changes: 15 additions & 2 deletions src/CloudApi/ConnectorFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,12 @@ class ConnectorFactory implements ConnectorFactoryInterface
/**
* @param array<string> $config
*/
public function __construct(protected array $config, protected ?string $baseUri = null, protected ?string $accountsUri = null)
{
public function __construct(
protected array $config,
protected ?string $baseUri = null,
protected ?string $accountsUri = null,
private ?DeviceTokenRefresher $deviceTokenRefresher = null,
) {
}

public function createConnector(): ConnectorInterface
Expand All @@ -40,6 +44,15 @@ private function buildConnector(): ConnectorInterface
return new Connector($this->config, $this->baseUri, $this->accountsUri);
}

// Device code token path.
if (!empty($this->config['deviceAccessToken'])) {
return new AccessTokenConnector([
'access_token' => $this->config['deviceAccessToken'],
'key' => null,
'secret' => null,
], $this->baseUri, $this->accountsUri, $this->deviceTokenRefresher);
Comment thread
bansodejoyce marked this conversation as resolved.
}

// Fall back to a valid access token.
if ($this->config['accessToken']) {
$accessToken = $this->createAccessToken();
Expand Down
Loading
Loading