mirror of
https://github.com/certbot/certbot.git
synced 2026-01-26 07:41:33 +03:00
google: use Application Default Credentials where available (#9670)
* google: use Application Default Credentials where available * Updated custom role documentation
This commit is contained in:
@@ -123,6 +123,7 @@ Authors
|
||||
* [James Balazs](https://github.com/jamesbalazs)
|
||||
* [James Kasten](https://github.com/jdkasten)
|
||||
* [Jason Grinblat](https://github.com/ptychomancer)
|
||||
* [Jawshua](https://github.com/jawshua)
|
||||
* [Jay Faulkner](https://github.com/jayofdoom)
|
||||
* [J.C. Jones](https://github.com/jcjones)
|
||||
* [Jeff Hodges](https://github.com/jmhodges)
|
||||
|
||||
@@ -14,7 +14,14 @@ Named Arguments
|
||||
======================================== =====================================
|
||||
``--dns-google-credentials`` Google Cloud Platform credentials_
|
||||
JSON file.
|
||||
(Required - Optional on Google Compute Engine)
|
||||
|
||||
(Required if not using `Application Default
|
||||
Credentials <https://cloud.google.com/docs/authentication/
|
||||
application-default-credentials>`_.)
|
||||
``--dns-google-project`` The ID of the Google Cloud project that the Google
|
||||
Cloud DNS managed zone(s) reside in.
|
||||
|
||||
(Default: project that the Google credentials_ belong to)
|
||||
``--dns-google-propagation-seconds`` The number of seconds to wait for DNS
|
||||
to propagate before asking the ACME
|
||||
server to verify the DNS record.
|
||||
@@ -25,45 +32,37 @@ Named Arguments
|
||||
Credentials
|
||||
-----------
|
||||
|
||||
Use of this plugin requires Google Cloud Platform API credentials
|
||||
for an account with the following permissions:
|
||||
Use of this plugin requires Google Cloud Platform credentials with the ability to modify the Cloud
|
||||
DNS managed zone(s) for which certificates are being issued.
|
||||
|
||||
* ``dns.changes.create``
|
||||
* ``dns.changes.get``
|
||||
* ``dns.changes.list``
|
||||
* ``dns.managedZones.get``
|
||||
* ``dns.managedZones.list``
|
||||
* ``dns.resourceRecordSets.create``
|
||||
* ``dns.resourceRecordSets.delete``
|
||||
* ``dns.resourceRecordSets.list``
|
||||
* ``dns.resourceRecordSets.update``
|
||||
In most cases, configuring credentials for Certbot will require `creating a service account
|
||||
<https://cloud.google.com/iam/docs/service-accounts-create>`_, and then either `granting permissions
|
||||
with predefined roles`_ or `granting permissions with custom roles`_ using IAM.
|
||||
|
||||
(The closest role is `dns.admin <https://cloud.google.com/dns/docs/
|
||||
access-control#dns.admin>`_).
|
||||
|
||||
If the above permissions are assigned at the `resource level <https://cloud
|
||||
.google.com/dns/docs/zones/iam-per-resource-zones>`_, the same user must
|
||||
have, at the PROJECT level, the following permissions:
|
||||
Providing Credentials
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
* ``dns.managedZones.get``
|
||||
* ``dns.managedZones.list``
|
||||
The preferred method of providing credentials is to `set up Application Default Credentials
|
||||
<https://cloud.google.com/docs/authentication/provide-credentials-adc>`_ (ADC) in the environment
|
||||
that Certbot is running in.
|
||||
|
||||
(The closest role is `dns.reader <https://cloud.google.com/dns/docs/
|
||||
access-control#dns.reader>`_).
|
||||
If you are running Certbot on Google Cloud then a service account can be assigned directly to most
|
||||
types of workload, including `Compute Engine VMs <https://cloud.google.com/compute/docs/access/
|
||||
create-enable-service-accounts-for-instances>`_, `Kubernetes Engine Pods <https://cloud.google.com/
|
||||
kubernetes-engine/docs/how-to/workload-identity>`_, `Cloud Run jobs <https://cloud.google.com/run
|
||||
/docs/securing/service-identity>`_, `Cloud Functions <https://cloud.google.com/functions/docs/
|
||||
securing/function-identity>`_, and `Cloud Builds <https://cloud.google.com/build/docs/securing-
|
||||
builds/configure-user-specified-service-accounts>`_.
|
||||
|
||||
Google provides instructions for `creating a service account <https://developers
|
||||
.google.com/identity/protocols/OAuth2ServiceAccount#creatinganaccount>`_ and
|
||||
`information about the required permissions <https://cloud.google.com/dns/access
|
||||
-control#permissions_and_roles>`_. If you're running on Google Compute Engine,
|
||||
you can `assign the service account to the instance <https://cloud.google.com/
|
||||
compute/docs/access/create-enable-service-accounts-for-instances>`_ which
|
||||
is running certbot. A credentials file is not required in this case, as they
|
||||
are automatically obtained by certbot through the `metadata service
|
||||
<https://cloud.google.com/compute/docs/storing-retrieving-metadata>`_ .
|
||||
If you are not running Certbot on Google Cloud then a credentials file should be provided using the
|
||||
``--dns-google-credentials`` command-line argument. Google provides documentation for `creating
|
||||
service account keys <https://cloud.google.com/iam/docs/keys-create-delete#creating>`_, which is the
|
||||
most common method of using a service account outside of Google Cloud.
|
||||
|
||||
.. code-block:: json
|
||||
:name: credentials.json
|
||||
:caption: Example credentials file:
|
||||
:name: credentials-sa.json
|
||||
:caption: Example service account key file:
|
||||
|
||||
{
|
||||
"type": "service_account",
|
||||
@@ -78,12 +77,8 @@ are automatically obtained by certbot through the `metadata service
|
||||
"client_x509_cert_url": "..."
|
||||
}
|
||||
|
||||
The path to this file can be provided interactively or using the
|
||||
``--dns-google-credentials`` command-line argument. Certbot records the path
|
||||
to this file for use during renewal, but does not store the file's contents.
|
||||
|
||||
.. caution::
|
||||
You should protect these API credentials as you would a password. Users who
|
||||
You should protect these credentials as you would a password. Users who
|
||||
can read this file can use these credentials to issue some types of API calls
|
||||
on your behalf, limited by the permissions assigned to the account. Users who
|
||||
can cause Certbot to run using these credentials can complete a ``dns-01``
|
||||
@@ -97,35 +92,132 @@ file. This warning will be emitted each time Certbot uses the credentials file,
|
||||
including for renewal, and cannot be silenced except by addressing the issue
|
||||
(e.g., by using a command like ``chmod 600`` to restrict access to the file).
|
||||
|
||||
If you are running Certbot within another cloud platform, a CI platform, or any other platform that
|
||||
supports issuing OpenID Connect Tokens, then you may also have the option of securely authenticating
|
||||
with `workload identity federation <https://cloud.google.com/iam/docs/workload-identity-
|
||||
federation>`_. Instructions are generally available for most platforms, including `AWS or Azure
|
||||
<https://cloud.google.com/iam/docs/workload-identity-federation-with-other-clouds>`_, `GitHub
|
||||
Actions <https://cloud.google.com/blog/products/identity-security/enabling-keyless-authentication
|
||||
-from-github-actions>`_, and `GitLab CI <https://docs.gitlab.com/ee/ci/cloud_services/
|
||||
google_cloud/>`_.
|
||||
|
||||
|
||||
Granting Permissions with Predefined Roles
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
The simplest method of granting the required permissions to the user or service account that Certbot
|
||||
is authenticating with is to use either of these predefined role strategies:
|
||||
|
||||
* `dns.admin <https://cloud.google.com/dns/docs/access-control#dns.admin>`_ against the *DNS
|
||||
zone(s)* that Certbot will be issuing certificates for.
|
||||
* `dns.reader <https://cloud.google.com/dns/docs/access-control#dns.reader>`_ against the *project*
|
||||
containing the relevant DNS zones.
|
||||
|
||||
*or*
|
||||
|
||||
* `dns.admin <https://cloud.google.com/dns/docs/access-control#dns.admin>`_ against the *project*
|
||||
containing the relevant DNS zones
|
||||
|
||||
For instructions on how to grant roles, please read the Google provided documentation for `granting
|
||||
access roles against a project <https://cloud.google.com/iam/docs/granting-changing-revoking-access
|
||||
#single-role>`_ and `granting access roles against zones <https://cloud.google.com/dns/docs/zones/
|
||||
iam-per-resource-zones#set_access_control_policy_for_a_specific_resource>`_.
|
||||
|
||||
.. caution::
|
||||
Granting the ``dns.admin`` role at the project level can present a significant security risk. It
|
||||
provides full administrative access to all DNS zones within the project, granting the ability to
|
||||
perform any action up to and including deleting all zones within a project.
|
||||
|
||||
|
||||
Granting Permissions with Custom Roles
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Custom roles are an alternative to predefined roles that provide the ability to define fine grained
|
||||
permission sets for specific use cases. They should generally be used when it is desirable to adhere
|
||||
to the principle of least privilege, such as within production or other security sensitive
|
||||
workloads.
|
||||
|
||||
The following is an example strategy for granting granular permissions to Certbot using custom
|
||||
roles. If you are not already familiar with how to do so, Google provides documentation for
|
||||
`creating a custom IAM role <https://cloud.google.com/iam/docs/creating-custom-roles#creating>`_.
|
||||
|
||||
Firstly, create a custom role containing the permissions required to make DNS record updates. We
|
||||
suggest naming the custom role ``Certbot - Zone Editor`` with the ID ``certbot.zoneEditor``. The
|
||||
following permissions are required:
|
||||
|
||||
* ``dns.changes.create``
|
||||
* ``dns.changes.get``
|
||||
* ``dns.changes.list``
|
||||
* ``dns.resourceRecordSets.create``
|
||||
* ``dns.resourceRecordSets.delete``
|
||||
* ``dns.resourceRecordSets.list``
|
||||
* ``dns.resourceRecordSets.update``
|
||||
|
||||
Next, create a custom role granting Certbot the ability to discover DNS zones. We suggest naming the
|
||||
custom role ``Certbot - Zone Lister`` with the ID ``certbot.zoneLister``. The following permissions
|
||||
are required:
|
||||
|
||||
* ``dns.managedZones.get``
|
||||
* ``dns.managedZones.list``
|
||||
|
||||
Finally, grant the custom roles to the user or service account that Certbot is authenticating with:
|
||||
|
||||
* Grant your custom ``Certbot - Zone Editor`` role against the *DNS zone(s)* that Certbot will be
|
||||
issuing certificates for.
|
||||
* Grant your custom ``Certbot - Zone Lister`` role against the *project* containing the relevant DNS
|
||||
zones.
|
||||
|
||||
For instructions on how to grant roles, please read the Google provided documentation for `granting
|
||||
access roles against a project <https://cloud.google.com/iam/docs/granting-changing-revoking-access
|
||||
#single-role>`_ and `granting access roles against zones <https://cloud.google.com/dns/docs/zones/
|
||||
iam-per-resource-zones#set_access_control_policy_for_a_specific_resource>`_.
|
||||
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
.. code-block:: bash
|
||||
:caption: To acquire a certificate for ``example.com``
|
||||
:caption: To acquire a certificate for ``example.com``, providing a credentials file
|
||||
|
||||
certbot certonly \\
|
||||
--dns-google \\
|
||||
--dns-google-credentials ~/.secrets/certbot/google.json \\
|
||||
-d example.com
|
||||
|
||||
.. code-block:: bash
|
||||
:caption: To acquire a certificate for ``example.com``, where ADC is available and
|
||||
a credentials file is not required
|
||||
|
||||
certbot certonly \\
|
||||
--dns-google \\
|
||||
-d example.com
|
||||
|
||||
.. code-block:: bash
|
||||
:caption: To acquire a single certificate for both ``example.com`` and
|
||||
``www.example.com``
|
||||
|
||||
certbot certonly \\
|
||||
--dns-google \\
|
||||
--dns-google-credentials ~/.secrets/certbot/google.json \\
|
||||
-d example.com \\
|
||||
-d www.example.com
|
||||
|
||||
.. code-block:: bash
|
||||
:caption: To acquire a certificate for ``example.com``, where the managed DNS
|
||||
zone resides in another Google Cloud project.
|
||||
|
||||
certbot certonly \\
|
||||
--dns-google \\
|
||||
--dns-google-credentials ~/.secrets/certbot/google-project-test-foo.json \\
|
||||
--dns-google-project test-bar \\
|
||||
-d example.com
|
||||
|
||||
|
||||
.. code-block:: bash
|
||||
:caption: To acquire a certificate for ``example.com``, waiting 120 seconds
|
||||
for DNS propagation
|
||||
|
||||
certbot certonly \\
|
||||
--dns-google \\
|
||||
--dns-google-credentials ~/.secrets/certbot/google.json \\
|
||||
--dns-google-propagation-seconds 120 \\
|
||||
-d example.com
|
||||
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
"""DNS Authenticator for Google Cloud DNS."""
|
||||
import json
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import Dict
|
||||
from typing import Optional
|
||||
|
||||
import google.auth
|
||||
|
||||
from google.auth import exceptions as googleauth_exceptions
|
||||
from googleapiclient import discovery
|
||||
from googleapiclient import errors as googleapiclient_errors
|
||||
import httplib2
|
||||
from oauth2client.service_account import ServiceAccountCredentials
|
||||
|
||||
from certbot import errors
|
||||
from certbot.plugins import dns_common
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
ADC_URL = 'https://cloud.google.com/docs/authentication/application-default-credentials'
|
||||
ACCT_URL = 'https://developers.google.com/identity/protocols/OAuth2ServiceAccount#creatinganaccount'
|
||||
PERMISSIONS_URL = 'https://cloud.google.com/dns/access-control#permissions_and_roles'
|
||||
METADATA_URL = 'http://metadata.google.internal/computeMetadata/v1/'
|
||||
@@ -31,15 +32,23 @@ class Authenticator(dns_common.DNSAuthenticator):
|
||||
description = ('Obtain certificates using a DNS TXT record (if you are using Google Cloud DNS '
|
||||
'for DNS).')
|
||||
ttl = 60
|
||||
google_client = None
|
||||
|
||||
@classmethod
|
||||
def add_parser_arguments(cls, add: Callable[..., None],
|
||||
default_propagation_seconds: int = 60) -> None:
|
||||
super().add_parser_arguments(add, default_propagation_seconds=60)
|
||||
add('credentials',
|
||||
help=('Path to Google Cloud DNS service account JSON file. (See {0} for' +
|
||||
'information about creating a service account and {1} for information about the' +
|
||||
'required permissions.)').format(ACCT_URL, PERMISSIONS_URL),
|
||||
help=('Path to Google Cloud DNS service account JSON file to use instead of relying on'
|
||||
' Application Default Credentials (ADC). (See {0} for information about ADC, {1}'
|
||||
' for information about creating a service account, and {2} for information about'
|
||||
' the permissions required to modify Cloud DNS records.)')
|
||||
.format(ADC_URL, ACCT_URL, PERMISSIONS_URL),
|
||||
default=None)
|
||||
|
||||
add('project',
|
||||
help=('The ID of the Google Cloud project that the Google Cloud DNS managed zone(s)' +
|
||||
' reside in. This will be determined automatically if not specified.'),
|
||||
default=None)
|
||||
|
||||
def more_info(self) -> str:
|
||||
@@ -47,22 +56,19 @@ class Authenticator(dns_common.DNSAuthenticator):
|
||||
'the Google Cloud DNS API.'
|
||||
|
||||
def _setup_credentials(self) -> None:
|
||||
if self.conf('credentials') is None:
|
||||
try:
|
||||
# use project_id query to check for availability of google metadata server
|
||||
# we won't use the result but know we're not on GCP when an exception is thrown
|
||||
_GoogleClient.get_project_id()
|
||||
except (ValueError, httplib2.ServerNotFoundError):
|
||||
raise errors.PluginError('Unable to get Google Cloud Metadata and no credentials'
|
||||
' specified. Automatic credential lookup is only '
|
||||
'available on Google Cloud Platform. Please configure'
|
||||
' credentials using --dns-google-credentials <file>')
|
||||
else:
|
||||
if self.conf('credentials') is not None:
|
||||
self._configure_file('credentials',
|
||||
'path to Google Cloud DNS service account JSON file')
|
||||
|
||||
dns_common.validate_file_permissions(self.conf('credentials'))
|
||||
|
||||
try:
|
||||
self._get_google_client()
|
||||
except googleauth_exceptions.DefaultCredentialsError as e:
|
||||
raise errors.PluginError('Authentication using Google Application Default Credentials '
|
||||
'failed ({}). Please configure credentials using'
|
||||
' --dns-google-credentials <file>'.format(e))
|
||||
|
||||
def _perform(self, domain: str, validation_name: str, validation: str) -> None:
|
||||
self._get_google_client().add_txt_record(domain, validation_name, validation, self.ttl)
|
||||
|
||||
@@ -70,7 +76,10 @@ class Authenticator(dns_common.DNSAuthenticator):
|
||||
self._get_google_client().del_txt_record(domain, validation_name, validation, self.ttl)
|
||||
|
||||
def _get_google_client(self) -> '_GoogleClient':
|
||||
return _GoogleClient(self.conf('credentials'))
|
||||
if self.google_client is None:
|
||||
self.google_client = _GoogleClient(self.conf('credentials'), self.conf('project'))
|
||||
return self.google_client
|
||||
|
||||
|
||||
|
||||
class _GoogleClient:
|
||||
@@ -79,20 +88,31 @@ class _GoogleClient:
|
||||
"""
|
||||
|
||||
def __init__(self, account_json: Optional[str] = None,
|
||||
dns_project_id: Optional[str] = None,
|
||||
dns_api: Optional[discovery.Resource] = None) -> None:
|
||||
|
||||
scopes = ['https://www.googleapis.com/auth/ndev.clouddns.readwrite']
|
||||
credentials = None
|
||||
project_id = None
|
||||
|
||||
if account_json is not None:
|
||||
try:
|
||||
credentials = ServiceAccountCredentials.from_json_keyfile_name(account_json, scopes)
|
||||
with open(account_json) as account:
|
||||
self.project_id = json.load(account)['project_id']
|
||||
except Exception as e:
|
||||
credentials, project_id = google.auth.load_credentials_from_file(
|
||||
account_json, scopes=scopes)
|
||||
except googleauth_exceptions.GoogleAuthError as e:
|
||||
raise errors.PluginError(
|
||||
"Error parsing credentials file '{}': {}".format(account_json, e))
|
||||
"Error loading credentials file '{}': {}".format(account_json, e))
|
||||
else:
|
||||
credentials = None
|
||||
self.project_id = self.get_project_id()
|
||||
credentials, project_id = google.auth.default(scopes=scopes)
|
||||
|
||||
if dns_project_id is not None:
|
||||
project_id = dns_project_id
|
||||
|
||||
if not project_id:
|
||||
raise errors.PluginError('The Google Cloud project could not be automatically '
|
||||
'determined. Please configure it using --dns-google-project'
|
||||
' <project>.')
|
||||
self.project_id = project_id
|
||||
|
||||
if not dns_api:
|
||||
self.dns = discovery.build('dns', 'v1',
|
||||
@@ -292,26 +312,3 @@ class _GoogleClient:
|
||||
|
||||
raise errors.PluginError('Unable to determine managed zone for {0} using zone names: {1}.'
|
||||
.format(domain, zone_dns_name_guesses))
|
||||
|
||||
@staticmethod
|
||||
def get_project_id() -> str:
|
||||
"""
|
||||
Query the google metadata service for the current project ID
|
||||
|
||||
This only works on Google Cloud Platform
|
||||
|
||||
:raises ServerNotFoundError: Not running on Google Compute or DNS not available
|
||||
:raises ValueError: Server is found, but response code is not 200
|
||||
:returns: project id
|
||||
"""
|
||||
url = '{0}project/project-id'.format(METADATA_URL)
|
||||
|
||||
# Request an access token from the metadata server.
|
||||
http = httplib2.Http()
|
||||
r, content = http.request(url, headers=METADATA_HEADERS)
|
||||
if r.status != 200:
|
||||
raise ValueError("Invalid status code: {0}".format(r))
|
||||
|
||||
if isinstance(content, bytes):
|
||||
return content.decode()
|
||||
return content
|
||||
|
||||
@@ -4,10 +4,10 @@ import sys
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
from google.auth import exceptions as googleauth_exceptions
|
||||
from googleapiclient import discovery
|
||||
from googleapiclient.errors import Error
|
||||
from googleapiclient.http import HttpMock
|
||||
from httplib2 import ServerNotFoundError
|
||||
import pytest
|
||||
|
||||
from certbot import errors
|
||||
@@ -20,7 +20,7 @@ from certbot.tests import util as test_util
|
||||
ACCOUNT_JSON_PATH = '/not/a/real/path.json'
|
||||
API_ERROR = Error()
|
||||
PROJECT_ID = "test-test-1"
|
||||
|
||||
SCOPES = ['https://www.googleapis.com/auth/ndev.clouddns.readwrite']
|
||||
|
||||
class AuthenticatorTest(test_util.TempDirTestCase, dns_test_common.BaseAuthenticatorTest):
|
||||
|
||||
@@ -34,22 +34,25 @@ class AuthenticatorTest(test_util.TempDirTestCase, dns_test_common.BaseAuthentic
|
||||
|
||||
super().setUp()
|
||||
self.config = mock.MagicMock(google_credentials=path,
|
||||
google_project=PROJECT_ID,
|
||||
google_propagation_seconds=0) # don't wait during tests
|
||||
|
||||
self.auth = Authenticator(self.config, "google")
|
||||
|
||||
self.mock_client = mock.MagicMock()
|
||||
# _get_google_client | pylint: disable=protected-access
|
||||
self.auth._get_google_client = mock.MagicMock(return_value=self.mock_client)
|
||||
|
||||
@test_util.patch_display_util()
|
||||
def test_perform(self, unused_mock_get_utility):
|
||||
# _get_google_client | pylint: disable=protected-access
|
||||
self.auth._get_google_client = mock.MagicMock(return_value=self.mock_client)
|
||||
self.auth.perform([self.achall])
|
||||
|
||||
expected = [mock.call.add_txt_record(DOMAIN, '_acme-challenge.'+DOMAIN, mock.ANY, mock.ANY)]
|
||||
assert expected == self.mock_client.mock_calls
|
||||
|
||||
def test_cleanup(self):
|
||||
# _get_google_client | pylint: disable=protected-access
|
||||
self.auth._get_google_client = mock.MagicMock(return_value=self.mock_client)
|
||||
# _attempt_cleanup | pylint: disable=protected-access
|
||||
self.auth._attempt_cleanup = True
|
||||
self.auth.cleanup([self.achall])
|
||||
@@ -57,13 +60,27 @@ class AuthenticatorTest(test_util.TempDirTestCase, dns_test_common.BaseAuthentic
|
||||
expected = [mock.call.del_txt_record(DOMAIN, '_acme-challenge.'+DOMAIN, mock.ANY, mock.ANY)]
|
||||
assert expected == self.mock_client.mock_calls
|
||||
|
||||
@mock.patch('httplib2.Http.request', side_effect=ServerNotFoundError)
|
||||
@test_util.patch_display_util()
|
||||
def test_without_auth(self, unused_mock_get_utility, unused_mock):
|
||||
def test_without_auth(self, unused_mock_get_utility):
|
||||
self.auth._get_google_client = mock.MagicMock(side_effect=googleauth_exceptions.DefaultCredentialsError)
|
||||
self.config.google_credentials = None
|
||||
with pytest.raises(PluginError):
|
||||
self.auth.perform([self.achall])
|
||||
|
||||
@mock.patch('certbot_dns_google._internal.dns_google._GoogleClient')
|
||||
def test_get_google_client(self, client_mock):
|
||||
test_client = mock.MagicMock()
|
||||
client_mock.return_value = test_client
|
||||
|
||||
self.auth._get_google_client()
|
||||
assert client_mock.called
|
||||
assert self.auth.google_client is test_client
|
||||
|
||||
def test_get_google_client_cached(self):
|
||||
test_client = mock.MagicMock()
|
||||
self.auth.google_client = test_client
|
||||
assert self.auth._get_google_client() is test_client
|
||||
|
||||
|
||||
class GoogleClientTest(unittest.TestCase):
|
||||
record_name = "foo"
|
||||
@@ -81,7 +98,7 @@ class GoogleClientTest(unittest.TestCase):
|
||||
http_mock = HttpMock(discovery_file, {'status': '200'})
|
||||
dns_api = discovery.build('dns', 'v1', http=http_mock)
|
||||
|
||||
client = _GoogleClient(ACCOUNT_JSON_PATH, dns_api)
|
||||
client = _GoogleClient(ACCOUNT_JSON_PATH, None, dns_api)
|
||||
|
||||
# Setup
|
||||
mock_mz = mock.MagicMock()
|
||||
@@ -107,32 +124,67 @@ class GoogleClientTest(unittest.TestCase):
|
||||
return client, mock_changes
|
||||
|
||||
@mock.patch('googleapiclient.discovery.build')
|
||||
@mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name')
|
||||
@mock.patch('certbot_dns_google._internal.dns_google._GoogleClient.get_project_id')
|
||||
def test_client_without_credentials(self, get_project_id_mock, credential_mock,
|
||||
unused_discovery_mock):
|
||||
@mock.patch('google.auth.default')
|
||||
def test_client_with_default_credentials(self, credential_mock, discovery_mock):
|
||||
test_credentials = mock.MagicMock()
|
||||
credential_mock.return_value = (test_credentials, PROJECT_ID)
|
||||
from certbot_dns_google._internal.dns_google import _GoogleClient
|
||||
_GoogleClient(None)
|
||||
assert not credential_mock.called
|
||||
assert get_project_id_mock.called
|
||||
client = _GoogleClient(None)
|
||||
credential_mock.assert_called_once_with(scopes=SCOPES)
|
||||
assert client.project_id == PROJECT_ID
|
||||
discovery_mock.assert_called_once_with('dns', 'v1',
|
||||
credentials=test_credentials,
|
||||
cache_discovery=False)
|
||||
|
||||
@mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name')
|
||||
@mock.patch('googleapiclient.discovery.build')
|
||||
@mock.patch('google.auth.load_credentials_from_file')
|
||||
def test_client_with_json_credentials(self, credential_mock, discovery_mock):
|
||||
test_credentials = mock.MagicMock()
|
||||
credential_mock.return_value = (test_credentials, PROJECT_ID)
|
||||
from certbot_dns_google._internal.dns_google import _GoogleClient
|
||||
client = _GoogleClient(ACCOUNT_JSON_PATH)
|
||||
credential_mock.assert_called_once_with(ACCOUNT_JSON_PATH, scopes=SCOPES)
|
||||
assert credential_mock.called
|
||||
assert client.project_id == PROJECT_ID
|
||||
discovery_mock.assert_called_once_with('dns', 'v1',
|
||||
credentials=test_credentials,
|
||||
cache_discovery=False)
|
||||
|
||||
@mock.patch('google.auth.load_credentials_from_file')
|
||||
def test_client_bad_credentials_file(self, credential_mock):
|
||||
credential_mock.side_effect = ValueError('Some exception buried in oauth2client')
|
||||
credential_mock.side_effect = googleauth_exceptions.DefaultCredentialsError('Some exception buried in google.auth')
|
||||
with pytest.raises(errors.PluginError) as exc_info:
|
||||
self._setUp_client_with_mock([])
|
||||
assert str(exc_info.value) == \
|
||||
"Error parsing credentials file '/not/a/real/path.json': " \
|
||||
"Some exception buried in oauth2client"
|
||||
"Error loading credentials file '/not/a/real/path.json': " \
|
||||
"Some exception buried in google.auth"
|
||||
|
||||
@mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name')
|
||||
@mock.patch('google.auth.load_credentials_from_file')
|
||||
def test_client_missing_project_id(self, credential_mock):
|
||||
credential_mock.return_value = (mock.MagicMock(), "")
|
||||
with pytest.raises(errors.PluginError) as exc_info:
|
||||
self._setUp_client_with_mock([])
|
||||
assert str(exc_info.value) == \
|
||||
"The Google Cloud project could not be automatically determined. " \
|
||||
"Please configure it using --dns-google-project <project>."
|
||||
|
||||
@mock.patch('googleapiclient.discovery.build')
|
||||
@mock.patch('google.auth.default')
|
||||
def test_client_with_project_id(self, credential_mock, unused_discovery_mock):
|
||||
credential_mock.return_value = (mock.MagicMock(), PROJECT_ID)
|
||||
from certbot_dns_google._internal.dns_google import _GoogleClient
|
||||
client = _GoogleClient(None, "test-project-2")
|
||||
assert credential_mock.called
|
||||
assert client.project_id == "test-project-2"
|
||||
|
||||
@mock.patch('google.auth.load_credentials_from_file')
|
||||
@mock.patch('certbot_dns_google._internal.dns_google.open',
|
||||
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True)
|
||||
@mock.patch('certbot_dns_google._internal.dns_google._GoogleClient.get_project_id')
|
||||
def test_add_txt_record(self, get_project_id_mock, credential_mock):
|
||||
def test_add_txt_record(self, credential_mock):
|
||||
credential_mock.return_value = (mock.MagicMock(), PROJECT_ID)
|
||||
|
||||
client, changes = self._setUp_client_with_mock([{'managedZones': [{'id': self.zone}]}])
|
||||
credential_mock.assert_called_once_with('/not/a/real/path.json', mock.ANY)
|
||||
assert not get_project_id_mock.called
|
||||
credential_mock.assert_called_once_with('/not/a/real/path.json', scopes=SCOPES)
|
||||
|
||||
client.add_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl)
|
||||
|
||||
@@ -153,10 +205,12 @@ class GoogleClientTest(unittest.TestCase):
|
||||
managedZone=self.zone,
|
||||
project=PROJECT_ID)
|
||||
|
||||
@mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name')
|
||||
@mock.patch('google.auth.load_credentials_from_file')
|
||||
@mock.patch('certbot_dns_google._internal.dns_google.open',
|
||||
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True)
|
||||
def test_add_txt_record_and_poll(self, unused_credential_mock):
|
||||
def test_add_txt_record_and_poll(self, credential_mock):
|
||||
credential_mock.return_value = (mock.MagicMock(), PROJECT_ID)
|
||||
|
||||
client, changes = self._setUp_client_with_mock([{'managedZones': [{'id': self.zone}]}])
|
||||
changes.create.return_value.execute.return_value = {'status': 'pending', 'id': self.change}
|
||||
changes.get.return_value.execute.return_value = {'status': 'done'}
|
||||
@@ -171,10 +225,12 @@ class GoogleClientTest(unittest.TestCase):
|
||||
managedZone=self.zone,
|
||||
project=PROJECT_ID)
|
||||
|
||||
@mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name')
|
||||
@mock.patch('google.auth.load_credentials_from_file')
|
||||
@mock.patch('certbot_dns_google._internal.dns_google.open',
|
||||
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True)
|
||||
def test_add_txt_record_delete_old(self, unused_credential_mock):
|
||||
def test_add_txt_record_delete_old(self, credential_mock):
|
||||
credential_mock.return_value = (mock.MagicMock(), PROJECT_ID)
|
||||
|
||||
client, changes = self._setUp_client_with_mock(
|
||||
[{'managedZones': [{'id': self.zone}]}])
|
||||
# pylint: disable=line-too-long
|
||||
@@ -187,10 +243,12 @@ class GoogleClientTest(unittest.TestCase):
|
||||
assert "sample-txt-contents" in deletions["rrdatas"]
|
||||
assert self.record_ttl == deletions["ttl"]
|
||||
|
||||
@mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name')
|
||||
@mock.patch('google.auth.load_credentials_from_file')
|
||||
@mock.patch('certbot_dns_google._internal.dns_google.open',
|
||||
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True)
|
||||
def test_add_txt_record_delete_old_ttl_case(self, unused_credential_mock):
|
||||
def test_add_txt_record_delete_old_ttl_case(self, credential_mock):
|
||||
credential_mock.return_value = (mock.MagicMock(), PROJECT_ID)
|
||||
|
||||
client, changes = self._setUp_client_with_mock(
|
||||
[{'managedZones': [{'id': self.zone}]}])
|
||||
# pylint: disable=line-too-long
|
||||
@@ -204,49 +262,59 @@ class GoogleClientTest(unittest.TestCase):
|
||||
assert "sample-txt-contents" in deletions["rrdatas"]
|
||||
assert custom_ttl == deletions["ttl"] #otherwise HTTP 412
|
||||
|
||||
@mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name')
|
||||
@mock.patch('google.auth.load_credentials_from_file')
|
||||
@mock.patch('certbot_dns_google._internal.dns_google.open',
|
||||
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True)
|
||||
def test_add_txt_record_noop(self, unused_credential_mock):
|
||||
def test_add_txt_record_noop(self, credential_mock):
|
||||
credential_mock.return_value = (mock.MagicMock(), PROJECT_ID)
|
||||
|
||||
client, changes = self._setUp_client_with_mock(
|
||||
[{'managedZones': [{'id': self.zone}]}])
|
||||
client.add_txt_record(DOMAIN, "_acme-challenge.example.org",
|
||||
"example-txt-contents", self.record_ttl)
|
||||
assert changes.create.called is False
|
||||
|
||||
@mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name')
|
||||
@mock.patch('google.auth.load_credentials_from_file')
|
||||
@mock.patch('certbot_dns_google._internal.dns_google.open',
|
||||
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True)
|
||||
def test_add_txt_record_error_during_zone_lookup(self, unused_credential_mock):
|
||||
def test_add_txt_record_error_during_zone_lookup(self, credential_mock):
|
||||
credential_mock.return_value = (mock.MagicMock(), PROJECT_ID)
|
||||
|
||||
client, unused_changes = self._setUp_client_with_mock(API_ERROR)
|
||||
|
||||
with pytest.raises(errors.PluginError):
|
||||
client.add_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl)
|
||||
|
||||
@mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name')
|
||||
@mock.patch('google.auth.load_credentials_from_file')
|
||||
@mock.patch('certbot_dns_google._internal.dns_google.open',
|
||||
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True)
|
||||
def test_add_txt_record_zone_not_found(self, unused_credential_mock):
|
||||
def test_add_txt_record_zone_not_found(self, credential_mock):
|
||||
credential_mock.return_value = (mock.MagicMock(), PROJECT_ID)
|
||||
|
||||
client, unused_changes = self._setUp_client_with_mock([{'managedZones': []},
|
||||
{'managedZones': []}])
|
||||
|
||||
with pytest.raises(errors.PluginError):
|
||||
client.add_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl)
|
||||
|
||||
@mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name')
|
||||
@mock.patch('google.auth.load_credentials_from_file')
|
||||
@mock.patch('certbot_dns_google._internal.dns_google.open',
|
||||
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True)
|
||||
def test_add_txt_record_error_during_add(self, unused_credential_mock):
|
||||
def test_add_txt_record_error_during_add(self, credential_mock):
|
||||
credential_mock.return_value = (mock.MagicMock(), PROJECT_ID)
|
||||
|
||||
client, changes = self._setUp_client_with_mock([{'managedZones': [{'id': self.zone}]}])
|
||||
changes.create.side_effect = API_ERROR
|
||||
|
||||
with pytest.raises(errors.PluginError):
|
||||
client.add_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl)
|
||||
|
||||
@mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name')
|
||||
@mock.patch('google.auth.load_credentials_from_file')
|
||||
@mock.patch('certbot_dns_google._internal.dns_google.open',
|
||||
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True)
|
||||
def test_del_txt_record_multi_rrdatas(self, unused_credential_mock):
|
||||
def test_del_txt_record_multi_rrdatas(self, credential_mock):
|
||||
credential_mock.return_value = (mock.MagicMock(), PROJECT_ID)
|
||||
|
||||
client, changes = self._setUp_client_with_mock([{'managedZones': [{'id': self.zone}]}])
|
||||
# pylint: disable=line-too-long
|
||||
mock_get_rrs = "certbot_dns_google._internal.dns_google._GoogleClient.get_existing_txt_rrset"
|
||||
@@ -282,10 +350,12 @@ class GoogleClientTest(unittest.TestCase):
|
||||
managedZone=self.zone,
|
||||
project=PROJECT_ID)
|
||||
|
||||
@mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name')
|
||||
@mock.patch('google.auth.load_credentials_from_file')
|
||||
@mock.patch('certbot_dns_google._internal.dns_google.open',
|
||||
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True)
|
||||
def test_del_txt_record_single_rrdatas(self, unused_credential_mock):
|
||||
def test_del_txt_record_single_rrdatas(self, credential_mock):
|
||||
credential_mock.return_value = (mock.MagicMock(), PROJECT_ID)
|
||||
|
||||
client, changes = self._setUp_client_with_mock([{'managedZones': [{'id': self.zone}]}])
|
||||
# pylint: disable=line-too-long
|
||||
mock_get_rrs = "certbot_dns_google._internal.dns_google._GoogleClient.get_existing_txt_rrset"
|
||||
@@ -311,36 +381,44 @@ class GoogleClientTest(unittest.TestCase):
|
||||
managedZone=self.zone,
|
||||
project=PROJECT_ID)
|
||||
|
||||
@mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name')
|
||||
@mock.patch('google.auth.load_credentials_from_file')
|
||||
@mock.patch('certbot_dns_google._internal.dns_google.open',
|
||||
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True)
|
||||
def test_del_txt_record_error_during_zone_lookup(self, unused_credential_mock):
|
||||
def test_del_txt_record_error_during_zone_lookup(self, credential_mock):
|
||||
credential_mock.return_value = (mock.MagicMock(), PROJECT_ID)
|
||||
|
||||
client, changes = self._setUp_client_with_mock(API_ERROR)
|
||||
client.del_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl)
|
||||
changes.create.assert_not_called()
|
||||
|
||||
@mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name')
|
||||
@mock.patch('google.auth.load_credentials_from_file')
|
||||
@mock.patch('certbot_dns_google._internal.dns_google.open',
|
||||
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True)
|
||||
def test_del_txt_record_zone_not_found(self, unused_credential_mock):
|
||||
def test_del_txt_record_zone_not_found(self, credential_mock):
|
||||
credential_mock.return_value = (mock.MagicMock(), PROJECT_ID)
|
||||
|
||||
client, changes = self._setUp_client_with_mock([{'managedZones': []},
|
||||
{'managedZones': []}])
|
||||
client.del_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl)
|
||||
changes.create.assert_not_called()
|
||||
|
||||
@mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name')
|
||||
@mock.patch('google.auth.load_credentials_from_file')
|
||||
@mock.patch('certbot_dns_google._internal.dns_google.open',
|
||||
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True)
|
||||
def test_del_txt_record_error_during_delete(self, unused_credential_mock):
|
||||
def test_del_txt_record_error_during_delete(self, credential_mock):
|
||||
credential_mock.return_value = (mock.MagicMock(), PROJECT_ID)
|
||||
|
||||
client, changes = self._setUp_client_with_mock([{'managedZones': [{'id': self.zone}]}])
|
||||
changes.create.side_effect = API_ERROR
|
||||
|
||||
client.del_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl)
|
||||
|
||||
@mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name')
|
||||
@mock.patch('google.auth.load_credentials_from_file')
|
||||
@mock.patch('certbot_dns_google._internal.dns_google.open',
|
||||
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True)
|
||||
def test_get_existing_found(self, unused_credential_mock):
|
||||
def test_get_existing_found(self, credential_mock):
|
||||
credential_mock.return_value = (mock.MagicMock(), PROJECT_ID)
|
||||
|
||||
client, unused_changes = self._setUp_client_with_mock(
|
||||
[{'managedZones': [{'id': self.zone}]}])
|
||||
# Record name mocked in setUp
|
||||
@@ -348,69 +426,39 @@ class GoogleClientTest(unittest.TestCase):
|
||||
assert found["rrdatas"] == ["\"example-txt-contents\""]
|
||||
assert found["ttl"] == 60
|
||||
|
||||
@mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name')
|
||||
@mock.patch('google.auth.load_credentials_from_file')
|
||||
@mock.patch('certbot_dns_google._internal.dns_google.open',
|
||||
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True)
|
||||
def test_get_existing_not_found(self, unused_credential_mock):
|
||||
def test_get_existing_not_found(self, credential_mock):
|
||||
credential_mock.return_value = (mock.MagicMock(), PROJECT_ID)
|
||||
|
||||
client, unused_changes = self._setUp_client_with_mock(
|
||||
[{'managedZones': [{'id': self.zone}]}])
|
||||
not_found = client.get_existing_txt_rrset(self.zone, "nonexistent.tld")
|
||||
assert not_found is None
|
||||
|
||||
@mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name')
|
||||
@mock.patch('google.auth.load_credentials_from_file')
|
||||
@mock.patch('certbot_dns_google._internal.dns_google.open',
|
||||
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True)
|
||||
def test_get_existing_with_error(self, unused_credential_mock):
|
||||
def test_get_existing_with_error(self, credential_mock):
|
||||
credential_mock.return_value = (mock.MagicMock(), PROJECT_ID)
|
||||
|
||||
client, unused_changes = self._setUp_client_with_mock(
|
||||
[{'managedZones': [{'id': self.zone}]}], API_ERROR)
|
||||
# Record name mocked in setUp
|
||||
found = client.get_existing_txt_rrset(self.zone, "_acme-challenge.example.org")
|
||||
assert found is None
|
||||
|
||||
@mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name')
|
||||
@mock.patch('google.auth.load_credentials_from_file')
|
||||
@mock.patch('certbot_dns_google._internal.dns_google.open',
|
||||
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True)
|
||||
def test_get_existing_fallback(self, unused_credential_mock):
|
||||
def test_get_existing_fallback(self, credential_mock):
|
||||
credential_mock.return_value = (mock.MagicMock(), PROJECT_ID)
|
||||
|
||||
client, unused_changes = self._setUp_client_with_mock(
|
||||
[{'managedZones': [{'id': self.zone}]}], API_ERROR)
|
||||
rrset = client.get_existing_txt_rrset(self.zone, "_acme-challenge.example.org")
|
||||
assert not rrset
|
||||
|
||||
def test_get_project_id(self):
|
||||
from certbot_dns_google._internal.dns_google import _GoogleClient
|
||||
|
||||
response = DummyResponse()
|
||||
response.status = 200
|
||||
|
||||
with mock.patch('httplib2.Http.request', return_value=(response, 'test-test-1')):
|
||||
project_id = _GoogleClient.get_project_id()
|
||||
assert project_id == 'test-test-1'
|
||||
|
||||
with mock.patch('httplib2.Http.request', return_value=(response, b'test-test-1')):
|
||||
project_id = _GoogleClient.get_project_id()
|
||||
assert project_id == 'test-test-1'
|
||||
|
||||
failed_response = DummyResponse()
|
||||
failed_response.status = 404
|
||||
|
||||
with mock.patch('httplib2.Http.request',
|
||||
return_value=(failed_response, "some detailed http error response")):
|
||||
with pytest.raises(ValueError):
|
||||
_GoogleClient.get_project_id()
|
||||
|
||||
with mock.patch('httplib2.Http.request', side_effect=ServerNotFoundError):
|
||||
with pytest.raises(ServerNotFoundError):
|
||||
_GoogleClient.get_project_id()
|
||||
|
||||
|
||||
class DummyResponse:
|
||||
"""
|
||||
Dummy object to create a fake HTTPResponse (the actual one requires a socket and we only
|
||||
need the status attribute)
|
||||
"""
|
||||
def __init__(self):
|
||||
self.status = 200
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover
|
||||
|
||||
@@ -7,11 +7,9 @@ from setuptools import setup
|
||||
version = '2.6.0.dev0'
|
||||
|
||||
install_requires = [
|
||||
'google-api-python-client>=1.5.5',
|
||||
'oauth2client>=4.0',
|
||||
'google-api-python-client>=1.6.5',
|
||||
'google-auth>=2.16.0',
|
||||
'setuptools>=41.6.0',
|
||||
# already a dependency of google-api-python-client, but added for consistency
|
||||
'httplib2'
|
||||
]
|
||||
|
||||
if not os.environ.get('SNAP_BUILD'):
|
||||
|
||||
@@ -6,16 +6,27 @@ Certbot adheres to [Semantic Versioning](https://semver.org/).
|
||||
|
||||
### Added
|
||||
|
||||
* `--dns-google-project` optionally allows for specifying the project that the DNS zone(s) reside in,
|
||||
which allows for Certbot usage in scenarios where the auth credentials reside in a different
|
||||
project to the zone(s) that are being managed.
|
||||
*
|
||||
|
||||
### Changed
|
||||
|
||||
* Lineage name validity is performed for new lineages. `--cert-name` may no longer contain
|
||||
filepath separators (i.e. `/` or `\`, depending on the platform).
|
||||
* `certbot-dns-google` now loads credentials using the standard [Application Default
|
||||
Credentials](https://cloud.google.com/docs/authentication/application-default-credentials) strategy,
|
||||
rather than explicitly requiring the Google Compute metadata server to be present if a service account
|
||||
is not provided using `--dns-google-credentials`.
|
||||
* `--dns-google-credentials` now supports additional types of file-based credential, such as
|
||||
[External Account Credentials](https://google.aip.dev/auth/4117) created by Workload Identity
|
||||
Federation. All file-based credentials implemented by the Google Auth library are supported.
|
||||
*
|
||||
|
||||
### Fixed
|
||||
|
||||
*
|
||||
* `certbot-dns-google` no longer requires deprecated `oauth2client` library.
|
||||
|
||||
More details about these changes can be found on our GitHub repo.
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
apacheconfig==0.3.2 ; python_full_version < "3.8.0" and python_version >= "3.7"
|
||||
asn1crypto==0.24.0 ; python_full_version >= "3.7.0" and python_full_version < "3.8.0"
|
||||
astroid==2.15.2 ; python_full_version >= "3.7.2" and python_full_version < "3.8.0"
|
||||
attrs==22.2.0 ; python_version >= "3.7" and python_full_version < "3.8.0"
|
||||
boto3==1.15.15 ; python_full_version < "3.8.0" and python_version >= "3.7"
|
||||
botocore==1.18.15 ; python_full_version < "3.8.0" and python_version >= "3.7"
|
||||
cachetools==5.3.0 ; python_version >= "3.7" and python_full_version < "3.8.0"
|
||||
certifi==2022.12.7 ; python_full_version < "3.8.0" and python_version >= "3.7"
|
||||
cffi==1.11.5 ; python_full_version < "3.8.0" and python_version >= "3.7"
|
||||
chardet==3.0.4 ; python_full_version < "3.8.0" and python_version >= "3.7"
|
||||
@@ -26,10 +26,11 @@ execnet==1.9.0 ; python_version >= "3.7" and python_full_version < "3.8.0"
|
||||
filelock==3.11.0 ; python_version >= "3.7" and python_full_version < "3.8.0"
|
||||
funcsigs==0.4 ; python_full_version >= "3.7.0" and python_full_version < "3.8.0"
|
||||
future==0.18.3 ; python_full_version < "3.8.0" and python_version >= "3.7"
|
||||
google-api-python-client==1.5.5 ; python_full_version < "3.8.0" and python_version >= "3.7"
|
||||
google-api-python-client==1.6.5 ; python_full_version < "3.8.0" and python_version >= "3.7"
|
||||
google-auth==2.16.0 ; python_full_version < "3.8.0" and python_version >= "3.7"
|
||||
httplib2==0.9.2 ; python_full_version < "3.8.0" and python_version >= "3.7"
|
||||
idna==2.6 ; python_full_version < "3.8.0" and python_version >= "3.7"
|
||||
importlib-metadata==6.1.0 ; python_version >= "3.7" and python_version < "3.8"
|
||||
importlib-metadata==6.4.1 ; python_version >= "3.7" and python_version < "3.8"
|
||||
iniconfig==2.0.0 ; python_version >= "3.7" and python_full_version < "3.8.0"
|
||||
ipaddress==1.0.16 ; python_full_version >= "3.7.0" and python_full_version < "3.8.0"
|
||||
isort==5.11.5 ; python_full_version >= "3.7.2" and python_full_version < "3.8.0"
|
||||
@@ -41,17 +42,17 @@ mccabe==0.7.0 ; python_full_version >= "3.7.2" and python_full_version < "3.8.0"
|
||||
mypy-extensions==1.0.0 ; python_version >= "3.7" and python_full_version < "3.8.0"
|
||||
mypy==1.2.0 ; python_version >= "3.7" and python_full_version < "3.8.0"
|
||||
ndg-httpsclient==0.3.2 ; python_full_version >= "3.7.0" and python_full_version < "3.8.0"
|
||||
oauth2client==4.0.0 ; python_full_version < "3.8.0" and python_version >= "3.7"
|
||||
packaging==23.0 ; python_version >= "3.7" and python_full_version < "3.8.0"
|
||||
oauth2client==4.1.3 ; python_full_version < "3.8.0" and python_version >= "3.7"
|
||||
packaging==23.1 ; python_version >= "3.7" and python_full_version < "3.8.0"
|
||||
parsedatetime==2.4 ; python_full_version < "3.8.0" and python_version >= "3.7"
|
||||
pbr==1.8.0 ; python_full_version >= "3.7.0" and python_full_version < "3.8.0"
|
||||
pip==23.0.1 ; python_version >= "3.7" and python_full_version < "3.8.0"
|
||||
pip==23.1 ; python_version >= "3.7" and python_full_version < "3.8.0"
|
||||
platformdirs==3.2.0 ; python_full_version < "3.8.0" and python_version >= "3.7"
|
||||
pluggy==1.0.0 ; python_version >= "3.7" and python_full_version < "3.8.0"
|
||||
ply==3.4 ; python_full_version < "3.8.0" and python_version >= "3.7"
|
||||
py==1.11.0 ; python_version >= "3.7" and python_full_version < "3.8.0"
|
||||
pyasn1-modules==0.0.10 ; python_full_version < "3.8.0" and python_version >= "3.7"
|
||||
pyasn1==0.1.9 ; python_full_version < "3.8.0" and python_version >= "3.7"
|
||||
pyasn1-modules==0.2.8 ; python_full_version < "3.8.0" and python_version >= "3.7"
|
||||
pyasn1==0.4.8 ; python_full_version < "3.8.0" and python_version >= "3.7"
|
||||
pycparser==2.14 ; python_full_version < "3.8.0" and python_version >= "3.7"
|
||||
pylint==2.17.2 ; python_full_version >= "3.7.2" and python_full_version < "3.8.0"
|
||||
pyopenssl==17.5.0 ; python_full_version < "3.8.0" and python_version >= "3.7"
|
||||
@@ -59,7 +60,7 @@ pyparsing==2.2.1 ; python_full_version < "3.8.0" and python_version >= "3.7"
|
||||
pyrfc3339==1.0 ; python_full_version < "3.8.0" and python_version >= "3.7"
|
||||
pytest-cov==4.0.0 ; python_version >= "3.7" and python_full_version < "3.8.0"
|
||||
pytest-xdist==3.2.1 ; python_version >= "3.7" and python_full_version < "3.8.0"
|
||||
pytest==7.2.2 ; python_version >= "3.7" and python_full_version < "3.8.0"
|
||||
pytest==7.3.1 ; python_version >= "3.7" and python_full_version < "3.8.0"
|
||||
python-augeas==0.5.0 ; python_full_version < "3.8.0" and python_version >= "3.7"
|
||||
python-dateutil==2.8.2 ; python_full_version < "3.8.0" and python_version >= "3.7"
|
||||
python-digitalocean==1.11 ; python_full_version < "3.8.0" and python_version >= "3.7"
|
||||
@@ -78,10 +79,12 @@ tomlkit==0.11.7 ; python_full_version >= "3.7.2" and python_full_version < "3.8.
|
||||
tox==1.9.2 ; python_version >= "3.7" and python_full_version < "3.8.0"
|
||||
typed-ast==1.5.4 ; python_version < "3.8" and python_version >= "3.7"
|
||||
types-cryptography==3.3.23.2 ; python_version >= "3.7" and python_full_version < "3.8.0"
|
||||
types-httplib2==0.22.0.1 ; python_version >= "3.7" and python_full_version < "3.8.0"
|
||||
types-pyopenssl==23.0.0.0 ; python_version >= "3.7" and python_full_version < "3.8.0"
|
||||
types-pyrfc3339==1.1.1.4 ; python_version >= "3.7" and python_full_version < "3.8.0"
|
||||
types-python-dateutil==2.8.19.12 ; python_version >= "3.7" and python_full_version < "3.8.0"
|
||||
types-pytz==2023.3.0.0 ; python_version >= "3.7" and python_full_version < "3.8.0"
|
||||
types-pywin32==306.0.0.1 ; python_version >= "3.7" and python_full_version < "3.8.0"
|
||||
types-requests==2.28.11.17 ; python_version >= "3.7" and python_full_version < "3.8.0"
|
||||
types-setuptools==67.6.0.7 ; python_version >= "3.7" and python_full_version < "3.8.0"
|
||||
types-six==1.16.21.8 ; python_version >= "3.7" and python_full_version < "3.8.0"
|
||||
|
||||
@@ -57,18 +57,18 @@ distro = "1.0.1"
|
||||
dns-lexicon = "3.2.1"
|
||||
dnspython = "1.15.0"
|
||||
funcsigs = "0.4"
|
||||
google-api-python-client = "1.5.5"
|
||||
google-api-python-client = "1.6.5"
|
||||
google-auth = "2.16.0"
|
||||
httplib2 = "0.9.2"
|
||||
idna = "2.6"
|
||||
ipaddress = "1.0.16"
|
||||
ndg-httpsclient = "0.3.2"
|
||||
oauth2client = "4.0.0"
|
||||
parsedatetime = "2.4"
|
||||
pbr = "1.8.0"
|
||||
ply = "3.4"
|
||||
pyOpenSSL = "17.5.0"
|
||||
pyRFC3339 = "1.0"
|
||||
pyasn1 = "0.1.9"
|
||||
pyasn1 = "0.4.8"
|
||||
pycparser = "2.14"
|
||||
pyparsing = "2.2.1"
|
||||
python-augeas = "0.5.0"
|
||||
|
||||
@@ -15,10 +15,10 @@ babel==2.12.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
backcall==0.2.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
backports-cached-property==1.0.2 ; python_version >= "3.7" and python_version < "3.8"
|
||||
bcrypt==4.0.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
beautifulsoup4==4.12.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
beautifulsoup4==4.12.2 ; python_version >= "3.7" and python_version < "4.0"
|
||||
bleach==6.0.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
boto3==1.26.105 ; python_version >= "3.7" and python_version < "4.0"
|
||||
botocore==1.29.105 ; python_version >= "3.7" and python_version < "4.0"
|
||||
boto3==1.26.113 ; python_version >= "3.7" and python_version < "4.0"
|
||||
botocore==1.29.113 ; python_version >= "3.7" and python_version < "4.0"
|
||||
cachecontrol==0.12.11 ; python_version >= "3.7" and python_version < "4.0"
|
||||
cachetools==5.3.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
cachy==0.3.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
@@ -30,7 +30,7 @@ cloudflare==2.11.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
colorama==0.4.6 ; python_version < "4.0" and sys_platform == "win32" and python_version >= "3.7" or python_version >= "3.7" and python_version < "4.0" and platform_system == "Windows"
|
||||
configargparse==1.5.3 ; python_version >= "3.7" and python_version < "4.0"
|
||||
configobj==5.0.8 ; python_version >= "3.7" and python_version < "4.0"
|
||||
coverage==7.2.2 ; python_version >= "3.7" and python_version < "4.0"
|
||||
coverage==7.2.3 ; python_version >= "3.7" and python_version < "4.0"
|
||||
crashtest==0.3.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
cryptography==40.0.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
cython==0.29.34 ; python_version >= "3.7" and python_version < "4.0"
|
||||
@@ -45,11 +45,11 @@ dulwich==0.20.50 ; python_version >= "3.7" and python_version < "4.0"
|
||||
exceptiongroup==1.1.1 ; python_version >= "3.7" and python_version < "3.11"
|
||||
execnet==1.9.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
fabric==3.0.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
filelock==3.10.7 ; python_version >= "3.7" and python_version < "4.0"
|
||||
filelock==3.11.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
google-api-core==2.11.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
google-api-python-client==2.83.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
google-api-python-client==2.85.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
google-auth-httplib2==0.1.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
google-auth==2.17.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
google-auth==2.17.3 ; python_version >= "3.7" and python_version < "4.0"
|
||||
googleapis-common-protos==1.59.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
html5lib==1.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
httplib2==0.22.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
@@ -84,10 +84,9 @@ more-itertools==9.1.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
msgpack==1.0.5 ; python_version >= "3.7" and python_version < "4.0"
|
||||
msrest==0.6.21 ; python_version >= "3.7" and python_version < "4.0"
|
||||
mypy-extensions==1.0.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
mypy==1.1.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
oauth2client==4.1.3 ; python_version >= "3.7" and python_version < "4.0"
|
||||
mypy==1.2.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
oauthlib==3.2.2 ; python_version >= "3.7" and python_version < "4.0"
|
||||
packaging==23.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
packaging==23.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
paramiko==3.1.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
parsedatetime==2.6 ; python_version >= "3.7" and python_version < "4.0"
|
||||
parso==0.8.3 ; python_version >= "3.7" and python_version < "4.0"
|
||||
@@ -103,13 +102,13 @@ poetry-core==1.3.2 ; python_version >= "3.7" and python_version < "4.0"
|
||||
poetry-plugin-export==1.2.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
poetry==1.2.2 ; python_version >= "3.7" and python_version < "4.0"
|
||||
prompt-toolkit==3.0.38 ; python_version >= "3.7" and python_version < "4.0"
|
||||
protobuf==4.22.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
protobuf==4.22.3 ; python_version >= "3.7" and python_version < "4.0"
|
||||
ptyprocess==0.7.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
py==1.11.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pyasn1-modules==0.2.8 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pyasn1==0.4.8 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pycparser==2.21 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pygments==2.14.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pygments==2.15.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pylev==1.4.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pylint==2.15.5 ; python_full_version >= "3.7.2" and python_version < "4.0"
|
||||
pynacl==1.5.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
@@ -120,7 +119,7 @@ pyrfc3339==1.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pyrsistent==0.19.3 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pytest-cov==4.0.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pytest-xdist==3.2.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pytest==7.2.2 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pytest==7.3.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
python-augeas==1.1.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
python-dateutil==2.8.2 ; python_version >= "3.7" and python_version < "4.0"
|
||||
python-digitalocean==1.17.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
@@ -135,7 +134,7 @@ requests-oauthlib==1.3.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
requests-toolbelt==0.9.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
requests==2.28.2 ; python_version >= "3.7" and python_version < "4"
|
||||
rfc3986==2.0.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
rich==13.3.3 ; python_version >= "3.7" and python_version < "4.0"
|
||||
rich==13.3.4 ; python_version >= "3.7" and python_version < "4.0"
|
||||
rsa==4.9 ; python_version >= "3.7" and python_version < "4"
|
||||
s3transfer==0.6.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
secretstorage==3.3.3 ; python_version >= "3.7" and python_version < "4.0" and sys_platform == "linux"
|
||||
@@ -163,7 +162,7 @@ traitlets==5.9.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
twine==4.0.2 ; python_version >= "3.7" and python_version < "4.0"
|
||||
typed-ast==1.5.4 ; python_version < "3.8" and python_version >= "3.7"
|
||||
types-httplib2==0.22.0.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
types-pyopenssl==23.1.0.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
types-pyopenssl==23.1.0.2 ; python_version >= "3.7" and python_version < "4.0"
|
||||
types-pyrfc3339==1.1.1.4 ; python_version >= "3.7" and python_version < "4.0"
|
||||
types-python-dateutil==2.8.19.12 ; python_version >= "3.7" and python_version < "4.0"
|
||||
types-pytz==2023.3.0.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
|
||||
Reference in New Issue
Block a user