diff --git a/google/api_core/client_options.py b/google/api_core/client_options.py index d11665d2..30bff482 100644 --- a/google/api_core/client_options.py +++ b/google/api_core/client_options.py @@ -49,6 +49,9 @@ def get_client_cert(): """ from typing import Callable, Mapping, Optional, Sequence, Tuple +import warnings + +from google.api_core import general_helpers class ClientOptions(object): @@ -67,8 +70,9 @@ class ClientOptions(object): and ``client_encrypted_cert_source`` are mutually exclusive. quota_project_id (Optional[str]): A project name that a client's quota belongs to. - credentials_file (Optional[str]): A path to a file storing credentials. - ``credentials_file` and ``api_key`` are mutually exclusive. + credentials_file (Optional[str]): Deprecated. A path to a file storing credentials. + ``credentials_file` and ``api_key`` are mutually exclusive. This argument will be + removed in the next major version of `google-api-core`. .. warning:: Important: If you accept a credential configuration (credential JSON/File/Stream) @@ -114,6 +118,9 @@ def __init__( api_audience: Optional[str] = None, universe_domain: Optional[str] = None, ): + if credentials_file is not None: + warnings.warn(general_helpers._CREDENTIALS_FILE_WARNING, DeprecationWarning) + if client_cert_source and client_encrypted_cert_source: raise ValueError( "client_cert_source and client_encrypted_cert_source are mutually exclusive" diff --git a/google/api_core/general_helpers.py b/google/api_core/general_helpers.py index a6af45b7..06282299 100644 --- a/google/api_core/general_helpers.py +++ b/google/api_core/general_helpers.py @@ -14,3 +14,39 @@ # This import for backward compatibility only. from functools import wraps # noqa: F401 pragma: NO COVER + +_CREDENTIALS_FILE_WARNING = """\ +The `credentials_file` argument is deprecated because of a potential security risk. + +The `google.auth.load_credentials_from_file` method does not validate the credential +configuration. The security risk occurs when a credential configuration is accepted +from a source that is not under your control and used without validation on your side. + +If you know that you will be loading credential configurations of a +specific type, it is recommended to use a credential-type-specific +load method. + +This will ensure that an unexpected credential type with potential for +malicious intent is not loaded unintentionally. You might still have to do +validation for certain credential types. Please follow the recommendations +for that method. For example, if you want to load only service accounts, +you can create the service account credentials explicitly: + +``` +from google.cloud.vision_v1 import ImageAnnotatorClient +from google.oauth2 import service_account + +credentials = service_account.Credentials.from_service_account_file(filename) +client = ImageAnnotatorClient(credentials=credentials) +``` + +If you are loading your credential configuration from an untrusted source and have +not mitigated the risks (e.g. by validating the configuration yourself), make +these changes as soon as possible to prevent security risks to your environment. + +Regardless of the method used, it is always your responsibility to validate +configurations received from external sources. + +Refer to https://cloud.google.com/docs/authentication/external/externally-sourced-credentials +for more details. +""" diff --git a/google/api_core/grpc_helpers.py b/google/api_core/grpc_helpers.py index 07963024..430b8ce4 100644 --- a/google/api_core/grpc_helpers.py +++ b/google/api_core/grpc_helpers.py @@ -13,20 +13,19 @@ # limitations under the License. """Helpers for :mod:`grpc`.""" -from typing import Generic, Iterator, Optional, TypeVar - import collections import functools +from typing import Generic, Iterator, Optional, TypeVar import warnings -import grpc - -from google.api_core import exceptions import google.auth import google.auth.credentials import google.auth.transport.grpc import google.auth.transport.requests import google.protobuf +import grpc + +from google.api_core import exceptions, general_helpers PROTOBUF_VERSION = google.protobuf.__version__ @@ -213,9 +212,10 @@ def _create_composite_credentials( credentials (google.auth.credentials.Credentials): The credentials. If not specified, then this function will attempt to ascertain the credentials from the environment using :func:`google.auth.default`. - credentials_file (str): A file with credentials that can be loaded with + credentials_file (str): Deprecated. A file with credentials that can be loaded with :func:`google.auth.load_credentials_from_file`. This argument is - mutually exclusive with credentials. + mutually exclusive with credentials. This argument will be + removed in the next major version of `google-api-core`. .. warning:: Important: If you accept a credential configuration (credential JSON/File/Stream) @@ -245,6 +245,9 @@ def _create_composite_credentials( Raises: google.api_core.DuplicateCredentialArgs: If both a credentials object and credentials_file are passed. """ + if credentials_file is not None: + warnings.warn(general_helpers._CREDENTIALS_FILE_WARNING, DeprecationWarning) + if credentials and credentials_file: raise exceptions.DuplicateCredentialArgs( "'credentials' and 'credentials_file' are mutually exclusive." diff --git a/google/api_core/grpc_helpers_async.py b/google/api_core/grpc_helpers_async.py index af661430..312d4df8 100644 --- a/google/api_core/grpc_helpers_async.py +++ b/google/api_core/grpc_helpers_async.py @@ -20,13 +20,14 @@ import asyncio import functools +import warnings from typing import AsyncGenerator, Generic, Iterator, Optional, TypeVar import grpc from grpc import aio -from google.api_core import exceptions, grpc_helpers +from google.api_core import exceptions, general_helpers, grpc_helpers # denotes the proto response type for grpc calls P = TypeVar("P") @@ -233,9 +234,10 @@ def create_channel( are passed to :func:`google.auth.default`. ssl_credentials (grpc.ChannelCredentials): Optional SSL channel credentials. This can be used to specify different certificates. - credentials_file (str): A file with credentials that can be loaded with + credentials_file (str): Deprecated. A file with credentials that can be loaded with :func:`google.auth.load_credentials_from_file`. This argument is - mutually exclusive with credentials. + mutually exclusive with credentials. This argument will be + removed in the next major version of `google-api-core`. .. warning:: Important: If you accept a credential configuration (credential JSON/File/Stream) @@ -280,6 +282,9 @@ def create_channel( ValueError: If `ssl_credentials` is set and `attempt_direct_path` is set to `True`. """ + if credentials_file is not None: + warnings.warn(general_helpers._CREDENTIALS_FILE_WARNING, DeprecationWarning) + # If `ssl_credentials` is set and `attempt_direct_path` is set to `True`, # raise ValueError as this is not yet supported. # See https://github.com/googleapis/python-api-core/issues/590 diff --git a/google/api_core/operations_v1/transports/base.py b/google/api_core/operations_v1/transports/base.py index 71764c1e..46c2f5d1 100644 --- a/google/api_core/operations_v1/transports/base.py +++ b/google/api_core/operations_v1/transports/base.py @@ -16,12 +16,8 @@ import abc import re from typing import Awaitable, Callable, Optional, Sequence, Union +import warnings -import google.api_core # type: ignore -from google.api_core import exceptions as core_exceptions # type: ignore -from google.api_core import gapic_v1 # type: ignore -from google.api_core import retry as retries # type: ignore -from google.api_core import version import google.auth # type: ignore from google.auth import credentials as ga_credentials # type: ignore from google.longrunning import operations_pb2 @@ -30,6 +26,12 @@ from google.protobuf import empty_pb2, json_format # type: ignore from grpc import Compression +import google.api_core # type: ignore +from google.api_core import exceptions as core_exceptions # type: ignore +from google.api_core import gapic_v1 # type: ignore +from google.api_core import general_helpers +from google.api_core import retry as retries # type: ignore +from google.api_core import version PROTOBUF_VERSION = google.protobuf.__version__ @@ -69,9 +71,10 @@ def __init__( credentials identify the application to the service; if none are specified, the client will attempt to ascertain the credentials from the environment. - credentials_file (Optional[str]): A file with credentials that can + credentials_file (Optional[str]): Deprecated. A file with credentials that can be loaded with :func:`google.auth.load_credentials_from_file`. - This argument is mutually exclusive with credentials. + This argument is mutually exclusive with credentials. This argument will be + removed in the next major version of `google-api-core`. .. warning:: Important: If you accept a credential configuration (credential JSON/File/Stream) @@ -98,6 +101,9 @@ def __init__( "https", but for testing or local servers, "http" can be specified. """ + if credentials_file is not None: + warnings.warn(general_helpers._CREDENTIALS_FILE_WARNING, DeprecationWarning) + maybe_url_match = re.match("^(?Phttp(?:s)?://)?(?P.*)$", host) if maybe_url_match is None: raise ValueError( diff --git a/google/api_core/operations_v1/transports/rest.py b/google/api_core/operations_v1/transports/rest.py index 0705c518..62f34d69 100644 --- a/google/api_core/operations_v1/transports/rest.py +++ b/google/api_core/operations_v1/transports/rest.py @@ -15,23 +15,26 @@ # from typing import Callable, Dict, Optional, Sequence, Tuple, Union +import warnings +from google.auth import credentials as ga_credentials # type: ignore +from google.auth.transport.requests import AuthorizedSession # type: ignore +from google.longrunning import operations_pb2 # type: ignore +import google.protobuf +from google.protobuf import empty_pb2 # type: ignore +from google.protobuf import json_format # type: ignore +import grpc from requests import __version__ as requests_version from google.api_core import exceptions as core_exceptions # type: ignore from google.api_core import gapic_v1 # type: ignore +from google.api_core import general_helpers from google.api_core import path_template # type: ignore from google.api_core import rest_helpers # type: ignore from google.api_core import retry as retries # type: ignore -from google.auth import credentials as ga_credentials # type: ignore -from google.auth.transport.requests import AuthorizedSession # type: ignore -from google.longrunning import operations_pb2 # type: ignore -from google.protobuf import empty_pb2 # type: ignore -from google.protobuf import json_format # type: ignore -import google.protobuf -import grpc -from .base import DEFAULT_CLIENT_INFO as BASE_DEFAULT_CLIENT_INFO, OperationsTransport +from .base import DEFAULT_CLIENT_INFO as BASE_DEFAULT_CLIENT_INFO +from .base import OperationsTransport PROTOBUF_VERSION = google.protobuf.__version__ @@ -91,9 +94,10 @@ def __init__( are specified, the client will attempt to ascertain the credentials from the environment. - credentials_file (Optional[str]): A file with credentials that can + credentials_file (Optional[str]): Deprecated. A file with credentials that can be loaded with :func:`google.auth.load_credentials_from_file`. - This argument is ignored if ``channel`` is provided. + This argument is ignored if ``channel`` is provided. This argument will be + removed in the next major version of `google-api-core`. .. warning:: Important: If you accept a credential configuration (credential JSON/File/Stream) @@ -101,9 +105,9 @@ def __init__( validate it before providing it to any Google API or client library. Providing an unvalidated credential configuration to Google APIs or libraries can compromise the security of your systems and data. For more information, refer to - `Validate credential configurations from external sources`_. + `Validate credential configuration from external sources`_. - .. _Validate credential configurations from external sources: + .. _Validate credential configuration from external sources: https://cloud.google.com/docs/authentication/external/externally-sourced-credentials scopes (Optional(Sequence[str])): A list of scopes. This argument is @@ -130,6 +134,9 @@ def __init__( "v1" by default. """ + if credentials_file is not None: + warnings.warn(general_helpers._CREDENTIALS_FILE_WARNING, DeprecationWarning) + # Run the base constructor # TODO(yon-mg): resolve other ctor params i.e. scopes, quota, etc. # TODO: When custom host (api_endpoint) is set, `scopes` must *also* be set on the diff --git a/google/api_core/operations_v1/transports/rest_asyncio.py b/google/api_core/operations_v1/transports/rest_asyncio.py index 71c20eb8..6fa9f56a 100644 --- a/google/api_core/operations_v1/transports/rest_asyncio.py +++ b/google/api_core/operations_v1/transports/rest_asyncio.py @@ -16,6 +16,7 @@ import json from typing import Any, Callable, Coroutine, Dict, Optional, Sequence, Tuple +import warnings from google.auth import __version__ as auth_version @@ -29,6 +30,7 @@ from google.api_core import exceptions as core_exceptions # type: ignore from google.api_core import gapic_v1 # type: ignore +from google.api_core import general_helpers from google.api_core import path_template # type: ignore from google.api_core import rest_helpers # type: ignore from google.api_core import retry_async as retries_async # type: ignore @@ -96,6 +98,22 @@ def __init__( credentials identify the application to the service; if none are specified, the client will attempt to ascertain the credentials from the environment. + credentials_file (Optional[str]): Deprecated. A file with credentials that can + be loaded with :func:`google.auth.load_credentials_from_file`. + This argument is ignored if ``channel`` is provided. This argument will be + removed in the next major version of `google-api-core`. + + .. warning:: + Important: If you accept a credential configuration (credential JSON/File/Stream) + from an external source for authentication to Google Cloud Platform, you must + validate it before providing it to any Google API or client library. Providing an + unvalidated credential configuration to Google APIs or libraries can compromise + the security of your systems and data. For more information, refer to + `Validate credential configurations from external sources`_. + + .. _Validate credential configurations from external sources: + + https://cloud.google.com/docs/authentication/external/externally-sourced-credentials client_info (google.api_core.gapic_v1.client_info.ClientInfo): The client info used to send a user-agent string along with API requests. If ``None``, then default info will be used. @@ -113,6 +131,9 @@ def __init__( "v1" by default. """ + if credentials_file is not None: + warnings.warn(general_helpers._CREDENTIALS_FILE_WARNING, DeprecationWarning) + unsupported_params = { # TODO(https://github.com/googleapis/python-api-core/issues/715): Add support for `credentials_file` to async REST transport. "google.api_core.client_options.ClientOptions.credentials_file": credentials_file, diff --git a/tests/unit/operations_v1/test_operations_rest_client.py b/tests/unit/operations_v1/test_operations_rest_client.py index d1f6e0eb..4e8ef407 100644 --- a/tests/unit/operations_v1/test_operations_rest_client.py +++ b/tests/unit/operations_v1/test_operations_rest_client.py @@ -368,6 +368,22 @@ def test_operations_client_client_options( always_use_jwt_access=True, ) + # Check the case credentials_file is provided + options = client_options.ClientOptions(credentials_file="credentials.json") + with mock.patch.object(transport_class, "__init__") as patched: + patched.return_value = None + client = client_class(client_options=options, transport=transport_name) + patched.assert_called_once_with( + credentials=None, + credentials_file="credentials.json", + host=client.DEFAULT_ENDPOINT, + scopes=None, + client_cert_source_for_mtls=None, + quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, + always_use_jwt_access=True, + ) + # TODO: Add support for mtls in async REST @pytest.mark.parametrize( @@ -544,8 +560,23 @@ def test_operations_client_client_options_credentials_file( ) -def test_list_operations_rest(): - client = _get_operations_client(is_async=False) +@pytest.mark.parametrize( + "credentials_file", + [None, "credentials.json"], +) +@mock.patch( + "google.auth.default", + autospec=True, + return_value=(mock.sentinel.credentials, mock.sentinel.project), +) +def test_list_operations_rest(google_auth_default, credentials_file): + sync_transport = transports.rest.OperationsRestTransport( + credentials_file=credentials_file, + http_options=HTTP_OPTIONS, + ) + + client = AbstractOperationsClient(transport=sync_transport) + # Mock the http request call within the method and fake a response. with mock.patch.object(_get_session_type(is_async=False), "request") as req: # Designate an appropriate value for the returned response.