New upstream version 2.0.0
Harlan Lieberman-Berg
1 year, 5 months ago
2 | 2 | recursive-include docs * |
3 | 3 | recursive-include certbot_dns_google/testdata * |
4 | 4 | recursive-include tests * |
5 | include certbot_dns_google/py.typed | |
5 | 6 | global-exclude __pycache__ |
6 | 7 | global-exclude *.py[cod] |
0 | 0 | Metadata-Version: 2.1 |
1 | 1 | Name: certbot-dns-google |
2 | Version: 1.21.0 | |
2 | Version: 2.0.0 | |
3 | 3 | Summary: Google Cloud DNS Authenticator plugin for Certbot |
4 | 4 | Home-page: https://github.com/certbot/certbot |
5 | 5 | Author: Certbot Project |
6 | 6 | Author-email: certbot-dev@eff.org |
7 | 7 | License: Apache License 2.0 |
8 | Platform: UNKNOWN | |
9 | 8 | Classifier: Development Status :: 5 - Production/Stable |
10 | 9 | Classifier: Environment :: Plugins |
11 | 10 | Classifier: Intended Audience :: System Administrators |
13 | 12 | Classifier: Operating System :: POSIX :: Linux |
14 | 13 | Classifier: Programming Language :: Python |
15 | 14 | Classifier: Programming Language :: Python :: 3 |
16 | Classifier: Programming Language :: Python :: 3.6 | |
17 | 15 | Classifier: Programming Language :: Python :: 3.7 |
18 | 16 | Classifier: Programming Language :: Python :: 3.8 |
19 | 17 | Classifier: Programming Language :: Python :: 3.9 |
18 | Classifier: Programming Language :: Python :: 3.10 | |
19 | Classifier: Programming Language :: Python :: 3.11 | |
20 | 20 | Classifier: Topic :: Internet :: WWW/HTTP |
21 | 21 | Classifier: Topic :: Security |
22 | 22 | Classifier: Topic :: System :: Installation/Setup |
23 | 23 | Classifier: Topic :: System :: Networking |
24 | 24 | Classifier: Topic :: System :: Systems Administration |
25 | 25 | Classifier: Topic :: Utilities |
26 | Requires-Python: >=3.6 | |
26 | Requires-Python: >=3.7 | |
27 | 27 | Provides-Extra: docs |
28 | 28 | License-File: LICENSE.txt |
29 | ||
30 | UNKNOWN | |
31 |
29 | 29 | |
30 | 30 | * ``dns.changes.create`` |
31 | 31 | * ``dns.changes.get`` |
32 | * ``dns.changes.list`` | |
33 | * ``dns.managedZones.get`` | |
32 | 34 | * ``dns.managedZones.list`` |
33 | 35 | * ``dns.resourceRecordSets.create`` |
34 | 36 | * ``dns.resourceRecordSets.delete`` |
0 | 0 | """DNS Authenticator for Google Cloud DNS.""" |
1 | 1 | import json |
2 | 2 | import logging |
3 | from typing import Any | |
4 | from typing import Callable | |
5 | from typing import Dict | |
6 | from typing import Optional | |
3 | 7 | |
4 | 8 | from googleapiclient import discovery |
5 | 9 | from googleapiclient import errors as googleapiclient_errors |
28 | 32 | ttl = 60 |
29 | 33 | |
30 | 34 | @classmethod |
31 | def add_parser_arguments(cls, add): # pylint: disable=arguments-differ | |
35 | def add_parser_arguments(cls, add: Callable[..., None], | |
36 | default_propagation_seconds: int = 60) -> None: | |
32 | 37 | super().add_parser_arguments(add, default_propagation_seconds=60) |
33 | 38 | add('credentials', |
34 | 39 | help=('Path to Google Cloud DNS service account JSON file. (See {0} for' + |
36 | 41 | 'required permissions.)').format(ACCT_URL, PERMISSIONS_URL), |
37 | 42 | default=None) |
38 | 43 | |
39 | def more_info(self): # pylint: disable=missing-function-docstring | |
44 | def more_info(self) -> str: | |
40 | 45 | return 'This plugin configures a DNS TXT record to respond to a dns-01 challenge using ' + \ |
41 | 46 | 'the Google Cloud DNS API.' |
42 | 47 | |
43 | def _setup_credentials(self): | |
48 | def _setup_credentials(self) -> None: | |
44 | 49 | if self.conf('credentials') is None: |
45 | 50 | try: |
46 | 51 | # use project_id query to check for availability of google metadata server |
57 | 62 | |
58 | 63 | dns_common.validate_file_permissions(self.conf('credentials')) |
59 | 64 | |
60 | def _perform(self, domain, validation_name, validation): | |
65 | def _perform(self, domain: str, validation_name: str, validation: str) -> None: | |
61 | 66 | self._get_google_client().add_txt_record(domain, validation_name, validation, self.ttl) |
62 | 67 | |
63 | def _cleanup(self, domain, validation_name, validation): | |
68 | def _cleanup(self, domain: str, validation_name: str, validation: str) -> None: | |
64 | 69 | self._get_google_client().del_txt_record(domain, validation_name, validation, self.ttl) |
65 | 70 | |
66 | def _get_google_client(self): | |
71 | def _get_google_client(self) -> '_GoogleClient': | |
67 | 72 | return _GoogleClient(self.conf('credentials')) |
68 | 73 | |
69 | 74 | |
72 | 77 | Encapsulates all communication with the Google Cloud DNS API. |
73 | 78 | """ |
74 | 79 | |
75 | def __init__(self, account_json=None, dns_api=None): | |
80 | def __init__(self, account_json: Optional[str] = None, | |
81 | dns_api: Optional[discovery.Resource] = None) -> None: | |
76 | 82 | |
77 | 83 | scopes = ['https://www.googleapis.com/auth/ndev.clouddns.readwrite'] |
78 | 84 | if account_json is not None: |
94 | 100 | else: |
95 | 101 | self.dns = dns_api |
96 | 102 | |
97 | def add_txt_record(self, domain, record_name, record_content, record_ttl): | |
103 | def add_txt_record(self, domain: str, record_name: str, record_content: str, | |
104 | record_ttl: int) -> None: | |
98 | 105 | """ |
99 | 106 | Add a TXT record using the supplied information. |
100 | 107 | |
109 | 116 | |
110 | 117 | record_contents = self.get_existing_txt_rrset(zone_id, record_name) |
111 | 118 | if record_contents is None: |
112 | # If it wasn't possible to fetch the records at this label (missing .list permission), | |
113 | # assume there aren't any (#5678). If there are actually records here, this will fail | |
114 | # with HTTP 409/412 API errors. | |
119 | # If it wasn't possible to fetch the records at this label (missing .list permission), | |
120 | # assume there aren't any (#5678). If there are actually records here, this will fail | |
121 | # with HTTP 409/412 API errors. | |
115 | 122 | record_contents = {"rrdatas": []} |
116 | 123 | add_records = record_contents["rrdatas"][:] |
117 | 124 | |
163 | 170 | raise errors.PluginError('Error communicating with the Google Cloud DNS API: {0}' |
164 | 171 | .format(e)) |
165 | 172 | |
166 | def del_txt_record(self, domain, record_name, record_content, record_ttl): | |
173 | def del_txt_record(self, domain: str, record_name: str, record_content: str, | |
174 | record_ttl: int) -> None: | |
167 | 175 | """ |
168 | 176 | Delete a TXT record using the supplied information. |
169 | 177 | |
223 | 231 | except googleapiclient_errors.Error as e: |
224 | 232 | logger.warning('Encountered error deleting TXT record: %s', e) |
225 | 233 | |
226 | def get_existing_txt_rrset(self, zone_id, record_name): | |
234 | def get_existing_txt_rrset(self, zone_id: str, record_name: str) -> Optional[Dict[str, Any]]: | |
227 | 235 | """ |
228 | 236 | Get existing TXT records from the RRset for the record name. |
229 | 237 | |
253 | 261 | return response["rrsets"][0] |
254 | 262 | return None |
255 | 263 | |
256 | def _find_managed_zone_id(self, domain): | |
264 | def _find_managed_zone_id(self, domain: str) -> str: | |
257 | 265 | """ |
258 | 266 | Find the managed zone for a given domain. |
259 | 267 | |
285 | 293 | .format(domain, zone_dns_name_guesses)) |
286 | 294 | |
287 | 295 | @staticmethod |
288 | def get_project_id(): | |
296 | def get_project_id() -> str: | |
289 | 297 | """ |
290 | 298 | Query the google metadata service for the current project ID |
291 | 299 |
0 | 0 | Metadata-Version: 2.1 |
1 | 1 | Name: certbot-dns-google |
2 | Version: 1.21.0 | |
2 | Version: 2.0.0 | |
3 | 3 | Summary: Google Cloud DNS Authenticator plugin for Certbot |
4 | 4 | Home-page: https://github.com/certbot/certbot |
5 | 5 | Author: Certbot Project |
6 | 6 | Author-email: certbot-dev@eff.org |
7 | 7 | License: Apache License 2.0 |
8 | Platform: UNKNOWN | |
9 | 8 | Classifier: Development Status :: 5 - Production/Stable |
10 | 9 | Classifier: Environment :: Plugins |
11 | 10 | Classifier: Intended Audience :: System Administrators |
13 | 12 | Classifier: Operating System :: POSIX :: Linux |
14 | 13 | Classifier: Programming Language :: Python |
15 | 14 | Classifier: Programming Language :: Python :: 3 |
16 | Classifier: Programming Language :: Python :: 3.6 | |
17 | 15 | Classifier: Programming Language :: Python :: 3.7 |
18 | 16 | Classifier: Programming Language :: Python :: 3.8 |
19 | 17 | Classifier: Programming Language :: Python :: 3.9 |
18 | Classifier: Programming Language :: Python :: 3.10 | |
19 | Classifier: Programming Language :: Python :: 3.11 | |
20 | 20 | Classifier: Topic :: Internet :: WWW/HTTP |
21 | 21 | Classifier: Topic :: Security |
22 | 22 | Classifier: Topic :: System :: Installation/Setup |
23 | 23 | Classifier: Topic :: System :: Networking |
24 | 24 | Classifier: Topic :: System :: Systems Administration |
25 | 25 | Classifier: Topic :: Utilities |
26 | Requires-Python: >=3.6 | |
26 | Requires-Python: >=3.7 | |
27 | 27 | Provides-Extra: docs |
28 | 28 | License-File: LICENSE.txt |
29 | ||
30 | UNKNOWN | |
31 |
0 | 0 | LICENSE.txt |
1 | 1 | MANIFEST.in |
2 | 2 | README.rst |
3 | setup.cfg | |
4 | 3 | setup.py |
5 | 4 | certbot_dns_google/__init__.py |
5 | certbot_dns_google/py.typed | |
6 | 6 | certbot_dns_google.egg-info/PKG-INFO |
7 | 7 | certbot_dns_google.egg-info/SOURCES.txt |
8 | 8 | certbot_dns_google.egg-info/dependency_links.txt |
0 | 0 | google-api-python-client>=1.5.5 |
1 | 1 | oauth2client>=4.0 |
2 | setuptools>=39.0.1 | |
2 | setuptools>=41.6.0 | |
3 | 3 | httplib2 |
4 | acme>=1.21.0 | |
5 | certbot>=1.21.0 | |
4 | acme>=2.0.0 | |
5 | certbot>=2.0.0 | |
6 | 6 | |
7 | 7 | [docs] |
8 | 8 | Sphinx>=1.0 |
176 | 176 | intersphinx_mapping = { |
177 | 177 | 'python': ('https://docs.python.org/', None), |
178 | 178 | 'acme': ('https://acme-python.readthedocs.org/en/latest/', None), |
179 | 'certbot': ('https://certbot.eff.org/docs/', None), | |
179 | 'certbot': ('https://eff-certbot.readthedocs.io/en/stable/', None), | |
180 | 180 | } |
3 | 3 | from setuptools import find_packages |
4 | 4 | from setuptools import setup |
5 | 5 | |
6 | version = '1.21.0' | |
6 | version = '2.0.0' | |
7 | 7 | |
8 | 8 | install_requires = [ |
9 | 9 | 'google-api-python-client>=1.5.5', |
10 | 10 | 'oauth2client>=4.0', |
11 | 'setuptools>=39.0.1', | |
11 | 'setuptools>=41.6.0', | |
12 | 12 | # already a dependency of google-api-python-client, but added for consistency |
13 | 13 | 'httplib2' |
14 | 14 | ] |
40 | 40 | author="Certbot Project", |
41 | 41 | author_email='certbot-dev@eff.org', |
42 | 42 | license='Apache License 2.0', |
43 | python_requires='>=3.6', | |
43 | python_requires='>=3.7', | |
44 | 44 | classifiers=[ |
45 | 45 | 'Development Status :: 5 - Production/Stable', |
46 | 46 | 'Environment :: Plugins', |
49 | 49 | 'Operating System :: POSIX :: Linux', |
50 | 50 | 'Programming Language :: Python', |
51 | 51 | 'Programming Language :: Python :: 3', |
52 | 'Programming Language :: Python :: 3.6', | |
53 | 52 | 'Programming Language :: Python :: 3.7', |
54 | 53 | 'Programming Language :: Python :: 3.8', |
55 | 54 | 'Programming Language :: Python :: 3.9', |
55 | 'Programming Language :: Python :: 3.10', | |
56 | 'Programming Language :: Python :: 3.11', | |
56 | 57 | 'Topic :: Internet :: WWW/HTTP', |
57 | 58 | 'Topic :: Security', |
58 | 59 | 'Topic :: System :: Installation/Setup', |
5 | 5 | from googleapiclient.errors import Error |
6 | 6 | from googleapiclient.http import HttpMock |
7 | 7 | from httplib2 import ServerNotFoundError |
8 | try: | |
9 | import mock | |
10 | except ImportError: # pragma: no cover | |
11 | from unittest import mock # type: ignore | |
8 | ||
9 | from unittest import mock | |
12 | 10 | |
13 | 11 | from certbot import errors |
14 | 12 | from certbot.compat import os |
183 | 181 | with mock.patch(mock_get_rrs) as mock_rrs: |
184 | 182 | mock_rrs.return_value = {"rrdatas": ["sample-txt-contents"], "ttl": self.record_ttl} |
185 | 183 | client.add_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl) |
186 | self.assertTrue(changes.create.called) | |
184 | self.assertIs(changes.create.called, True) | |
187 | 185 | deletions = changes.create.call_args_list[0][1]["body"]["deletions"][0] |
188 | self.assertTrue("sample-txt-contents" in deletions["rrdatas"]) | |
186 | self.assertIn("sample-txt-contents", deletions["rrdatas"]) | |
189 | 187 | self.assertEqual(self.record_ttl, deletions["ttl"]) |
190 | 188 | |
191 | 189 | @mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name') |
200 | 198 | custom_ttl = 300 |
201 | 199 | mock_rrs.return_value = {"rrdatas": ["sample-txt-contents"], "ttl": custom_ttl} |
202 | 200 | client.add_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl) |
203 | self.assertTrue(changes.create.called) | |
201 | self.assertIs(changes.create.called, True) | |
204 | 202 | deletions = changes.create.call_args_list[0][1]["body"]["deletions"][0] |
205 | self.assertTrue("sample-txt-contents" in deletions["rrdatas"]) | |
203 | self.assertIn("sample-txt-contents", deletions["rrdatas"]) | |
206 | 204 | self.assertEqual(custom_ttl, deletions["ttl"]) #otherwise HTTP 412 |
207 | 205 | |
208 | 206 | @mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name') |
213 | 211 | [{'managedZones': [{'id': self.zone}]}]) |
214 | 212 | client.add_txt_record(DOMAIN, "_acme-challenge.example.org", |
215 | 213 | "example-txt-contents", self.record_ttl) |
216 | self.assertFalse(changes.create.called) | |
214 | self.assertIs(changes.create.called, False) | |
217 | 215 | |
218 | 216 | @mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name') |
219 | 217 | @mock.patch('certbot_dns_google._internal.dns_google.open', |
356 | 354 | client, unused_changes = self._setUp_client_with_mock( |
357 | 355 | [{'managedZones': [{'id': self.zone}]}]) |
358 | 356 | not_found = client.get_existing_txt_rrset(self.zone, "nonexistent.tld") |
359 | self.assertEqual(not_found, None) | |
357 | self.assertIsNone(not_found) | |
360 | 358 | |
361 | 359 | @mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name') |
362 | 360 | @mock.patch('certbot_dns_google._internal.dns_google.open', |
366 | 364 | [{'managedZones': [{'id': self.zone}]}], API_ERROR) |
367 | 365 | # Record name mocked in setUp |
368 | 366 | found = client.get_existing_txt_rrset(self.zone, "_acme-challenge.example.org") |
369 | self.assertEqual(found, None) | |
367 | self.assertIsNone(found) | |
370 | 368 | |
371 | 369 | @mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name') |
372 | 370 | @mock.patch('certbot_dns_google._internal.dns_google.open', |
410 | 408 | def __init__(self): |
411 | 409 | self.status = 200 |
412 | 410 | |
411 | ||
413 | 412 | if __name__ == "__main__": |
414 | 413 | unittest.main() # pragma: no cover |