4848from google .auth import _helpers
4949from google .auth import exceptions
5050from google .auth import external_account
51+ from google .auth .transport import _mtls_helper
5152
5253
5354class SubjectTokenSupplier (metaclass = abc .ABCMeta ):
@@ -141,6 +142,14 @@ def get_subject_token(self, context, request):
141142 )
142143
143144
145+ class _X509Supplier (SubjectTokenSupplier ):
146+ """Internal supplier for X509 workload credentials. This class is used internally and always returns an empty string as the subject token."""
147+
148+ @_helpers .copy_docstring (SubjectTokenSupplier )
149+ def get_subject_token (self , context , request ):
150+ return ""
151+
152+
144153def _parse_token_data (token_content , format_type = "text" , subject_token_field_name = None ):
145154 if format_type == "text" :
146155 token = token_content .content
@@ -247,6 +256,7 @@ def __init__(
247256 self ._subject_token_supplier = subject_token_supplier
248257 self ._credential_source_file = None
249258 self ._credential_source_url = None
259+ self ._credential_source_certificate = None
250260 else :
251261 if not isinstance (credential_source , Mapping ):
252262 self ._credential_source_executable = None
@@ -255,76 +265,70 @@ def __init__(
255265 )
256266 self ._credential_source_file = credential_source .get ("file" )
257267 self ._credential_source_url = credential_source .get ("url" )
258- self ._credential_source_headers = credential_source .get ("headers" )
259- credential_source_format = credential_source .get ("format" , {})
260- # Get credential_source format type. When not provided, this
261- # defaults to text.
262- self ._credential_source_format_type = (
263- credential_source_format .get ("type" ) or "text"
264- )
268+ self ._credential_source_certificate = credential_source .get ("certificate" )
269+
265270 # environment_id is only supported in AWS or dedicated future external
266271 # account credentials.
267272 if "environment_id" in credential_source :
268273 raise exceptions .MalformedError (
269274 "Invalid Identity Pool credential_source field 'environment_id'"
270275 )
271- if self ._credential_source_format_type not in ["text" , "json" ]:
272- raise exceptions .MalformedError (
273- "Invalid credential_source format '{}'" .format (
274- self ._credential_source_format_type
275- )
276- )
277- # For JSON types, get the required subject_token field name.
278- if self ._credential_source_format_type == "json" :
279- self ._credential_source_field_name = credential_source_format .get (
280- "subject_token_field_name"
281- )
282- if self ._credential_source_field_name is None :
283- raise exceptions .MalformedError (
284- "Missing subject_token_field_name for JSON credential_source format"
285- )
286- else :
287- self ._credential_source_field_name = None
288276
289- if self ._credential_source_file and self ._credential_source_url :
290- raise exceptions .MalformedError (
291- "Ambiguous credential_source. 'file' is mutually exclusive with 'url'."
292- )
293- if not self ._credential_source_file and not self ._credential_source_url :
294- raise exceptions .MalformedError (
295- "Missing credential_source. A 'file' or 'url' must be provided."
296- )
277+ # check that only one of file, url, or certificate are provided.
278+ self ._validate_single_source ()
279+
280+ if self ._credential_source_certificate :
281+ self ._validate_certificate_config ()
282+ else :
283+ self ._validate_file_or_url_config (credential_source )
297284
298285 if self ._credential_source_file :
299286 self ._subject_token_supplier = _FileSupplier (
300287 self ._credential_source_file ,
301288 self ._credential_source_format_type ,
302289 self ._credential_source_field_name ,
303290 )
304- else :
291+ elif self . _credential_source_url :
305292 self ._subject_token_supplier = _UrlSupplier (
306293 self ._credential_source_url ,
307294 self ._credential_source_format_type ,
308295 self ._credential_source_field_name ,
309296 self ._credential_source_headers ,
310297 )
298+ else : # self._credential_source_certificate
299+ self ._subject_token_supplier = _X509Supplier ()
311300
312301 @_helpers .copy_docstring (external_account .Credentials )
313302 def retrieve_subject_token (self , request ):
314303 return self ._subject_token_supplier .get_subject_token (
315304 self ._supplier_context , request
316305 )
317306
307+ def _get_mtls_cert_and_key_paths (self ):
308+ if self ._credential_source_certificate is None :
309+ raise exceptions .RefreshError (
310+ 'The credential is not configured to use mtls requests. The credential should include a "certificate" section in the credential source.'
311+ )
312+ else :
313+ return _mtls_helper ._get_workload_cert_and_key_paths (
314+ self ._certificate_config_location
315+ )
316+
317+ def _mtls_required (self ):
318+ return self ._credential_source_certificate is not None
319+
318320 def _create_default_metrics_options (self ):
319321 metrics_options = super (Credentials , self )._create_default_metrics_options ()
320- # Check that credential source is a dict before checking for file vs url . This check needs to be done
322+ # Check that credential source is a dict before checking for credential type . This check needs to be done
321323 # here because the external_account credential constructor needs to pass the metrics options to the
322324 # impersonated credential object before the identity_pool credentials are validated.
323325 if isinstance (self ._credential_source , Mapping ):
324326 if self ._credential_source .get ("file" ):
325327 metrics_options ["source" ] = "file"
326- else :
328+ elif self . _credential_source . get ( "url" ) :
327329 metrics_options ["source" ] = "url"
330+ else :
331+ metrics_options ["source" ] = "x509"
328332 else :
329333 metrics_options ["source" ] = "programmatic"
330334 return metrics_options
@@ -339,6 +343,67 @@ def _constructor_args(self):
339343 args .update ({"subject_token_supplier" : self ._subject_token_supplier })
340344 return args
341345
346+ def _validate_certificate_config (self ):
347+ self ._certificate_config_location = self ._credential_source_certificate .get (
348+ "certificate_config_location"
349+ )
350+ use_default = self ._credential_source_certificate .get (
351+ "use_default_certificate_config"
352+ )
353+ if self ._certificate_config_location and use_default :
354+ raise exceptions .MalformedError (
355+ "Invalid certificate configuration, certificate_config_location cannot be specified when use_default_certificate_config = true."
356+ )
357+ if not self ._certificate_config_location and not use_default :
358+ raise exceptions .MalformedError (
359+ "Invalid certificate configuration, use_default_certificate_config should be true if no certificate_config_location is provided."
360+ )
361+
362+ def _validate_file_or_url_config (self , credential_source ):
363+ self ._credential_source_headers = credential_source .get ("headers" )
364+ credential_source_format = credential_source .get ("format" , {})
365+ # Get credential_source format type. When not provided, this
366+ # defaults to text.
367+ self ._credential_source_format_type = (
368+ credential_source_format .get ("type" ) or "text"
369+ )
370+ if self ._credential_source_format_type not in ["text" , "json" ]:
371+ raise exceptions .MalformedError (
372+ "Invalid credential_source format '{}'" .format (
373+ self ._credential_source_format_type
374+ )
375+ )
376+ # For JSON types, get the required subject_token field name.
377+ if self ._credential_source_format_type == "json" :
378+ self ._credential_source_field_name = credential_source_format .get (
379+ "subject_token_field_name"
380+ )
381+ if self ._credential_source_field_name is None :
382+ raise exceptions .MalformedError (
383+ "Missing subject_token_field_name for JSON credential_source format"
384+ )
385+ else :
386+ self ._credential_source_field_name = None
387+
388+ def _validate_single_source (self ):
389+ credential_sources = [
390+ self ._credential_source_file ,
391+ self ._credential_source_url ,
392+ self ._credential_source_certificate ,
393+ ]
394+ valid_credential_sources = list (
395+ filter (lambda source : source is not None , credential_sources )
396+ )
397+
398+ if len (valid_credential_sources ) > 1 :
399+ raise exceptions .MalformedError (
400+ "Ambiguous credential_source. 'file', 'url', and 'certificate' are mutually exclusive.."
401+ )
402+ if len (valid_credential_sources ) != 1 :
403+ raise exceptions .MalformedError (
404+ "Missing credential_source. A 'file', 'url', or 'certificate' must be provided."
405+ )
406+
342407 @classmethod
343408 def from_info (cls , info , ** kwargs ):
344409 """Creates an Identity Pool Credentials instance from parsed external account info.
0 commit comments