Hello community, here is the log from the commit of package python-certbot-dns-cloudflare for openSUSE:Leap:15.2 checked in at 2020-02-27 06:41:49 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Leap:15.2/python-certbot-dns-cloudflare (Old) and /work/SRC/openSUSE:Leap:15.2/.python-certbot-dns-cloudflare.new.26092 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-certbot-dns-cloudflare" Thu Feb 27 06:41:49 2020 rev:6 rq:779447 version:1.2.0 Changes: -------- --- /work/SRC/openSUSE:Leap:15.2/python-certbot-dns-cloudflare/python-certbot-dns-cloudflare.changes 2020-02-19 18:48:30.223017941 +0100 +++ /work/SRC/openSUSE:Leap:15.2/.python-certbot-dns-cloudflare.new.26092/python-certbot-dns-cloudflare.changes 2020-02-27 06:41:50.961652849 +0100 @@ -1,0 +2,6 @@ +Fri Feb 21 15:32:45 UTC 2020 - Marketa Calabkova <mcalabk...@suse.com> + +- update to version 1.2.0 + * Added support for Cloudflare's limited-scope API Tokens + +------------------------------------------------------------------- Old: ---- certbot-dns-cloudflare-1.1.0.tar.gz New: ---- certbot-dns-cloudflare-1.2.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-certbot-dns-cloudflare.spec ++++++ --- /var/tmp/diff_new_pack.AAPEQA/_old 2020-02-27 06:41:51.241653432 +0100 +++ /var/tmp/diff_new_pack.AAPEQA/_new 2020-02-27 06:41:51.241653432 +0100 @@ -18,7 +18,7 @@ %{?!python_module:%define python_module() python-%{**} python3-%{**}} Name: python-certbot-dns-cloudflare -Version: 1.1.0 +Version: 1.2.0 Release: 0 Summary: Cloudflare Authenticator plugin for Certbot License: Apache-2.0 ++++++ certbot-dns-cloudflare-1.1.0.tar.gz -> certbot-dns-cloudflare-1.2.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-dns-cloudflare-1.1.0/PKG-INFO new/certbot-dns-cloudflare-1.2.0/PKG-INFO --- old/certbot-dns-cloudflare-1.1.0/PKG-INFO 2020-01-14 19:41:51.000000000 +0100 +++ new/certbot-dns-cloudflare-1.2.0/PKG-INFO 2020-02-04 22:47:10.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: certbot-dns-cloudflare -Version: 1.1.0 +Version: 1.2.0 Summary: Cloudflare DNS Authenticator plugin for Certbot Home-page: https://github.com/certbot/certbot Author: Certbot Project @@ -17,7 +17,6 @@ Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 @@ -28,5 +27,5 @@ Classifier: Topic :: System :: Networking Classifier: Topic :: System :: Systems Administration Classifier: Topic :: Utilities -Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* +Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.* Provides-Extra: docs diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-dns-cloudflare-1.1.0/certbot_dns_cloudflare/__init__.py new/certbot-dns-cloudflare-1.2.0/certbot_dns_cloudflare/__init__.py --- old/certbot-dns-cloudflare-1.1.0/certbot_dns_cloudflare/__init__.py 2020-01-14 19:41:31.000000000 +0100 +++ new/certbot-dns-cloudflare-1.2.0/certbot_dns_cloudflare/__init__.py 2020-02-04 22:46:57.000000000 +0100 @@ -22,17 +22,40 @@ Use of this plugin requires a configuration file containing Cloudflare API credentials, obtained from your Cloudflare -`account page <https://www.cloudflare.com/a/account/my-account>`_. This plugin -does not currently support Cloudflare's "API Tokens", so please ensure you use -the "Global API Key" for authentication. +`account page <https://dash.cloudflare.com/profile/api-tokens>`_. + +Previously, Cloudflare's "Global API Key" was used for authentication, however +this key can access the entire Cloudflare API for all domains in your account, +meaning it could cause a lot of damage if leaked. + +Cloudflare's newer API Tokens can be restricted to specific domains and +operations, and are therefore now the recommended authentication option. + +However, due to some shortcomings in Cloudflare's implementation of Tokens, +Tokens created for Certbot currently require ``Zone:Zone:Read`` and ``Zone:DNS:Edit`` +permissions for **all** zones in your account. While this is not ideal, your Token +will still have fewer permission than the Global key, so it's still worth doing. +Hopefully Cloudflare will improve this in the future. + +Using Cloudflare Tokens also requires at least version 2.3.1 of the ``cloudflare`` +python module. If the version that automatically installed with this plugin is +older than that, and you can't upgrade it on your system, you'll have to stick to +the Global key. + +.. code-block:: ini + :name: certbot_cloudflare_token.ini + :caption: Example credentials file using restricted API Token (recommended): + + # Cloudflare API token used by Certbot + dns_cloudflare_api_token = 0123456789abcdef0123456789abcdef01234567 .. code-block:: ini - :name: credentials.ini - :caption: Example credentials file: + :name: certbot_cloudflare_key.ini + :caption: Example credentials file using Global API Key (not recommended): # Cloudflare API credentials used by Certbot dns_cloudflare_email = cloudfl...@example.com - dns_cloudflare_api_key = 0123456789abcdef0123456789abcdef01234567 + dns_cloudflare_api_key = 0123456789abcdef0123456789abcdef01234 The path to this file can be provided interactively or using the ``--dns-cloudflare-credentials`` command-line argument. Certbot records the path diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-dns-cloudflare-1.1.0/certbot_dns_cloudflare/_internal/dns_cloudflare.py new/certbot-dns-cloudflare-1.2.0/certbot_dns_cloudflare/_internal/dns_cloudflare.py --- old/certbot-dns-cloudflare-1.1.0/certbot_dns_cloudflare/_internal/dns_cloudflare.py 2020-01-14 19:41:31.000000000 +0100 +++ new/certbot-dns-cloudflare-1.2.0/certbot_dns_cloudflare/_internal/dns_cloudflare.py 2020-02-04 22:46:57.000000000 +0100 @@ -4,6 +4,10 @@ import CloudFlare import zope.interface +from acme.magic_typing import Any +from acme.magic_typing import Dict +from acme.magic_typing import List + from certbot import errors from certbot import interfaces from certbot.plugins import dns_common @@ -38,14 +42,35 @@ return 'This plugin configures a DNS TXT record to respond to a dns-01 challenge using ' + \ 'the Cloudflare API.' + def _validate_credentials(self, credentials): + token = credentials.conf('api-token') + email = credentials.conf('email') + key = credentials.conf('api-key') + if token: + if email or key: + raise errors.PluginError('{}: dns_cloudflare_email and dns_cloudflare_api_key are ' + 'not needed when using an API Token' + .format(credentials.confobj.filename)) + elif email or key: + if not email: + raise errors.PluginError('{}: dns_cloudflare_email is required when using a Global ' + 'API Key. (should be email address associated with ' + 'Cloudflare account)'.format(credentials.confobj.filename)) + if not key: + raise errors.PluginError('{}: dns_cloudflare_api_key is required when using a ' + 'Global API Key. (see {})' + .format(credentials.confobj.filename, ACCOUNT_URL)) + else: + raise errors.PluginError('{}: Either dns_cloudflare_api_token (recommended), or ' + 'dns_cloudflare_email and dns_cloudflare_api_key are required.' + ' (see {})'.format(credentials.confobj.filename, ACCOUNT_URL)) + def _setup_credentials(self): self.credentials = self._configure_credentials( 'credentials', 'Cloudflare credentials INI file', - { - 'email': 'email address associated with Cloudflare account', - 'api-key': 'API key for Cloudflare account, obtained from {0}'.format(ACCOUNT_URL) - } + None, + self._validate_credentials ) def _perform(self, domain, validation_name, validation): @@ -55,6 +80,8 @@ self._get_cloudflare_client().del_txt_record(domain, validation_name, validation) def _get_cloudflare_client(self): + if self.credentials.conf('api-token'): + return _CloudflareClient(None, self.credentials.conf('api-token')) return _CloudflareClient(self.credentials.conf('email'), self.credentials.conf('api-key')) @@ -88,8 +115,15 @@ logger.debug('Attempting to add record to zone %s: %s', zone_id, data) self.cf.zones.dns_records.post(zone_id, data=data) # zones | pylint: disable=no-member except CloudFlare.exceptions.CloudFlareAPIError as e: + code = int(e) + hint = None + + if code == 9109: + hint = 'Does your API token have "Zone:DNS:Edit" permissions?' + logger.error('Encountered CloudFlareAPIError adding TXT record: %d %s', e, e) - raise errors.PluginError('Error communicating with the Cloudflare API: {0}'.format(e)) + raise errors.PluginError('Error communicating with the Cloudflare API: {0}{1}' + .format(e, ' ({0})'.format(hint) if hint else '')) record_id = self._find_txt_record_id(zone_id, record_name, record_content) logger.debug('Successfully added TXT record with record_id: %s', record_id) @@ -139,6 +173,8 @@ """ zone_name_guesses = dns_common.base_domain_name_guesses(domain) + zones = [] # type: List[Dict[str, Any]] + code = msg = None for zone_name in zone_name_guesses: params = {'name': zone_name, @@ -148,16 +184,26 @@ zones = self.cf.zones.get(params=params) # zones | pylint: disable=no-member except CloudFlare.exceptions.CloudFlareAPIError as e: code = int(e) + msg = str(e) hint = None if code == 6003: - hint = 'Did you copy your entire API key?' + hint = ('Did you copy your entire API token/key? To use Cloudflare tokens, ' + 'you\'ll need the python package cloudflare>=2.3.1.{}' + .format(' This certbot is running cloudflare ' + str(CloudFlare.__version__) + if hasattr(CloudFlare, '__version__') else '')) elif code == 9103: - hint = 'Did you enter the correct email address?' - - raise errors.PluginError('Error determining zone_id: {0} {1}. Please confirm that ' - 'you have supplied valid Cloudflare API credentials.{2}' - .format(code, e, ' ({0})'.format(hint) if hint else '')) + hint = 'Did you enter the correct email address and Global key?' + elif code == 9109: + hint = 'Did you enter a valid Cloudflare Token?' + + if hint: + raise errors.PluginError('Error determining zone_id: {0} {1}. Please confirm ' + 'that you have supplied valid Cloudflare API credentials. ({2})' + .format(code, msg, hint)) + else: + logger.debug('Unrecognised CloudFlareAPIError while finding zone_id: %d %s. ' + 'Continuing with next zone guess...', e, e) if zones: zone_id = zones[0]['id'] @@ -165,9 +211,10 @@ return zone_id raise errors.PluginError('Unable to determine zone_id for {0} using zone names: {1}. ' - 'Please confirm that the domain name has been entered correctly ' - 'and is already associated with the supplied Cloudflare account.' - .format(domain, zone_name_guesses)) + 'Please confirm that the domain name has been entered correctly ' + 'and is already associated with the supplied Cloudflare account.{2}' + .format(domain, zone_name_guesses, ' The error from Cloudflare was:' + ' {0} {1}'.format(code, msg) if code is not None else '')) def _find_txt_record_id(self, zone_id, record_name, record_content): """ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-dns-cloudflare-1.1.0/certbot_dns_cloudflare.egg-info/PKG-INFO new/certbot-dns-cloudflare-1.2.0/certbot_dns_cloudflare.egg-info/PKG-INFO --- old/certbot-dns-cloudflare-1.1.0/certbot_dns_cloudflare.egg-info/PKG-INFO 2020-01-14 19:41:51.000000000 +0100 +++ new/certbot-dns-cloudflare-1.2.0/certbot_dns_cloudflare.egg-info/PKG-INFO 2020-02-04 22:47:10.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: certbot-dns-cloudflare -Version: 1.1.0 +Version: 1.2.0 Summary: Cloudflare DNS Authenticator plugin for Certbot Home-page: https://github.com/certbot/certbot Author: Certbot Project @@ -17,7 +17,6 @@ Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 @@ -28,5 +27,5 @@ Classifier: Topic :: System :: Networking Classifier: Topic :: System :: Systems Administration Classifier: Topic :: Utilities -Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* +Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.* Provides-Extra: docs diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-dns-cloudflare-1.1.0/certbot_dns_cloudflare.egg-info/requires.txt new/certbot-dns-cloudflare-1.2.0/certbot_dns_cloudflare.egg-info/requires.txt --- old/certbot-dns-cloudflare-1.1.0/certbot_dns_cloudflare.egg-info/requires.txt 2020-01-14 19:41:51.000000000 +0100 +++ new/certbot-dns-cloudflare-1.2.0/certbot_dns_cloudflare.egg-info/requires.txt 2020-02-04 22:47:10.000000000 +0100 @@ -1,5 +1,5 @@ acme>=0.29.0 -certbot>=1.0.0 +certbot>=1.1.0 cloudflare>=1.5.1 mock setuptools diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-dns-cloudflare-1.1.0/setup.py new/certbot-dns-cloudflare-1.2.0/setup.py --- old/certbot-dns-cloudflare-1.1.0/setup.py 2020-01-14 19:41:33.000000000 +0100 +++ new/certbot-dns-cloudflare-1.2.0/setup.py 2020-02-04 22:46:58.000000000 +0100 @@ -4,13 +4,13 @@ from setuptools import setup from setuptools.command.test import test as TestCommand -version = '1.1.0' +version = '1.2.0' # Remember to update local-oldest-requirements.txt when changing the minimum # acme/certbot version. install_requires = [ 'acme>=0.29.0', - 'certbot>=1.0.0', + 'certbot>=1.1.0', 'cloudflare>=1.5.1', 'mock', 'setuptools', @@ -44,7 +44,7 @@ author="Certbot Project", author_email='client-...@letsencrypt.org', license='Apache License 2.0', - python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*', + python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*', classifiers=[ 'Development Status :: 5 - Production/Stable', 'Environment :: Plugins', @@ -55,7 +55,6 @@ 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-dns-cloudflare-1.1.0/tests/dns_cloudflare_test.py new/certbot-dns-cloudflare-1.2.0/tests/dns_cloudflare_test.py --- old/certbot-dns-cloudflare-1.1.0/tests/dns_cloudflare_test.py 2020-01-14 19:41:31.000000000 +0100 +++ new/certbot-dns-cloudflare-1.2.0/tests/dns_cloudflare_test.py 2020-02-04 22:46:57.000000000 +0100 @@ -12,6 +12,9 @@ from certbot.tests import util as test_util API_ERROR = CloudFlare.exceptions.CloudFlareAPIError(1000, '', '') + +API_TOKEN = 'an-api-token' + API_KEY = 'an-api-key' EMAIL = 'exam...@example.com' @@ -49,6 +52,50 @@ expected = [mock.call.del_txt_record(DOMAIN, '_acme-challenge.'+DOMAIN, mock.ANY)] self.assertEqual(expected, self.mock_client.mock_calls) + def test_api_token(self): + dns_test_common.write({"cloudflare_api_token": API_TOKEN}, + self.config.cloudflare_credentials) + self.auth.perform([self.achall]) + + expected = [mock.call.add_txt_record(DOMAIN, '_acme-challenge.'+DOMAIN, mock.ANY, mock.ANY)] + self.assertEqual(expected, self.mock_client.mock_calls) + + def test_no_creds(self): + dns_test_common.write({}, self.config.cloudflare_credentials) + self.assertRaises(errors.PluginError, + self.auth.perform, + [self.achall]) + + def test_missing_email_or_key(self): + dns_test_common.write({"cloudflare_api_key": API_KEY}, self.config.cloudflare_credentials) + self.assertRaises(errors.PluginError, + self.auth.perform, + [self.achall]) + + dns_test_common.write({"cloudflare_email": EMAIL}, self.config.cloudflare_credentials) + self.assertRaises(errors.PluginError, + self.auth.perform, + [self.achall]) + + def test_email_or_key_with_token(self): + dns_test_common.write({"cloudflare_api_token": API_TOKEN, "cloudflare_email": EMAIL}, + self.config.cloudflare_credentials) + self.assertRaises(errors.PluginError, + self.auth.perform, + [self.achall]) + + dns_test_common.write({"cloudflare_api_token": API_TOKEN, "cloudflare_api_key": API_KEY}, + self.config.cloudflare_credentials) + self.assertRaises(errors.PluginError, + self.auth.perform, + [self.achall]) + + dns_test_common.write({"cloudflare_api_token": API_TOKEN, "cloudflare_email": EMAIL, + "cloudflare_api_key": API_KEY}, self.config.cloudflare_credentials) + self.assertRaises(errors.PluginError, + self.auth.perform, + [self.achall]) + class CloudflareClientTest(unittest.TestCase): record_name = "foo" @@ -83,7 +130,7 @@ def test_add_txt_record_error(self): self.cf.zones.get.return_value = [{'id': self.zone_id}] - self.cf.zones.dns_records.post.side_effect = API_ERROR + self.cf.zones.dns_records.post.side_effect = CloudFlare.exceptions.CloudFlareAPIError(9109, '', '') self.assertRaises( errors.PluginError, @@ -104,6 +151,25 @@ self.assertRaises( errors.PluginError, self.cloudflare_client.add_txt_record, + DOMAIN, self.record_name, self.record_content, self.record_ttl) + + def test_add_txt_record_bad_creds(self): + self.cf.zones.get.side_effect = CloudFlare.exceptions.CloudFlareAPIError(6003, '', '') + self.assertRaises( + errors.PluginError, + self.cloudflare_client.add_txt_record, + DOMAIN, self.record_name, self.record_content, self.record_ttl) + + self.cf.zones.get.side_effect = CloudFlare.exceptions.CloudFlareAPIError(9103, '', '') + self.assertRaises( + errors.PluginError, + self.cloudflare_client.add_txt_record, + DOMAIN, self.record_name, self.record_content, self.record_ttl) + + self.cf.zones.get.side_effect = CloudFlare.exceptions.CloudFlareAPIError(9109, '', '') + self.assertRaises( + errors.PluginError, + self.cloudflare_client.add_txt_record, DOMAIN, self.record_name, self.record_content, self.record_ttl) def test_del_txt_record(self):