From 4f3084cc67b1fd53e4fadec0c6aa85b7a8970b0b Mon Sep 17 00:00:00 2001 From: Patrick Dawkins Date: Mon, 17 Mar 2025 20:56:23 +0000 Subject: [PATCH] Support authenticating with client credentials --- src/Selector/Selector.php | 4 ++++ src/Service/Api.php | 45 +++++++++++++++++++++++++++++++++------ src/Service/Config.php | 5 +++++ 3 files changed, 48 insertions(+), 6 deletions(-) diff --git a/src/Selector/Selector.php b/src/Selector/Selector.php index 97cd7e7de..56fbccbdf 100644 --- a/src/Selector/Selector.php +++ b/src/Selector/Selector.php @@ -951,6 +951,10 @@ public function selectOrganization(InputInterface $input, string $filterByLink = } } + if ($this->api->isUsingClientCredentials()) { + throw new \InvalidArgumentException('An organization name or ID (--org) is required when client credentials are in use.'); + } + $userId = $this->api->getMyUserId(); $organizations = $this->api->getClient()->listOrganizationsWithMember($userId); diff --git a/src/Service/Api.php b/src/Service/Api.php index 35b5e4723..33e09d5e5 100644 --- a/src/Service/Api.php +++ b/src/Service/Api.php @@ -280,7 +280,7 @@ private function getConnectorOptions(): array $connectorOptions['verify'] = $this->config->getBool('api.skip_ssl') ? false : $this->caBundlePath(); $connectorOptions['debug'] = false; - $connectorOptions['client_id'] = $this->config->get('api.oauth2_client_id'); + $connectorOptions['client_id'] = $this->config->getStr('api.oauth2_client_id'); $connectorOptions['user_agent'] = $this->config->getUserAgent(); $connectorOptions['timeout'] = $this->config->getInt('api.default_timeout'); @@ -292,6 +292,13 @@ private function getConnectorOptions(): array $connectorOptions['api_token_type'] = 'access'; } + if ($this->config->has('api.oauth2_client_secret')) { + $connectorOptions['client_secret'] = $this->config->getStr('api.oauth2_client_secret'); + } + if ($this->config->has('api.oauth2_scopes')) { + $connectorOptions['scopes'] = (array) $this->config->get('api.oauth2_scopes'); + } + $connectorOptions['proxy'] = $this->guzzleProxyConfig(); $connectorOptions['token_url'] = $this->config->get('api.oauth2_token_url'); @@ -510,11 +517,17 @@ public function getClient(bool $autoLogin = true, bool $reset = false): Platform $sessionId = $this->config->getSessionId(); - // Override the session ID if an API token is set. - // This ensures file storage from other credentials will not be - // reused. - if (!empty($options['api_token'])) { - $sessionId = 'api-token-' . \substr(\hash('sha256', (string) $options['api_token']), 0, 32); + // Override the session ID if an API token or client credentials + // are set. This ensures file storage from other credentials will + // not be reused. + if (!empty($options['api_token']) || !empty($options['client_secret'])) { + $credsKeys = []; + foreach (['api_token', 'client_id', 'client_secret', 'scopes'] as $key) { + if (array_key_exists($key, $options)) { + $credsKeys[] = is_array($options[$key]) ? implode(' ', $options[$key]) : $options[$key]; + } + } + $sessionId = 'c-' . \hash('sha256', implode(':', $credsKeys)); } // Set up a session to store OAuth2 tokens. @@ -628,6 +641,11 @@ private function matchesVendorFilter(string|array|null $filters, BasicProjectInf return empty($filters) || in_array($project->vendor, (array) $filters); } + public function isUsingClientCredentials(): bool + { + return $this->config->has('api.oauth2_client_secret') && $this->getClient()->getMyUserId() === false; + } + /** * Returns the project list for the current user. * @@ -637,6 +655,10 @@ private function matchesVendorFilter(string|array|null $filters, BasicProjectInf */ public function getMyProjects(?bool $refresh = null): array { + if ($this->isUsingClientCredentials()) { + return []; + } + $new = $this->config->getBool('api.centralized_permissions') && $this->config->getBool('api.organizations'); /** @var string[]|string|null $vendorFilter */ $vendorFilter = $this->config->getWithDefault('api.vendor_filter', null); @@ -1672,6 +1694,17 @@ public function getCodeSourceIntegration(Project $project): ?Integration */ public function showSessionInfo(bool $logout = false, bool $newline = true): void { + if ($this->isUsingClientCredentials()) { + if ($newline) { + $this->stdErr->writeln(''); + } + $this->stdErr->writeln(\sprintf( + 'Client credentials are configured (client ID: %s)', + $this->config->getStr('api.oauth2_client_id'), + )); + return; + } + $sessionId = $this->config->getSessionId(); if ($sessionId !== 'default' || count($this->listSessionIds()) > 1) { if ($newline) { diff --git a/src/Service/Config.php b/src/Service/Config.php index 93796149d..36e31c72c 100644 --- a/src/Service/Config.php +++ b/src/Service/Config.php @@ -416,6 +416,8 @@ private function applyEnvironmentOverrides(): void 'AUTH_URL' => 'api.auth_url', 'OAUTH2_AUTH_URL' => 'api.oauth2_auth_url', 'OAUTH2_CLIENT_ID' => 'api.oauth2_client_id', + 'OAUTH2_CLIENT_SECRET' => 'api.oauth2_client_secret', + 'OAUTH2_SCOPE' => 'api.oauth2_scopes', 'OAUTH2_TOKEN_URL' => 'api.oauth2_token_url', 'OAUTH2_REVOKE_URL' => 'api.oauth2_revoke_url', 'CERTIFIER_URL' => 'api.certifier_url', @@ -672,6 +674,9 @@ private function applyDynamicDefaults(): void if (!isset($this->config['api']['oauth2_client_id'])) { $this->config['api']['oauth2_client_id'] = $this->getStr('application.slug'); } + if (isset($this->config['api']['oauth2_scopes']) && is_string($this->config['api']['oauth2_scopes'])) { + $this->config['api']['oauth2_scopes'] = explode(' ', $this->config['api']['oauth2_scopes']); + } if (!isset($this->config['detection']['console_domain']) && isset($this->config['service']['console_url'])) { $consoleDomain = parse_url((string) $this->config['service']['console_url'], PHP_URL_HOST); if ($consoleDomain !== false) {