PluginProbe
Media Cloud Sync / 1.4.2
Media Cloud Sync v1.4.2
1.4.2 1.4.1 1.4.0 1.3.12 1.3.11 1.3.10 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.2.0 1.2.10 1.2.11 1.2.12 1.2.13 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 All 36 releases
← All changes | includes/sdk/google/google/auth/src/Credentials/ImpersonatedServiceAccountCredentials.php +142 -35 1.0.0 → 1.4.2 View file →
@@ -14,52 +14,119 @@
14 14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 15 * See the License for the specific language governing permissions and
16 16 * limitations under the License.
17 17 */
18 -namespace Dudlewebs\WPMCS\Google\Auth\Credentials;
18 +namespace Dudlewebs\WPMCS\GCP\Google\Auth\Credentials;
19 19
20 -use Dudlewebs\WPMCS\Google\Auth\CredentialsLoader;
21 -use Dudlewebs\WPMCS\Google\Auth\IamSignerTrait;
22 -use Dudlewebs\WPMCS\Google\Auth\SignBlobInterface;
23 -class ImpersonatedServiceAccountCredentials extends CredentialsLoader implements SignBlobInterface
20 +use Dudlewebs\WPMCS\GCP\Google\Auth\CacheTrait;
21 +use Dudlewebs\WPMCS\GCP\Google\Auth\CredentialsLoader;
22 +use Dudlewebs\WPMCS\GCP\Google\Auth\FetchAuthTokenInterface;
23 +use Dudlewebs\WPMCS\GCP\Google\Auth\GetUniverseDomainInterface;
24 +use Dudlewebs\WPMCS\GCP\Google\Auth\HttpHandler\HttpClientCache;
25 +use Dudlewebs\WPMCS\GCP\Google\Auth\HttpHandler\HttpHandlerFactory;
26 +use Dudlewebs\WPMCS\GCP\Google\Auth\IamSignerTrait;
27 +use Dudlewebs\WPMCS\GCP\Google\Auth\SignBlobInterface;
28 +use Dudlewebs\WPMCS\GCP\GuzzleHttp\Psr7\Request;
29 +use InvalidArgumentException;
30 +use LogicException;
31 +/**
32 + * **IMPORTANT**:
33 + * This class does not validate the credential configuration. A security
34 + * risk occurs when a credential configuration configured with malicious urls
35 + * is used.
36 + * When the credential configuration is accepted from an
37 + * untrusted source, you should validate it before creating this class.
38 + * @see https://cloud.google.com/docs/authentication/external/externally-sourced-credentials
39 + */
40 +class ImpersonatedServiceAccountCredentials extends CredentialsLoader implements SignBlobInterface, GetUniverseDomainInterface
24 41 {
42 + use CacheTrait;
25 43 use IamSignerTrait;
44 + private const CRED_TYPE = 'imp';
45 + private const IAM_SCOPE = 'https://www.googleapis.com/auth/iam';
46 + private const ID_TOKEN_IMPERSONATION_URL = 'https://iamcredentials.UNIVERSE_DOMAIN/v1/projects/-/serviceAccounts/%s:generateIdToken';
26 47 /**
27 48 * @var string
28 49 */
29 50 protected $impersonatedServiceAccountName;
51 + protected FetchAuthTokenInterface $sourceCredentials;
52 + private string $serviceAccountImpersonationUrl;
30 53 /**
31 - * @var UserRefreshCredentials
54 + * @var string[]
32 55 */
33 - protected $sourceCredentials;
56 + private array $delegates;
34 57 /**
58 + * @var string|string[]
59 + */
60 + private string|array $targetScope;
61 + private int $lifetime;
62 + /**
63 + * @var array<mixed>|null
64 + */
65 + protected array|null $lastReceivedToken = null;
66 + /**
35 67 * Instantiate an instance of ImpersonatedServiceAccountCredentials from a credentials file that
36 - * has be created with the --impersonated-service-account flag.
68 + * has be created with the --impersonate-service-account flag.
37 69 *
38 - * @param string|string[] $scope The scope of the access request, expressed either as an
39 - * array or as a space-delimited string.
40 - * @param string|array<mixed> $jsonKey JSON credential file path or JSON credentials
41 - * as an associative array.
70 + * @param string|string[]|null $scope The scope of the access request, expressed either as an
71 + * array or as a space-delimited string.
72 + * @param string|array<mixed> $jsonKey JSON credential file path or JSON array credentials {
73 + * JSON credentials as an associative array.
74 + *
75 + * @type string $service_account_impersonation_url The URL to the service account
76 + * @type string|FetchAuthTokenInterface $source_credentials The source credentials to impersonate
77 + * @type int $lifetime The lifetime of the impersonated credentials
78 + * @type string[] $delegates The delegates to impersonate
79 + * }
80 + * @param string|null $targetAudience The audience to request an ID token.
81 + * @param string|string[]|null $defaultScope The scopes to be used if no "scopes" field exists
82 + * in the `$jsonKey`.
42 83 */
43 - public function __construct($scope, $jsonKey)
84 + public function __construct(string|array|null $scope, string|array $jsonKey, private ?string $targetAudience = null, string|array|null $defaultScope = null)
44 85 {
45 - if (is_string($jsonKey)) {
46 - if (!file_exists($jsonKey)) {
47 - throw new \InvalidArgumentException('file does not exist');
86 + if (\is_string($jsonKey)) {
87 + if (!\file_exists($jsonKey)) {
88 + throw new InvalidArgumentException('file does not exist');
48 89 }
49 - $json = file_get_contents($jsonKey);
50 - if (!$jsonKey = json_decode((string) $json, \true)) {
51 - throw new \LogicException('invalid json for auth config');
90 + $json = \file_get_contents($jsonKey);
91 + if (!($jsonKey = \json_decode((string) $json, \true))) {
92 + throw new LogicException('invalid json for auth config');
52 93 }
53 94 }
54 - if (!array_key_exists('service_account_impersonation_url', $jsonKey)) {
55 - throw new \LogicException('json key is missing the service_account_impersonation_url field');
95 + if (!\array_key_exists('service_account_impersonation_url', $jsonKey)) {
96 + throw new LogicException('json key is missing the service_account_impersonation_url field');
56 97 }
57 - if (!array_key_exists('source_credentials', $jsonKey)) {
58 - throw new \LogicException('json key is missing the source_credentials field');
98 + if (!\array_key_exists('source_credentials', $jsonKey)) {
99 + throw new LogicException('json key is missing the source_credentials field');
59 100 }
60 - $this->impersonatedServiceAccountName = $this->getImpersonatedServiceAccountNameFromUrl($jsonKey['service_account_impersonation_url']);
61 - $this->sourceCredentials = new UserRefreshCredentials($scope, $jsonKey['source_credentials']);
101 + $jsonKeyScope = $jsonKey['scopes'] ?? null;
102 + $scope = ($scope ?: $jsonKeyScope) ?: $defaultScope;
103 + if ($scope && $targetAudience) {
104 + throw new InvalidArgumentException('Scope and targetAudience cannot both be supplied');
105 + }
106 + if (\is_array($jsonKey['source_credentials'])) {
107 + if (!\array_key_exists('type', $jsonKey['source_credentials'])) {
108 + throw new InvalidArgumentException('json key source credentials are missing the type field');
109 + }
110 + if ($targetAudience !== null && $jsonKey['source_credentials']['type'] === 'service_account') {
111 + // Service account tokens MUST request a scope, and as this token is only used to impersonate
112 + // an ID token, the narrowest scope we can request is `iam`.
113 + $scope = self::IAM_SCOPE;
114 + }
115 + $jsonKey['source_credentials'] = match ($jsonKey['source_credentials']['type'] ?? null) {
116 + // Do not pass $defaultScope to ServiceAccountCredentials
117 + 'service_account' => new ServiceAccountCredentials($scope, $jsonKey['source_credentials']),
118 + 'authorized_user' => new UserRefreshCredentials($scope, $jsonKey['source_credentials']),
119 + 'external_account' => new ExternalAccountCredentials($scope, $jsonKey['source_credentials']),
120 + default => throw new \InvalidArgumentException('invalid value in the type field'),
121 + };
122 + }
123 + $this->targetScope = $scope ?? [];
124 + $this->lifetime = $jsonKey['lifetime'] ?? 3600;
125 + $this->delegates = $jsonKey['delegates'] ?? [];
126 + $this->serviceAccountImpersonationUrl = $jsonKey['service_account_impersonation_url'];
127 + $this->impersonatedServiceAccountName = $this->getImpersonatedServiceAccountNameFromUrl($this->serviceAccountImpersonationUrl);
128 + $this->sourceCredentials = $jsonKey['source_credentials'];
62 129 }
63 130 /**
64 131 * Helper function for extracting the Server Account Name from the URL saved in the account
65 132 * credentials file.
@@ -66,13 +133,13 @@
66 133 *
67 134 * @param $serviceAccountImpersonationUrl string URL from "service_account_impersonation_url"
68 135 * @return string Service account email or ID.
69 136 */
70 - private function getImpersonatedServiceAccountNameFromUrl(string $serviceAccountImpersonationUrl): string
137 + private function getImpersonatedServiceAccountNameFromUrl(string $serviceAccountImpersonationUrl) : string
71 138 {
72 - $fields = explode('/', $serviceAccountImpersonationUrl);
73 - $lastField = end($fields);
74 - $splitter = explode(':', $lastField);
139 + $fields = \explode('/', $serviceAccountImpersonationUrl);
140 + $lastField = \end($fields);
141 + $splitter = \explode(':', $lastField);
75 142 return $splitter[0];
76 143 }
77 144 /**
78 145 * Get the client name from the keyfile
@@ -81,14 +148,14 @@
81 148 *
82 149 * @param callable|null $unusedHttpHandler not used by this credentials type.
83 150 * @return string Token issuer email
84 151 */
85 - public function getClientName(callable $unusedHttpHandler = null)
152 + public function getClientName(?callable $unusedHttpHandler = null)
86 153 {
87 154 return $this->impersonatedServiceAccountName;
88 155 }
89 156 /**
90 - * @param callable $httpHandler
157 + * @param callable|null $httpHandler
91 158 *
92 159 * @return array<mixed> {
93 160 * A set of auth related metadata, containing the following
94 161 *
@@ -98,18 +165,46 @@
98 165 * @type string $token_type
99 166 * @type string $id_token
100 167 * }
101 168 */
102 - public function fetchAuthToken(callable $httpHandler = null)
169 + public function fetchAuthToken(?callable $httpHandler = null)
103 170 {
104 - return $this->sourceCredentials->fetchAuthToken($httpHandler);
171 + $httpHandler = $httpHandler ?? HttpHandlerFactory::build(HttpClientCache::getHttpClient());
172 + // The FetchAuthTokenInterface technically does not have a "headers" argument, but all of
173 + // the implementations do. Additionally, passing in more parameters than the function has
174 + // defined is allowed in PHP. So we'll just ignore the phpstan error here.
175 + // @phpstan-ignore-next-line
176 + $authToken = $this->sourceCredentials->fetchAuthToken($httpHandler, $this->applyTokenEndpointMetrics([], 'at'));
177 + $headers = $this->applyTokenEndpointMetrics(['Content-Type' => 'application/json', 'Cache-Control' => 'no-store', 'Authorization' => \sprintf('Bearer %s', $authToken['access_token'] ?? $authToken['id_token'])], $this->isIdTokenRequest() ? 'it' : 'at');
178 + $body = match ($this->isIdTokenRequest()) {
179 + \true => ['audience' => $this->targetAudience, 'includeEmail' => \true],
180 + \false => ['scope' => $this->targetScope, 'delegates' => $this->delegates, 'lifetime' => \sprintf('%ss', $this->lifetime)],
181 + };
182 + $url = $this->serviceAccountImpersonationUrl;
183 + if ($this->isIdTokenRequest()) {
184 + $regex = '/serviceAccounts\\/(?<email>[^:]+):generateAccessToken$/';
185 + if (!\preg_match($regex, $url, $matches)) {
186 + throw new InvalidArgumentException('Invalid service account impersonation URL - unable to parse service account email');
187 + }
188 + $url = \str_replace('UNIVERSE_DOMAIN', $this->getUniverseDomain(), \sprintf(self::ID_TOKEN_IMPERSONATION_URL, $matches['email']));
189 + }
190 + $request = new Request('POST', $url, $headers, (string) \json_encode($body));
191 + $response = $httpHandler($request);
192 + $body = \json_decode((string) $response->getBody(), \true);
193 + return $this->lastReceivedToken = match ($this->isIdTokenRequest()) {
194 + \true => ['id_token' => $body['token']],
195 + \false => ['access_token' => $body['accessToken'], 'expires_at' => \strtotime($body['expireTime'])],
196 + };
105 197 }
106 198 /**
199 + * Returns the Cache Key for the credentials
200 + * The cache key is the same as the UserRefreshCredentials class
201 + *
107 202 * @return string
108 203 */
109 204 public function getCacheKey()
110 205 {
111 - return $this->sourceCredentials->getCacheKey();
206 + return $this->getFullCacheKey($this->serviceAccountImpersonationUrl . $this->sourceCredentials->getCacheKey());
112 207 }
113 208 /**
114 209 * @return array<mixed>
115 210 */
@@ -114,7 +209,19 @@
114 209 * @return array<mixed>
115 210 */
116 211 public function getLastReceivedToken()
117 212 {
118 - return $this->sourceCredentials->getLastReceivedToken();
213 + return $this->lastReceivedToken;
214 + }
215 + protected function getCredType() : string
216 + {
217 + return self::CRED_TYPE;
218 + }
219 + private function isIdTokenRequest() : bool
220 + {
221 + return !\is_null($this->targetAudience);
222 + }
223 + public function getUniverseDomain() : string
224 + {
225 + return $this->sourceCredentials instanceof GetUniverseDomainInterface ? $this->sourceCredentials->getUniverseDomain() : self::DEFAULT_UNIVERSE_DOMAIN;
119 226 }
120 227 }