Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
external_account.py576 linesDownload Raw Back to auth
1# Copyright 2020 Google LLC2#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 at6#7#      http://www.apache.org/licenses/LICENSE-2.08#9# Unless required by applicable law or agreed to in writing, software10# 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 and13# limitations under the License.14 15"""External Account Credentials.16 17This module provides credentials that exchange workload identity pool external18credentials for Google access tokens. This facilitates accessing Google Cloud19Platform resources from on-prem and non-Google Cloud platforms (e.g. AWS,20Microsoft Azure, OIDC identity providers), using native credentials retrieved21from the current environment without the need to copy, save and manage22long-lived service account credentials.23 24Specifically, this is intended to use access tokens acquired using the GCP STS25token exchange endpoint following the `OAuth 2.0 Token Exchange`_ spec.26 27.. _OAuth 2.0 Token Exchange: https://tools.ietf.org/html/rfc869328"""29 30import abc31import copy32from dataclasses import dataclass33import datetime34import io35import json36import re37 38from google.auth import _helpers39from google.auth import credentials40from google.auth import exceptions41from google.auth import impersonated_credentials42from google.auth import metrics43from google.oauth2 import sts44from google.oauth2 import utils45 46# External account JSON type identifier.47_EXTERNAL_ACCOUNT_JSON_TYPE = "external_account"48# The token exchange grant_type used for exchanging credentials.49_STS_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange"50# The token exchange requested_token_type. This is always an access_token.51_STS_REQUESTED_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token"52# Cloud resource manager URL used to retrieve project information.53_CLOUD_RESOURCE_MANAGER = "https://cloudresourcemanager.googleapis.com/v1/projects/"54# Default Google sts token url.55_DEFAULT_TOKEN_URL = "https://sts.googleapis.com/v1/token"56 57 58@dataclass59class SupplierContext:60    """A context class that contains information about the requested third party credential that is passed61        to AWS security credential and subject token suppliers.62 63        Attributes:64            subject_token_type (str): The requested subject token type based on the Oauth2.0 token exchange spec.65                Expected values include::66 67                    “urn:ietf:params:oauth:token-type:jwt”68                    “urn:ietf:params:oauth:token-type:id-token”69                    “urn:ietf:params:oauth:token-type:saml2”70                    “urn:ietf:params:aws:token-type:aws4_request”71 72            audience (str): The requested audience for the subject token.73    """74 75    subject_token_type: str76    audience: str77 78 79class Credentials(80    credentials.Scoped,81    credentials.CredentialsWithQuotaProject,82    credentials.CredentialsWithTokenUri,83    metaclass=abc.ABCMeta,84):85    """Base class for all external account credentials.86 87    This is used to instantiate Credentials for exchanging external account88    credentials for Google access token and authorizing requests to Google APIs.89    The base class implements the common logic for exchanging external account90    credentials for Google access tokens.91    """92 93    def __init__(94        self,95        audience,96        subject_token_type,97        token_url,98        credential_source,99        service_account_impersonation_url=None,100        service_account_impersonation_options=None,101        client_id=None,102        client_secret=None,103        token_info_url=None,104        quota_project_id=None,105        scopes=None,106        default_scopes=None,107        workforce_pool_user_project=None,108        universe_domain=credentials.DEFAULT_UNIVERSE_DOMAIN,109        trust_boundary=None,110    ):111        """Instantiates an external account credentials object.112 113        Args:114            audience (str): The STS audience field.115            subject_token_type (str): The subject token type based on the Oauth2.0 token exchange spec.116                Expected values include::117 118                    “urn:ietf:params:oauth:token-type:jwt”119                    “urn:ietf:params:oauth:token-type:id-token”120                    “urn:ietf:params:oauth:token-type:saml2”121                    “urn:ietf:params:aws:token-type:aws4_request”122 123            token_url (str): The STS endpoint URL.124            credential_source (Mapping): The credential source dictionary.125            service_account_impersonation_url (Optional[str]): The optional service account126                impersonation generateAccessToken URL.127            client_id (Optional[str]): The optional client ID.128            client_secret (Optional[str]): The optional client secret.129            token_info_url (str): The optional STS endpoint URL for token introspection.130            quota_project_id (Optional[str]): The optional quota project ID.131            scopes (Optional[Sequence[str]]): Optional scopes to request during the132                authorization grant.133            default_scopes (Optional[Sequence[str]]): Default scopes passed by a134                Google client library. Use 'scopes' for user-defined scopes.135            workforce_pool_user_project (Optona[str]): The optional workforce pool user136                project number when the credential corresponds to a workforce pool and not137                a workload identity pool. The underlying principal must still have138                serviceusage.services.use IAM permission to use the project for139                billing/quota.140            universe_domain (str): The universe domain. The default universe141                domain is googleapis.com.142            trust_boundary (str): String representation of trust boundary meta.143        Raises:144            google.auth.exceptions.RefreshError: If the generateAccessToken145                endpoint returned an error.146        """147        super(Credentials, self).__init__()148        self._audience = audience149        self._subject_token_type = subject_token_type150        self._token_url = token_url151        self._token_info_url = token_info_url152        self._credential_source = credential_source153        self._service_account_impersonation_url = service_account_impersonation_url154        self._service_account_impersonation_options = (155            service_account_impersonation_options or {}156        )157        self._client_id = client_id158        self._client_secret = client_secret159        self._quota_project_id = quota_project_id160        self._scopes = scopes161        self._default_scopes = default_scopes162        self._workforce_pool_user_project = workforce_pool_user_project163        self._universe_domain = universe_domain or credentials.DEFAULT_UNIVERSE_DOMAIN164        self._trust_boundary = {165            "locations": [],166            "encoded_locations": "0x0",167        }  # expose a placeholder trust boundary value.168 169        if self._client_id:170            self._client_auth = utils.ClientAuthentication(171                utils.ClientAuthType.basic, self._client_id, self._client_secret172            )173        else:174            self._client_auth = None175        self._sts_client = sts.Client(self._token_url, self._client_auth)176 177        self._metrics_options = self._create_default_metrics_options()178 179        self._impersonated_credentials = None180        self._project_id = None181        self._supplier_context = SupplierContext(182            self._subject_token_type, self._audience183        )184 185        if not self.is_workforce_pool and self._workforce_pool_user_project:186            # Workload identity pools do not support workforce pool user projects.187            raise exceptions.InvalidValue(188                "workforce_pool_user_project should not be set for non-workforce pool "189                "credentials"190            )191 192    @property193    def info(self):194        """Generates the dictionary representation of the current credentials.195 196        Returns:197            Mapping: The dictionary representation of the credentials. This is the198                reverse of "from_info" defined on the subclasses of this class. It is199                useful for serializing the current credentials so it can deserialized200                later.201        """202        config_info = self._constructor_args()203        config_info.update(204            type=_EXTERNAL_ACCOUNT_JSON_TYPE,205            service_account_impersonation=config_info.pop(206                "service_account_impersonation_options", None207            ),208        )209        config_info.pop("scopes", None)210        config_info.pop("default_scopes", None)211        return {key: value for key, value in config_info.items() if value is not None}212 213    def _constructor_args(self):214        args = {215            "audience": self._audience,216            "subject_token_type": self._subject_token_type,217            "token_url": self._token_url,218            "token_info_url": self._token_info_url,219            "service_account_impersonation_url": self._service_account_impersonation_url,220            "service_account_impersonation_options": copy.deepcopy(221                self._service_account_impersonation_options222            )223            or None,224            "credential_source": copy.deepcopy(self._credential_source),225            "quota_project_id": self._quota_project_id,226            "client_id": self._client_id,227            "client_secret": self._client_secret,228            "workforce_pool_user_project": self._workforce_pool_user_project,229            "scopes": self._scopes,230            "default_scopes": self._default_scopes,231            "universe_domain": self._universe_domain,232        }233        if not self.is_workforce_pool:234            args.pop("workforce_pool_user_project")235        return args236 237    @property238    def service_account_email(self):239        """Returns the service account email if service account impersonation is used.240 241        Returns:242            Optional[str]: The service account email if impersonation is used. Otherwise243                None is returned.244        """245        if self._service_account_impersonation_url:246            # Parse email from URL. The formal looks as follows:247            # https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/name@project-id.iam.gserviceaccount.com:generateAccessToken248            url = self._service_account_impersonation_url249            start_index = url.rfind("/")250            end_index = url.find(":generateAccessToken")251            if start_index != -1 and end_index != -1 and start_index < end_index:252                start_index = start_index + 1253                return url[start_index:end_index]254        return None255 256    @property257    def is_user(self):258        """Returns whether the credentials represent a user (True) or workload (False).259        Workloads behave similarly to service accounts. Currently workloads will use260        service account impersonation but will eventually not require impersonation.261        As a result, this property is more reliable than the service account email262        property in determining if the credentials represent a user or workload.263 264        Returns:265            bool: True if the credentials represent a user. False if they represent a266                workload.267        """268        # If service account impersonation is used, the credentials will always represent a269        # service account.270        if self._service_account_impersonation_url:271            return False272        return self.is_workforce_pool273 274    @property275    def is_workforce_pool(self):276        """Returns whether the credentials represent a workforce pool (True) or277        workload (False) based on the credentials' audience.278 279        This will also return True for impersonated workforce pool credentials.280 281        Returns:282            bool: True if the credentials represent a workforce pool. False if they283                represent a workload.284        """285        # Workforce pools representing users have the following audience format:286        # //iam.googleapis.com/locations/$location/workforcePools/$poolId/providers/$providerId287        p = re.compile(r"//iam\.googleapis\.com/locations/[^/]+/workforcePools/")288        return p.match(self._audience or "") is not None289 290    @property291    def requires_scopes(self):292        """Checks if the credentials requires scopes.293 294        Returns:295            bool: True if there are no scopes set otherwise False.296        """297        return not self._scopes and not self._default_scopes298 299    @property300    def project_number(self):301        """Optional[str]: The project number corresponding to the workload identity pool."""302 303        # STS audience pattern:304        # //iam.googleapis.com/projects/$PROJECT_NUMBER/locations/...305        components = self._audience.split("/")306        try:307            project_index = components.index("projects")308            if project_index + 1 < len(components):309                return components[project_index + 1] or None310        except ValueError:311            return None312 313    @property314    def token_info_url(self):315        """Optional[str]: The STS token introspection endpoint."""316 317        return self._token_info_url318 319    @_helpers.copy_docstring(credentials.Scoped)320    def with_scopes(self, scopes, default_scopes=None):321        kwargs = self._constructor_args()322        kwargs.update(scopes=scopes, default_scopes=default_scopes)323        scoped = self.__class__(**kwargs)324        scoped._metrics_options = self._metrics_options325        return scoped326 327    @abc.abstractmethod328    def retrieve_subject_token(self, request):329        """Retrieves the subject token using the credential_source object.330 331        Args:332            request (google.auth.transport.Request): A callable used to make333                HTTP requests.334        Returns:335            str: The retrieved subject token.336        """337        # pylint: disable=missing-raises-doc338        # (pylint doesn't recognize that this is abstract)339        raise NotImplementedError("retrieve_subject_token must be implemented")340 341    def get_project_id(self, request):342        """Retrieves the project ID corresponding to the workload identity or workforce pool.343        For workforce pool credentials, it returns the project ID corresponding to344        the workforce_pool_user_project.345 346        When not determinable, None is returned.347 348        This is introduced to support the current pattern of using the Auth library:349 350            credentials, project_id = google.auth.default()351 352        The resource may not have permission (resourcemanager.projects.get) to353        call this API or the required scopes may not be selected:354        https://cloud.google.com/resource-manager/reference/rest/v1/projects/get#authorization-scopes355 356        Args:357            request (google.auth.transport.Request): A callable used to make358                HTTP requests.359        Returns:360            Optional[str]: The project ID corresponding to the workload identity pool361                or workforce pool if determinable.362        """363        if self._project_id:364            # If already retrieved, return the cached project ID value.365            return self._project_id366        scopes = self._scopes if self._scopes is not None else self._default_scopes367        # Scopes are required in order to retrieve a valid access token.368        project_number = self.project_number or self._workforce_pool_user_project369        if project_number and scopes:370            headers = {}371            url = _CLOUD_RESOURCE_MANAGER + project_number372            self.before_request(request, "GET", url, headers)373            response = request(url=url, method="GET", headers=headers)374 375            response_body = (376                response.data.decode("utf-8")377                if hasattr(response.data, "decode")378                else response.data379            )380            response_data = json.loads(response_body)381 382            if response.status == 200:383                # Cache result as this field is immutable.384                self._project_id = response_data.get("projectId")385                return self._project_id386 387        return None388 389    @_helpers.copy_docstring(credentials.Credentials)390    def refresh(self, request):391        scopes = self._scopes if self._scopes is not None else self._default_scopes392 393        if self._should_initialize_impersonated_credentials():394            self._impersonated_credentials = self._initialize_impersonated_credentials()395 396        if self._impersonated_credentials:397            self._impersonated_credentials.refresh(request)398            self.token = self._impersonated_credentials.token399            self.expiry = self._impersonated_credentials.expiry400        else:401            now = _helpers.utcnow()402            additional_options = None403            # Do not pass workforce_pool_user_project when client authentication404            # is used. The client ID is sufficient for determining the user project.405            if self._workforce_pool_user_project and not self._client_id:406                additional_options = {"userProject": self._workforce_pool_user_project}407            additional_headers = {408                metrics.API_CLIENT_HEADER: metrics.byoid_metrics_header(409                    self._metrics_options410                )411            }412            response_data = self._sts_client.exchange_token(413                request=request,414                grant_type=_STS_GRANT_TYPE,415                subject_token=self.retrieve_subject_token(request),416                subject_token_type=self._subject_token_type,417                audience=self._audience,418                scopes=scopes,419                requested_token_type=_STS_REQUESTED_TOKEN_TYPE,420                additional_options=additional_options,421                additional_headers=additional_headers,422            )423            self.token = response_data.get("access_token")424            expires_in = response_data.get("expires_in")425            # Some services do not respect the OAUTH2.0 RFC and send expires_in as a426            # JSON String.427            if isinstance(expires_in, str):428                expires_in = int(expires_in)429 430            lifetime = datetime.timedelta(seconds=expires_in)431 432            self.expiry = now + lifetime433 434    @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)435    def with_quota_project(self, quota_project_id):436        # Return copy of instance with the provided quota project ID.437        kwargs = self._constructor_args()438        kwargs.update(quota_project_id=quota_project_id)439        new_cred = self.__class__(**kwargs)440        new_cred._metrics_options = self._metrics_options441        return new_cred442 443    @_helpers.copy_docstring(credentials.CredentialsWithTokenUri)444    def with_token_uri(self, token_uri):445        kwargs = self._constructor_args()446        kwargs.update(token_url=token_uri)447        new_cred = self.__class__(**kwargs)448        new_cred._metrics_options = self._metrics_options449        return new_cred450 451    @_helpers.copy_docstring(credentials.CredentialsWithUniverseDomain)452    def with_universe_domain(self, universe_domain):453        kwargs = self._constructor_args()454        kwargs.update(universe_domain=universe_domain)455        new_cred = self.__class__(**kwargs)456        new_cred._metrics_options = self._metrics_options457        return new_cred458 459    def _should_initialize_impersonated_credentials(self):460        return (461            self._service_account_impersonation_url is not None462            and self._impersonated_credentials is None463        )464 465    def _initialize_impersonated_credentials(self):466        """Generates an impersonated credentials.467 468        For more details, see `projects.serviceAccounts.generateAccessToken`_.469 470        .. _projects.serviceAccounts.generateAccessToken: https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/generateAccessToken471 472        Returns:473            impersonated_credentials.Credential: The impersonated credentials474                object.475 476        Raises:477            google.auth.exceptions.RefreshError: If the generateAccessToken478                endpoint returned an error.479        """480        # Return copy of instance with no service account impersonation.481        kwargs = self._constructor_args()482        kwargs.update(483            service_account_impersonation_url=None,484            service_account_impersonation_options={},485        )486        source_credentials = self.__class__(**kwargs)487        source_credentials._metrics_options = self._metrics_options488 489        # Determine target_principal.490        target_principal = self.service_account_email491        if not target_principal:492            raise exceptions.RefreshError(493                "Unable to determine target principal from service account impersonation URL."494            )495 496        scopes = self._scopes if self._scopes is not None else self._default_scopes497        # Initialize and return impersonated credentials.498        return impersonated_credentials.Credentials(499            source_credentials=source_credentials,500            target_principal=target_principal,501            target_scopes=scopes,502            quota_project_id=self._quota_project_id,503            iam_endpoint_override=self._service_account_impersonation_url,504            lifetime=self._service_account_impersonation_options.get(505                "token_lifetime_seconds"506            ),507        )508 509    def _create_default_metrics_options(self):510        metrics_options = {}511        if self._service_account_impersonation_url:512            metrics_options["sa-impersonation"] = "true"513        else:514            metrics_options["sa-impersonation"] = "false"515        if self._service_account_impersonation_options.get("token_lifetime_seconds"):516            metrics_options["config-lifetime"] = "true"517        else:518            metrics_options["config-lifetime"] = "false"519 520        return metrics_options521 522    @classmethod523    def from_info(cls, info, **kwargs):524        """Creates a Credentials instance from parsed external account info.525 526        Args:527            info (Mapping[str, str]): The external account info in Google528                format.529            kwargs: Additional arguments to pass to the constructor.530 531        Returns:532            google.auth.identity_pool.Credentials: The constructed533                credentials.534 535        Raises:536            InvalidValue: For invalid parameters.537        """538        return cls(539            audience=info.get("audience"),540            subject_token_type=info.get("subject_token_type"),541            token_url=info.get("token_url"),542            token_info_url=info.get("token_info_url"),543            service_account_impersonation_url=info.get(544                "service_account_impersonation_url"545            ),546            service_account_impersonation_options=info.get(547                "service_account_impersonation"548            )549            or {},550            client_id=info.get("client_id"),551            client_secret=info.get("client_secret"),552            credential_source=info.get("credential_source"),553            quota_project_id=info.get("quota_project_id"),554            workforce_pool_user_project=info.get("workforce_pool_user_project"),555            universe_domain=info.get(556                "universe_domain", credentials.DEFAULT_UNIVERSE_DOMAIN557            ),558            **kwargs559        )560 561    @classmethod562    def from_file(cls, filename, **kwargs):563        """Creates a Credentials instance from an external account json file.564 565        Args:566            filename (str): The path to the external account json file.567            kwargs: Additional arguments to pass to the constructor.568 569        Returns:570            google.auth.identity_pool.Credentials: The constructed571                credentials.572        """573        with io.open(filename, "r", encoding="utf-8") as json_file:574            data = json.load(json_file)575            return cls.from_info(data, **kwargs)576