PluginProbe ʕ •ᴥ•ʔ
Booking for Appointments and Events Calendar – Amelia / trunk
Booking for Appointments and Events Calendar – Amelia vtrunk
2.4.4 2.4.3 2.4.2 2.4.1 2.4 trunk 1.2.1 1.2.10 1.2.11 1.2.12 1.2.13 1.2.14 1.2.15 1.2.16 1.2.17 1.2.18 1.2.19 1.2.2 1.2.20 1.2.21 1.2.22 1.2.23 1.2.24 1.2.25 1.2.26 1.2.27 1.2.28 1.2.29 1.2.3 1.2.30 1.2.31 1.2.32 1.2.33 1.2.34 1.2.35 1.2.36 1.2.37 1.2.38 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 2.0 2.0.1 2.0.2 2.1 2.1.1 2.1.2 2.1.3 2.2 2.2.1 2.3
ameliabooking / vendor / google / auth / src / Credentials / ExternalAccountCredentials.php
ameliabooking / vendor / google / auth / src / Credentials Last commit date
AppIdentityCredentials.php 6 months ago ExternalAccountCredentials.php 6 months ago GCECredentials.php 6 months ago IAMCredentials.php 6 months ago ImpersonatedServiceAccountCredentials.php 6 months ago InsecureCredentials.php 6 months ago ServiceAccountCredentials.php 6 months ago ServiceAccountJwtAccessCredentials.php 6 months ago UserRefreshCredentials.php 6 months ago
ExternalAccountCredentials.php
329 lines
1 <?php
2 /*
3 * Copyright 2023 Google Inc.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18 namespace AmeliaVendor\Google\Auth\Credentials;
19
20 use AmeliaVendor\Google\Auth\CredentialSource\AwsNativeSource;
21 use AmeliaVendor\Google\Auth\CredentialSource\FileSource;
22 use AmeliaVendor\Google\Auth\CredentialSource\UrlSource;
23 use AmeliaVendor\Google\Auth\ExternalAccountCredentialSourceInterface;
24 use AmeliaVendor\Google\Auth\FetchAuthTokenInterface;
25 use AmeliaVendor\Google\Auth\GetQuotaProjectInterface;
26 use AmeliaVendor\Google\Auth\GetUniverseDomainInterface;
27 use AmeliaVendor\Google\Auth\HttpHandler\HttpClientCache;
28 use AmeliaVendor\Google\Auth\HttpHandler\HttpHandlerFactory;
29 use AmeliaVendor\Google\Auth\OAuth2;
30 use AmeliaVendor\Google\Auth\ProjectIdProviderInterface;
31 use AmeliaVendor\Google\Auth\UpdateMetadataInterface;
32 use AmeliaVendor\Google\Auth\UpdateMetadataTrait;
33 use AmeliaVendor\GuzzleHttp\Psr7\Request;
34 use InvalidArgumentException;
35
36 class ExternalAccountCredentials implements
37 FetchAuthTokenInterface,
38 UpdateMetadataInterface,
39 GetQuotaProjectInterface,
40 GetUniverseDomainInterface,
41 ProjectIdProviderInterface
42 {
43 use UpdateMetadataTrait;
44
45 private const EXTERNAL_ACCOUNT_TYPE = 'external_account';
46 private const CLOUD_RESOURCE_MANAGER_URL = 'https://cloudresourcemanager.UNIVERSE_DOMAIN/v1/projects/%s';
47
48 private OAuth2 $auth;
49 private ?string $quotaProject;
50 private ?string $serviceAccountImpersonationUrl;
51 private ?string $workforcePoolUserProject;
52 private ?string $projectId;
53 private string $universeDomain;
54
55 /**
56 * @param string|string[] $scope The scope of the access request, expressed either as an array
57 * or as a space-delimited string.
58 * @param array<mixed> $jsonKey JSON credentials as an associative array.
59 */
60 public function __construct(
61 $scope,
62 array $jsonKey
63 ) {
64 if (!array_key_exists('type', $jsonKey)) {
65 throw new InvalidArgumentException('json key is missing the type field');
66 }
67 if ($jsonKey['type'] !== self::EXTERNAL_ACCOUNT_TYPE) {
68 throw new InvalidArgumentException(sprintf(
69 'expected "%s" type but received "%s"',
70 self::EXTERNAL_ACCOUNT_TYPE,
71 $jsonKey['type']
72 ));
73 }
74
75 if (!array_key_exists('token_url', $jsonKey)) {
76 throw new InvalidArgumentException(
77 'json key is missing the token_url field'
78 );
79 }
80
81 if (!array_key_exists('audience', $jsonKey)) {
82 throw new InvalidArgumentException(
83 'json key is missing the audience field'
84 );
85 }
86
87 if (!array_key_exists('subject_token_type', $jsonKey)) {
88 throw new InvalidArgumentException(
89 'json key is missing the subject_token_type field'
90 );
91 }
92
93 if (!array_key_exists('credential_source', $jsonKey)) {
94 throw new InvalidArgumentException(
95 'json key is missing the credential_source field'
96 );
97 }
98
99 if (array_key_exists('service_account_impersonation_url', $jsonKey)) {
100 $this->serviceAccountImpersonationUrl = $jsonKey['service_account_impersonation_url'];
101 }
102
103 $this->quotaProject = $jsonKey['quota_project_id'] ?? null;
104 $this->workforcePoolUserProject = $jsonKey['workforce_pool_user_project'] ?? null;
105 $this->universeDomain = $jsonKey['universe_domain'] ?? GetUniverseDomainInterface::DEFAULT_UNIVERSE_DOMAIN;
106
107 $this->auth = new OAuth2([
108 'tokenCredentialUri' => $jsonKey['token_url'],
109 'audience' => $jsonKey['audience'],
110 'scope' => $scope,
111 'subjectTokenType' => $jsonKey['subject_token_type'],
112 'subjectTokenFetcher' => self::buildCredentialSource($jsonKey),
113 'additionalOptions' => $this->workforcePoolUserProject
114 ? ['userProject' => $this->workforcePoolUserProject]
115 : [],
116 ]);
117
118 if (!$this->isWorkforcePool() && $this->workforcePoolUserProject) {
119 throw new InvalidArgumentException(
120 'workforce_pool_user_project should not be set for non-workforce pool credentials.'
121 );
122 }
123 }
124
125 /**
126 * @param array<mixed> $jsonKey
127 */
128 private static function buildCredentialSource(array $jsonKey): ExternalAccountCredentialSourceInterface
129 {
130 $credentialSource = $jsonKey['credential_source'];
131 if (isset($credentialSource['file'])) {
132 return new FileSource(
133 $credentialSource['file'],
134 $credentialSource['format']['type'] ?? null,
135 $credentialSource['format']['subject_token_field_name'] ?? null
136 );
137 }
138
139 if (
140 isset($credentialSource['environment_id'])
141 && 1 === preg_match('/^aws(\d+)$/', $credentialSource['environment_id'], $matches)
142 ) {
143 if ($matches[1] !== '1') {
144 throw new InvalidArgumentException(
145 "aws version \"$matches[1]\" is not supported in the current build."
146 );
147 }
148 if (!array_key_exists('regional_cred_verification_url', $credentialSource)) {
149 throw new InvalidArgumentException(
150 'The regional_cred_verification_url field is required for aws1 credential source.'
151 );
152 }
153 if (!array_key_exists('audience', $jsonKey)) {
154 throw new InvalidArgumentException(
155 'aws1 credential source requires an audience to be set in the JSON file.'
156 );
157 }
158
159 return new AwsNativeSource(
160 $jsonKey['audience'],
161 $credentialSource['regional_cred_verification_url'], // $regionalCredVerificationUrl
162 $credentialSource['region_url'] ?? null, // $regionUrl
163 $credentialSource['url'] ?? null, // $securityCredentialsUrl
164 $credentialSource['imdsv2_session_token_url'] ?? null, // $imdsV2TokenUrl
165 );
166 }
167
168 if (isset($credentialSource['url'])) {
169 return new UrlSource(
170 $credentialSource['url'],
171 $credentialSource['format']['type'] ?? null,
172 $credentialSource['format']['subject_token_field_name'] ?? null,
173 $credentialSource['headers'] ?? null,
174 );
175 }
176
177 throw new InvalidArgumentException('Unable to determine credential source from json key.');
178 }
179
180 /**
181 * @param string $stsToken
182 * @param callable $httpHandler
183 *
184 * @return array<mixed> {
185 * A set of auth related metadata, containing the following
186 *
187 * @type string $access_token
188 * @type int $expires_at
189 * }
190 */
191 private function getImpersonatedAccessToken(string $stsToken, ?callable $httpHandler = null): array
192 {
193 if (!isset($this->serviceAccountImpersonationUrl)) {
194 throw new InvalidArgumentException(
195 'service_account_impersonation_url must be set in JSON credentials.'
196 );
197 }
198 $request = new Request(
199 'POST',
200 $this->serviceAccountImpersonationUrl,
201 [
202 'Content-Type' => 'application/json',
203 'Authorization' => 'Bearer ' . $stsToken,
204 ],
205 (string) json_encode([
206 'lifetime' => sprintf('%ss', OAuth2::DEFAULT_EXPIRY_SECONDS),
207 'scope' => explode(' ', $this->auth->getScope()),
208 ]),
209 );
210 if (is_null($httpHandler)) {
211 $httpHandler = HttpHandlerFactory::build(HttpClientCache::getHttpClient());
212 }
213 $response = $httpHandler($request);
214 $body = json_decode((string) $response->getBody(), true);
215 return [
216 'access_token' => $body['accessToken'],
217 'expires_at' => strtotime($body['expireTime']),
218 ];
219 }
220
221 /**
222 * @param callable $httpHandler
223 *
224 * @return array<mixed> {
225 * A set of auth related metadata, containing the following
226 *
227 * @type string $access_token
228 * @type int $expires_at (impersonated service accounts only)
229 * @type int $expires_in (identity pool only)
230 * @type string $issued_token_type (identity pool only)
231 * @type string $token_type (identity pool only)
232 * }
233 */
234 public function fetchAuthToken(?callable $httpHandler = null)
235 {
236 $stsToken = $this->auth->fetchAuthToken($httpHandler);
237
238 if (isset($this->serviceAccountImpersonationUrl)) {
239 return $this->getImpersonatedAccessToken($stsToken['access_token'], $httpHandler);
240 }
241
242 return $stsToken;
243 }
244
245 public function getCacheKey()
246 {
247 return $this->auth->getCacheKey();
248 }
249
250 public function getLastReceivedToken()
251 {
252 return $this->auth->getLastReceivedToken();
253 }
254
255 /**
256 * Get the quota project used for this API request
257 *
258 * @return string|null
259 */
260 public function getQuotaProject()
261 {
262 return $this->quotaProject;
263 }
264
265 /**
266 * Get the universe domain used for this API request
267 *
268 * @return string
269 */
270 public function getUniverseDomain(): string
271 {
272 return $this->universeDomain;
273 }
274
275 /**
276 * Get the project ID.
277 *
278 * @param callable $httpHandler Callback which delivers psr7 request
279 * @param string $accessToken The access token to use to sign the blob. If
280 * provided, saves a call to the metadata server for a new access
281 * token. **Defaults to** `null`.
282 * @return string|null
283 */
284 public function getProjectId(?callable $httpHandler = null, ?string $accessToken = null)
285 {
286 if (isset($this->projectId)) {
287 return $this->projectId;
288 }
289
290 $projectNumber = $this->getProjectNumber() ?: $this->workforcePoolUserProject;
291 if (!$projectNumber) {
292 return null;
293 }
294
295 if (is_null($httpHandler)) {
296 $httpHandler = HttpHandlerFactory::build(HttpClientCache::getHttpClient());
297 }
298
299 $url = str_replace(
300 'UNIVERSE_DOMAIN',
301 $this->getUniverseDomain(),
302 sprintf(self::CLOUD_RESOURCE_MANAGER_URL, $projectNumber)
303 );
304
305 if (is_null($accessToken)) {
306 $accessToken = $this->fetchAuthToken($httpHandler)['access_token'];
307 }
308
309 $request = new Request('GET', $url, ['authorization' => 'Bearer ' . $accessToken]);
310 $response = $httpHandler($request);
311
312 $body = json_decode((string) $response->getBody(), true);
313 return $this->projectId = $body['projectId'];
314 }
315
316 private function getProjectNumber(): ?string
317 {
318 $parts = explode('/', $this->auth->getAudience());
319 $i = array_search('projects', $parts);
320 return $parts[$i + 1] ?? null;
321 }
322
323 private function isWorkforcePool(): bool
324 {
325 $regex = '#//iam\.googleapis\.com/locations/[^/]+/workforcePools/#';
326 return preg_match($regex, $this->auth->getAudience()) === 1;
327 }
328 }
329