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/ExternalAccountCredentials.php +105 -57 1.2.12 → 1.4.2 View file →
@@ -14,25 +14,36 @@
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\CredentialSource\AwsNativeSource;
21 -use Dudlewebs\WPMCS\Google\Auth\CredentialSource\FileSource;
22 -use Dudlewebs\WPMCS\Google\Auth\CredentialSource\UrlSource;
23 -use Dudlewebs\WPMCS\Google\Auth\ExternalAccountCredentialSourceInterface;
24 -use Dudlewebs\WPMCS\Google\Auth\FetchAuthTokenInterface;
25 -use Dudlewebs\WPMCS\Google\Auth\GetQuotaProjectInterface;
26 -use Dudlewebs\WPMCS\Google\Auth\GetUniverseDomainInterface;
27 -use Dudlewebs\WPMCS\Google\Auth\HttpHandler\HttpClientCache;
28 -use Dudlewebs\WPMCS\Google\Auth\HttpHandler\HttpHandlerFactory;
29 -use Dudlewebs\WPMCS\Google\Auth\OAuth2;
30 -use Dudlewebs\WPMCS\Google\Auth\ProjectIdProviderInterface;
31 -use Dudlewebs\WPMCS\Google\Auth\UpdateMetadataInterface;
32 -use Dudlewebs\WPMCS\Google\Auth\UpdateMetadataTrait;
33 -use Dudlewebs\WPMCS\GuzzleHttp\Psr7\Request;
20 +use Dudlewebs\WPMCS\GCP\Google\Auth\CredentialSource\AwsNativeSource;
21 +use Dudlewebs\WPMCS\GCP\Google\Auth\CredentialSource\ExecutableSource;
22 +use Dudlewebs\WPMCS\GCP\Google\Auth\CredentialSource\FileSource;
23 +use Dudlewebs\WPMCS\GCP\Google\Auth\CredentialSource\UrlSource;
24 +use Dudlewebs\WPMCS\GCP\Google\Auth\ExecutableHandler\ExecutableHandler;
25 +use Dudlewebs\WPMCS\GCP\Google\Auth\ExternalAccountCredentialSourceInterface;
26 +use Dudlewebs\WPMCS\GCP\Google\Auth\FetchAuthTokenInterface;
27 +use Dudlewebs\WPMCS\GCP\Google\Auth\GetQuotaProjectInterface;
28 +use Dudlewebs\WPMCS\GCP\Google\Auth\GetUniverseDomainInterface;
29 +use Dudlewebs\WPMCS\GCP\Google\Auth\HttpHandler\HttpClientCache;
30 +use Dudlewebs\WPMCS\GCP\Google\Auth\HttpHandler\HttpHandlerFactory;
31 +use Dudlewebs\WPMCS\GCP\Google\Auth\OAuth2;
32 +use Dudlewebs\WPMCS\GCP\Google\Auth\ProjectIdProviderInterface;
33 +use Dudlewebs\WPMCS\GCP\Google\Auth\UpdateMetadataInterface;
34 +use Dudlewebs\WPMCS\GCP\Google\Auth\UpdateMetadataTrait;
35 +use Dudlewebs\WPMCS\GCP\GuzzleHttp\Psr7\Request;
34 36 use InvalidArgumentException;
37 +/**
38 + * **IMPORTANT**:
39 + * This class does not validate the credential configuration. A security
40 + * risk occurs when a credential configuration configured with malicious urls
41 + * is used.
42 + * When the credential configuration is accepted from an
43 + * untrusted source, you should validate it before creating this class.
44 + * @see https://cloud.google.com/docs/authentication/external/externally-sourced-credentials
45 + */
35 46 class ExternalAccountCredentials implements FetchAuthTokenInterface, UpdateMetadataInterface, GetQuotaProjectInterface, GetUniverseDomainInterface, ProjectIdProviderInterface
36 47 {
37 48 use UpdateMetadataTrait;
38 49 private const EXTERNAL_ACCOUNT_TYPE = 'external_account';
@@ -41,8 +52,10 @@
41 52 private ?string $quotaProject;
42 53 private ?string $serviceAccountImpersonationUrl;
43 54 private ?string $workforcePoolUserProject;
44 55 private ?string $projectId;
56 + /** @var array<mixed> */
57 + private ?array $lastImpersonatedAccessToken;
45 58 private string $universeDomain;
46 59 /**
47 60 * @param string|string[] $scope The scope of the access request, expressed either as an array
48 61 * or as a space-delimited string.
@@ -49,29 +62,27 @@
49 62 * @param array<mixed> $jsonKey JSON credentials as an associative array.
50 63 */
51 64 public function __construct($scope, array $jsonKey)
52 65 {
53 - if (!array_key_exists('type', $jsonKey)) {
66 + if (!\array_key_exists('type', $jsonKey)) {
54 67 throw new InvalidArgumentException('json key is missing the type field');
55 68 }
56 69 if ($jsonKey['type'] !== self::EXTERNAL_ACCOUNT_TYPE) {
57 - throw new InvalidArgumentException(sprintf('expected "%s" type but received "%s"', self::EXTERNAL_ACCOUNT_TYPE, $jsonKey['type']));
70 + throw new InvalidArgumentException(\sprintf('expected "%s" type but received "%s"', self::EXTERNAL_ACCOUNT_TYPE, $jsonKey['type']));
58 71 }
59 - if (!array_key_exists('token_url', $jsonKey)) {
72 + if (!\array_key_exists('token_url', $jsonKey)) {
60 73 throw new InvalidArgumentException('json key is missing the token_url field');
61 74 }
62 - if (!array_key_exists('audience', $jsonKey)) {
75 + if (!\array_key_exists('audience', $jsonKey)) {
63 76 throw new InvalidArgumentException('json key is missing the audience field');
64 77 }
65 - if (!array_key_exists('subject_token_type', $jsonKey)) {
78 + if (!\array_key_exists('subject_token_type', $jsonKey)) {
66 79 throw new InvalidArgumentException('json key is missing the subject_token_type field');
67 80 }
68 - if (!array_key_exists('credential_source', $jsonKey)) {
81 + if (!\array_key_exists('credential_source', $jsonKey)) {
69 82 throw new InvalidArgumentException('json key is missing the credential_source field');
70 83 }
71 - if (array_key_exists('service_account_impersonation_url', $jsonKey)) {
72 - $this->serviceAccountImpersonationUrl = $jsonKey['service_account_impersonation_url'];
73 - }
84 + $this->serviceAccountImpersonationUrl = $jsonKey['service_account_impersonation_url'] ?? null;
74 85 $this->quotaProject = $jsonKey['quota_project_id'] ?? null;
75 86 $this->workforcePoolUserProject = $jsonKey['workforce_pool_user_project'] ?? null;
76 87 $this->universeDomain = $jsonKey['universe_domain'] ?? GetUniverseDomainInterface::DEFAULT_UNIVERSE_DOMAIN;
77 88 $this->auth = new OAuth2(['tokenCredentialUri' => $jsonKey['token_url'], 'audience' => $jsonKey['audience'], 'scope' => $scope, 'subjectTokenType' => $jsonKey['subject_token_type'], 'subjectTokenFetcher' => self::buildCredentialSource($jsonKey), 'additionalOptions' => $this->workforcePoolUserProject ? ['userProject' => $this->workforcePoolUserProject] : []]);
@@ -81,24 +92,21 @@
81 92 }
82 93 /**
83 94 * @param array<mixed> $jsonKey
84 95 */
85 - private static function buildCredentialSource(array $jsonKey): ExternalAccountCredentialSourceInterface
96 + private static function buildCredentialSource(array $jsonKey) : ExternalAccountCredentialSourceInterface
86 97 {
87 98 $credentialSource = $jsonKey['credential_source'];
88 99 if (isset($credentialSource['file'])) {
89 100 return new FileSource($credentialSource['file'], $credentialSource['format']['type'] ?? null, $credentialSource['format']['subject_token_field_name'] ?? null);
90 101 }
91 - if (isset($credentialSource['environment_id']) && 1 === preg_match('/^aws(\d+)$/', $credentialSource['environment_id'], $matches)) {
102 + if (isset($credentialSource['environment_id']) && 1 === \preg_match('/^aws(\\d+)$/', $credentialSource['environment_id'], $matches)) {
92 103 if ($matches[1] !== '1') {
93 104 throw new InvalidArgumentException("aws version \"{$matches[1]}\" is not supported in the current build.");
94 105 }
95 - if (!array_key_exists('regional_cred_verification_url', $credentialSource)) {
106 + if (!\array_key_exists('regional_cred_verification_url', $credentialSource)) {
96 107 throw new InvalidArgumentException('The regional_cred_verification_url field is required for aws1 credential source.');
97 108 }
98 - if (!array_key_exists('audience', $jsonKey)) {
99 - throw new InvalidArgumentException('aws1 credential source requires an audience to be set in the JSON file.');
100 - }
101 109 return new AwsNativeSource(
102 110 $jsonKey['audience'],
103 111 $credentialSource['regional_cred_verification_url'],
104 112 // $regionalCredVerificationUrl
@@ -111,13 +119,38 @@
111 119 }
112 120 if (isset($credentialSource['url'])) {
113 121 return new UrlSource($credentialSource['url'], $credentialSource['format']['type'] ?? null, $credentialSource['format']['subject_token_field_name'] ?? null, $credentialSource['headers'] ?? null);
114 122 }
123 + if (isset($credentialSource['executable'])) {
124 + if (!\array_key_exists('command', $credentialSource['executable'])) {
125 + throw new InvalidArgumentException('executable source requires a command to be set in the JSON file.');
126 + }
127 + // Build command environment variables
128 + $env = [
129 + 'GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE' => $jsonKey['audience'],
130 + 'GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE' => $jsonKey['subject_token_type'],
131 + // Always set to 0 because interactive mode is not supported.
132 + 'GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE' => '0',
133 + ];
134 + if ($outputFile = $credentialSource['executable']['output_file'] ?? null) {
135 + $env['GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE'] = $outputFile;
136 + }
137 + if ($serviceAccountImpersonationUrl = $jsonKey['service_account_impersonation_url'] ?? null) {
138 + // Parse email from URL. The formal looks as follows:
139 + // https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/[email protected]:generateAccessToken
140 + $regex = '/serviceAccounts\\/(?<email>[^:]+):generateAccessToken$/';
141 + if (\preg_match($regex, $serviceAccountImpersonationUrl, $matches)) {
142 + $env['GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL'] = $matches['email'];
143 + }
144 + }
145 + $timeoutMs = $credentialSource['executable']['timeout_millis'] ?? null;
146 + return new ExecutableSource($credentialSource['executable']['command'], $outputFile, $timeoutMs ? new ExecutableHandler($env, $timeoutMs) : new ExecutableHandler($env));
147 + }
115 148 throw new InvalidArgumentException('Unable to determine credential source from json key.');
116 149 }
117 150 /**
118 151 * @param string $stsToken
119 - * @param callable $httpHandler
152 + * @param callable|null $httpHandler
120 153 *
121 154 * @return array<mixed> {
122 155 * A set of auth related metadata, containing the following
123 156 *
@@ -124,23 +157,25 @@
124 157 * @type string $access_token
125 158 * @type int $expires_at
126 159 * }
127 160 */
128 - private function getImpersonatedAccessToken(string $stsToken, callable $httpHandler = null): array
161 + private function getImpersonatedAccessToken(string $stsToken, ?callable $httpHandler = null) : array
129 162 {
130 163 if (!isset($this->serviceAccountImpersonationUrl)) {
131 164 throw new InvalidArgumentException('service_account_impersonation_url must be set in JSON credentials.');
132 165 }
133 - $request = new Request('POST', $this->serviceAccountImpersonationUrl, ['Content-Type' => 'application/json', 'Authorization' => 'Bearer ' . $stsToken], (string) json_encode(['lifetime' => sprintf('%ss', OAuth2::DEFAULT_EXPIRY_SECONDS), 'scope' => explode(' ', $this->auth->getScope())]));
134 - if (is_null($httpHandler)) {
166 + $request = new Request('POST', $this->serviceAccountImpersonationUrl, ['Content-Type' => 'application/json', 'Authorization' => 'Bearer ' . $stsToken], (string) \json_encode(['lifetime' => \sprintf('%ss', OAuth2::DEFAULT_EXPIRY_SECONDS), 'scope' => \explode(' ', $this->auth->getScope())]));
167 + if (\is_null($httpHandler)) {
135 168 $httpHandler = HttpHandlerFactory::build(HttpClientCache::getHttpClient());
136 169 }
137 170 $response = $httpHandler($request);
138 - $body = json_decode((string) $response->getBody(), \true);
139 - return ['access_token' => $body['accessToken'], 'expires_at' => strtotime($body['expireTime'])];
171 + $body = \json_decode((string) $response->getBody(), \true);
172 + return ['access_token' => $body['accessToken'], 'expires_at' => \strtotime($body['expireTime'])];
140 173 }
141 174 /**
142 - * @param callable $httpHandler
175 + * @param callable|null $httpHandler
176 + * @param array<mixed> $headers [optional] Metrics headers to be inserted
177 + * into the token endpoint request present.
143 178 *
144 179 * @return array<mixed> {
145 180 * A set of auth related metadata, containing the following
146 181 *
@@ -150,23 +185,36 @@
150 185 * @type string $issued_token_type (identity pool only)
151 186 * @type string $token_type (identity pool only)
152 187 * }
153 188 */
154 - public function fetchAuthToken(callable $httpHandler = null)
189 + public function fetchAuthToken(?callable $httpHandler = null, array $headers = [])
155 190 {
156 - $stsToken = $this->auth->fetchAuthToken($httpHandler);
191 + $stsToken = $this->auth->fetchAuthToken($httpHandler, $headers);
157 192 if (isset($this->serviceAccountImpersonationUrl)) {
158 - return $this->getImpersonatedAccessToken($stsToken['access_token'], $httpHandler);
193 + return $this->lastImpersonatedAccessToken = $this->getImpersonatedAccessToken($stsToken['access_token'], $httpHandler);
159 194 }
160 195 return $stsToken;
161 196 }
162 - public function getCacheKey()
197 + /**
198 + * Get the cache token key for the credentials.
199 + * The cache token key format depends on the type of source
200 + * The format for the cache key one of the following:
201 + * FetcherCacheKey.Scope.[ServiceAccount].[TokenType].[WorkforcePoolUserProject]
202 + * FetcherCacheKey.Audience.[ServiceAccount].[TokenType].[WorkforcePoolUserProject]
203 + *
204 + * @return ?string;
205 + */
206 + public function getCacheKey() : ?string
163 207 {
164 - return $this->auth->getCacheKey();
208 + $scopeOrAudience = $this->auth->getAudience();
209 + if (!$scopeOrAudience) {
210 + $scopeOrAudience = $this->auth->getScope();
211 + }
212 + return $this->auth->getSubjectTokenFetcher()->getCacheKey() . '.' . $scopeOrAudience . '.' . ($this->serviceAccountImpersonationUrl ?? '') . '.' . ($this->auth->getSubjectTokenType() ?? '') . '.' . ($this->workforcePoolUserProject ?? '');
165 213 }
166 214 public function getLastReceivedToken()
167 215 {
168 - return $this->auth->getLastReceivedToken();
216 + return $this->lastImpersonatedAccessToken ?? $this->auth->getLastReceivedToken();
169 217 }
170 218 /**
171 219 * Get the quota project used for this API request
172 220 *
@@ -180,9 +228,9 @@
180 228 * Get the universe domain used for this API request
181 229 *
182 230 * @return string
183 231 */
184 - public function getUniverseDomain(): string
232 + public function getUniverseDomain() : string
185 233 {
186 234 return $this->universeDomain;
187 235 }
188 236 /**
@@ -187,15 +235,15 @@
187 235 }
188 236 /**
189 237 * Get the project ID.
190 238 *
191 - * @param callable $httpHandler Callback which delivers psr7 request
192 - * @param string $accessToken The access token to use to sign the blob. If
239 + * @param callable|null $httpHandler Callback which delivers psr7 request
240 + * @param string|null $accessToken The access token to use to sign the blob. If
193 241 * provided, saves a call to the metadata server for a new access
194 242 * token. **Defaults to** `null`.
195 243 * @return string|null
196 244 */
197 - public function getProjectId(callable $httpHandler = null, string $accessToken = null)
245 + public function getProjectId(?callable $httpHandler = null, ?string $accessToken = null)
198 246 {
199 247 if (isset($this->projectId)) {
200 248 return $this->projectId;
201 249 }
@@ -202,28 +250,28 @@
202 250 $projectNumber = $this->getProjectNumber() ?: $this->workforcePoolUserProject;
203 251 if (!$projectNumber) {
204 252 return null;
205 253 }
206 - if (is_null($httpHandler)) {
254 + if (\is_null($httpHandler)) {
207 255 $httpHandler = HttpHandlerFactory::build(HttpClientCache::getHttpClient());
208 256 }
209 - $url = str_replace('UNIVERSE_DOMAIN', $this->getUniverseDomain(), sprintf(self::CLOUD_RESOURCE_MANAGER_URL, $projectNumber));
210 - if (is_null($accessToken)) {
257 + $url = \str_replace('UNIVERSE_DOMAIN', $this->getUniverseDomain(), \sprintf(self::CLOUD_RESOURCE_MANAGER_URL, $projectNumber));
258 + if (\is_null($accessToken)) {
211 259 $accessToken = $this->fetchAuthToken($httpHandler)['access_token'];
212 260 }
213 261 $request = new Request('GET', $url, ['authorization' => 'Bearer ' . $accessToken]);
214 262 $response = $httpHandler($request);
215 - $body = json_decode((string) $response->getBody(), \true);
263 + $body = \json_decode((string) $response->getBody(), \true);
216 264 return $this->projectId = $body['projectId'];
217 265 }
218 - private function getProjectNumber(): ?string
266 + private function getProjectNumber() : ?string
219 267 {
220 - $parts = explode('/', $this->auth->getAudience());
221 - $i = array_search('projects', $parts);
268 + $parts = \explode('/', $this->auth->getAudience());
269 + $i = \array_search('projects', $parts);
222 270 return $parts[$i + 1] ?? null;
223 271 }
224 - private function isWorkforcePool(): bool
272 + private function isWorkforcePool() : bool
225 273 {
226 - $regex = '#//iam\.googleapis\.com/locations/[^/]+/workforcePools/#';
227 - return preg_match($regex, $this->auth->getAudience()) === 1;
274 + $regex = '#//iam\\.googleapis\\.com/locations/[^/]+/workforcePools/#';
275 + return \preg_match($regex, $this->auth->getAudience()) === 1;
228 276 }
229 277 }