blob: 901fd62fb9991c5d1e4f182c3b01577fe0c2d6b7 [file] [log] [blame]
bojeil-googled4d7f382021-02-16 12:33:20 -08001# Copyright 2020 Google LLC
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Identity Pool Credentials.
16
17This module provides credentials to access Google Cloud resources from on-prem
18or non-Google Cloud platforms which support external credentials (e.g. OIDC ID
19tokens) retrieved from local file locations or local servers. This includes
20Microsoft Azure and OIDC identity providers (e.g. K8s workloads registered with
21Hub with Hub workload identity enabled).
22
23These credentials are recommended over the use of service account credentials
24in on-prem/non-Google Cloud platforms as they do not involve the management of
25long-live service account private keys.
26
27Identity Pool Credentials are initialized using external_account
28arguments which are typically loaded from an external credentials file or
29an external credentials URL. Unlike other Credentials that can be initialized
30with a list of explicit arguments, secrets or credentials, external account
31clients use the environment and hints/guidelines provided by the
32external_account JSON file to retrieve credentials and exchange them for Google
33access tokens.
34"""
35
Tres Seaver560cf1e2021-08-03 16:35:54 -040036from collections.abc import Mapping
bojeil-googled4d7f382021-02-16 12:33:20 -080037import io
38import json
39import os
40
41from google.auth import _helpers
42from google.auth import exceptions
43from google.auth import external_account
44
45
46class Credentials(external_account.Credentials):
47 """External account credentials sourced from files and URLs."""
48
49 def __init__(
50 self,
51 audience,
52 subject_token_type,
53 token_url,
54 credential_source,
55 service_account_impersonation_url=None,
56 client_id=None,
57 client_secret=None,
58 quota_project_id=None,
59 scopes=None,
60 default_scopes=None,
bojeil-google993bab22021-09-21 14:00:15 -070061 workforce_pool_user_project=None,
bojeil-googled4d7f382021-02-16 12:33:20 -080062 ):
63 """Instantiates an external account credentials object from a file/URL.
64
65 Args:
66 audience (str): The STS audience field.
67 subject_token_type (str): The subject token type.
68 token_url (str): The STS endpoint URL.
69 credential_source (Mapping): The credential source dictionary used to
70 provide instructions on how to retrieve external credential to be
71 exchanged for Google access tokens.
72
73 Example credential_source for url-sourced credential::
74
75 {
76 "url": "http://www.example.com",
77 "format": {
78 "type": "json",
79 "subject_token_field_name": "access_token",
80 },
81 "headers": {"foo": "bar"},
82 }
83
84 Example credential_source for file-sourced credential::
85
86 {
87 "file": "/path/to/token/file.txt"
88 }
89
90 service_account_impersonation_url (Optional[str]): The optional service account
91 impersonation getAccessToken URL.
92 client_id (Optional[str]): The optional client ID.
93 client_secret (Optional[str]): The optional client secret.
94 quota_project_id (Optional[str]): The optional quota project ID.
95 scopes (Optional[Sequence[str]]): Optional scopes to request during the
96 authorization grant.
97 default_scopes (Optional[Sequence[str]]): Default scopes passed by a
98 Google client library. Use 'scopes' for user-defined scopes.
bojeil-google993bab22021-09-21 14:00:15 -070099 workforce_pool_user_project (Optona[str]): The optional workforce pool user
100 project number when the credential corresponds to a workforce pool and not
101 a workload identity pool. The underlying principal must still have
102 serviceusage.services.use IAM permission to use the project for
103 billing/quota.
bojeil-googled4d7f382021-02-16 12:33:20 -0800104
105 Raises:
106 google.auth.exceptions.RefreshError: If an error is encountered during
107 access token retrieval logic.
108 ValueError: For invalid parameters.
109
110 .. note:: Typically one of the helper constructors
111 :meth:`from_file` or
112 :meth:`from_info` are used instead of calling the constructor directly.
113 """
114
115 super(Credentials, self).__init__(
116 audience=audience,
117 subject_token_type=subject_token_type,
118 token_url=token_url,
119 credential_source=credential_source,
120 service_account_impersonation_url=service_account_impersonation_url,
121 client_id=client_id,
122 client_secret=client_secret,
123 quota_project_id=quota_project_id,
124 scopes=scopes,
125 default_scopes=default_scopes,
bojeil-google993bab22021-09-21 14:00:15 -0700126 workforce_pool_user_project=workforce_pool_user_project,
bojeil-googled4d7f382021-02-16 12:33:20 -0800127 )
128 if not isinstance(credential_source, Mapping):
129 self._credential_source_file = None
130 self._credential_source_url = None
131 else:
132 self._credential_source_file = credential_source.get("file")
133 self._credential_source_url = credential_source.get("url")
134 self._credential_source_headers = credential_source.get("headers")
135 credential_source_format = credential_source.get("format", {})
136 # Get credential_source format type. When not provided, this
137 # defaults to text.
138 self._credential_source_format_type = (
139 credential_source_format.get("type") or "text"
140 )
141 # environment_id is only supported in AWS or dedicated future external
142 # account credentials.
143 if "environment_id" in credential_source:
144 raise ValueError(
145 "Invalid Identity Pool credential_source field 'environment_id'"
146 )
147 if self._credential_source_format_type not in ["text", "json"]:
148 raise ValueError(
149 "Invalid credential_source format '{}'".format(
150 self._credential_source_format_type
151 )
152 )
153 # For JSON types, get the required subject_token field name.
154 if self._credential_source_format_type == "json":
155 self._credential_source_field_name = credential_source_format.get(
156 "subject_token_field_name"
157 )
158 if self._credential_source_field_name is None:
159 raise ValueError(
160 "Missing subject_token_field_name for JSON credential_source format"
161 )
162 else:
163 self._credential_source_field_name = None
164
165 if self._credential_source_file and self._credential_source_url:
166 raise ValueError(
167 "Ambiguous credential_source. 'file' is mutually exclusive with 'url'."
168 )
169 if not self._credential_source_file and not self._credential_source_url:
170 raise ValueError(
171 "Missing credential_source. A 'file' or 'url' must be provided."
172 )
173
174 @_helpers.copy_docstring(external_account.Credentials)
175 def retrieve_subject_token(self, request):
176 return self._parse_token_data(
177 self._get_token_data(request),
178 self._credential_source_format_type,
179 self._credential_source_field_name,
180 )
181
182 def _get_token_data(self, request):
183 if self._credential_source_file:
184 return self._get_file_data(self._credential_source_file)
185 else:
186 return self._get_url_data(
187 request, self._credential_source_url, self._credential_source_headers
188 )
189
190 def _get_file_data(self, filename):
191 if not os.path.exists(filename):
192 raise exceptions.RefreshError("File '{}' was not found.".format(filename))
193
194 with io.open(filename, "r", encoding="utf-8") as file_obj:
195 return file_obj.read(), filename
196
197 def _get_url_data(self, request, url, headers):
198 response = request(url=url, method="GET", headers=headers)
199
200 # support both string and bytes type response.data
201 response_body = (
202 response.data.decode("utf-8")
203 if hasattr(response.data, "decode")
204 else response.data
205 )
206
207 if response.status != 200:
208 raise exceptions.RefreshError(
209 "Unable to retrieve Identity Pool subject token", response_body
210 )
211
212 return response_body, url
213
214 def _parse_token_data(
215 self, token_content, format_type="text", subject_token_field_name=None
216 ):
217 content, filename = token_content
218 if format_type == "text":
219 token = content
220 else:
221 try:
222 # Parse file content as JSON.
223 response_data = json.loads(content)
224 # Get the subject_token.
225 token = response_data[subject_token_field_name]
226 except (KeyError, ValueError):
227 raise exceptions.RefreshError(
228 "Unable to parse subject_token from JSON file '{}' using key '{}'".format(
229 filename, subject_token_field_name
230 )
231 )
232 if not token:
233 raise exceptions.RefreshError(
234 "Missing subject_token in the credential_source file"
235 )
236 return token
237
238 @classmethod
239 def from_info(cls, info, **kwargs):
240 """Creates an Identity Pool Credentials instance from parsed external account info.
241
242 Args:
243 info (Mapping[str, str]): The Identity Pool external account info in Google
244 format.
245 kwargs: Additional arguments to pass to the constructor.
246
247 Returns:
248 google.auth.identity_pool.Credentials: The constructed
249 credentials.
250
251 Raises:
252 ValueError: For invalid parameters.
253 """
254 return cls(
255 audience=info.get("audience"),
256 subject_token_type=info.get("subject_token_type"),
257 token_url=info.get("token_url"),
258 service_account_impersonation_url=info.get(
259 "service_account_impersonation_url"
260 ),
261 client_id=info.get("client_id"),
262 client_secret=info.get("client_secret"),
263 credential_source=info.get("credential_source"),
264 quota_project_id=info.get("quota_project_id"),
bojeil-google993bab22021-09-21 14:00:15 -0700265 workforce_pool_user_project=info.get("workforce_pool_user_project"),
bojeil-googled4d7f382021-02-16 12:33:20 -0800266 **kwargs
267 )
268
269 @classmethod
270 def from_file(cls, filename, **kwargs):
271 """Creates an IdentityPool Credentials instance from an external account json file.
272
273 Args:
274 filename (str): The path to the IdentityPool external account json file.
275 kwargs: Additional arguments to pass to the constructor.
276
277 Returns:
278 google.auth.identity_pool.Credentials: The constructed
279 credentials.
280 """
281 with io.open(filename, "r", encoding="utf-8") as json_file:
282 data = json.load(json_file)
283 return cls.from_info(data, **kwargs)