diff --git a/CHANGELOG.md b/CHANGELOG.md index afc8b8b21..c1af9ffb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ Certbot adheres to [Semantic Versioning](https://semver.org/). * Removed the fallback introduced with 0.32.0 in `acme` to retry a challenge response with a `keyAuthorization` if sending the response without this field caused a `malformed` error to be received from the ACME server. +* Linode DNS plugin now supports api keys created from their new panel + at [cloud.linode.com](https://cloud.linode.com) ### Fixed diff --git a/certbot-dns-linode/certbot_dns_linode/__init__.py b/certbot-dns-linode/certbot_dns_linode/__init__.py index 0a6ccec61..107781a13 100644 --- a/certbot-dns-linode/certbot_dns_linode/__init__.py +++ b/certbot-dns-linode/certbot_dns_linode/__init__.py @@ -27,7 +27,8 @@ Credentials Use of this plugin requires a configuration file containing Linode API credentials, obtained from your Linode account's `Applications & API -Tokens page `_. +Tokens page (legacy) `_ or `Applications +& API Tokens page (new) `_. .. code-block:: ini :name: credentials.ini @@ -35,6 +36,7 @@ Tokens page `_. # Linode API credentials used by Certbot dns_linode_key = 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ64 + dns_linode_version = [|3|4] The path to this file can be provided interactively or using the ``--dns-linode-credentials`` command-line argument. Certbot records the path diff --git a/certbot-dns-linode/certbot_dns_linode/dns_linode.py b/certbot-dns-linode/certbot_dns_linode/dns_linode.py index c2097a7d6..507ad5e53 100644 --- a/certbot-dns-linode/certbot_dns_linode/dns_linode.py +++ b/certbot-dns-linode/certbot_dns_linode/dns_linode.py @@ -1,8 +1,10 @@ """DNS Authenticator for Linode.""" import logging +import re import zope.interface from lexicon.providers import linode +from lexicon.providers import linode4 from certbot import errors from certbot import interfaces @@ -12,6 +14,7 @@ from certbot.plugins import dns_common_lexicon logger = logging.getLogger(__name__) API_KEY_URL = 'https://manager.linode.com/profile/api' +API_KEY_URL_V4 = 'https://cloud.linode.com/profile/tokens' @zope.interface.implementer(interfaces.IAuthenticator) @zope.interface.provider(interfaces.IPluginFactory) @@ -41,7 +44,8 @@ class Authenticator(dns_common.DNSAuthenticator): 'credentials', 'Linode credentials INI file', { - 'key': 'API key for Linode account, obtained from {0}'.format(API_KEY_URL) + 'key': 'API key for Linode account, obtained from {0} or {1}' + .format(API_KEY_URL, API_KEY_URL_V4) } ) @@ -52,7 +56,23 @@ class Authenticator(dns_common.DNSAuthenticator): self._get_linode_client().del_txt_record(domain, validation_name, validation) def _get_linode_client(self): - return _LinodeLexiconClient(self.credentials.conf('key')) + api_key = self.credentials.conf('key') + api_version = self.credentials.conf('version') + if api_version == '': + api_version = None + + if not api_version: + api_version = 3 + + # Match for v4 api key + regex_v4 = re.compile('^[0-9a-f]{64}$') + regex_match = regex_v4.match(api_key) + if regex_match: + api_version = 4 + else: + api_version = int(api_version) + + return _LinodeLexiconClient(api_key, api_version) class _LinodeLexiconClient(dns_common_lexicon.LexiconClient): @@ -60,14 +80,26 @@ class _LinodeLexiconClient(dns_common_lexicon.LexiconClient): Encapsulates all communication with the Linode API. """ - def __init__(self, api_key): + def __init__(self, api_key, api_version): super(_LinodeLexiconClient, self).__init__() - config = dns_common_lexicon.build_lexicon_config('linode', {}, { - 'auth_token': api_key, - }) + self.api_version = api_version - self.provider = linode.Provider(config) + if api_version == 3: + config = dns_common_lexicon.build_lexicon_config('linode', {}, { + 'auth_token': api_key, + }) + + self.provider = linode.Provider(config) + elif api_version == 4: + config = dns_common_lexicon.build_lexicon_config('linode4', {}, { + 'auth_token': api_key, + }) + + self.provider = linode4.Provider(config) + else: + raise errors.PluginError('Invalid api version specified: {0}. (Supported: 3, 4)' + .format(api_version)) def _handle_general_error(self, e, domain_name): if not str(e).startswith('Domain not found'): diff --git a/certbot-dns-linode/certbot_dns_linode/dns_linode_test.py b/certbot-dns-linode/certbot_dns_linode/dns_linode_test.py index c1a4e0ec0..153f8b51d 100644 --- a/certbot-dns-linode/certbot_dns_linode/dns_linode_test.py +++ b/certbot-dns-linode/certbot_dns_linode/dns_linode_test.py @@ -4,12 +4,16 @@ import unittest import mock +from certbot import errors from certbot.compat import os from certbot.plugins import dns_test_common from certbot.plugins import dns_test_common_lexicon from certbot.tests import util as test_util +from certbot_dns_linode.dns_linode import Authenticator TOKEN = 'a-token' +TOKEN_V3 = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ64' +TOKEN_V4 = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef' class AuthenticatorTest(test_util.TempDirTestCase, dns_test_common_lexicon.BaseLexiconAuthenticatorTest): @@ -17,8 +21,6 @@ class AuthenticatorTest(test_util.TempDirTestCase, def setUp(self): super(AuthenticatorTest, self).setUp() - from certbot_dns_linode.dns_linode import Authenticator - path = os.path.join(self.tempdir, 'file.ini') dns_test_common.write({"linode_key": TOKEN}, path) @@ -31,6 +33,89 @@ class AuthenticatorTest(test_util.TempDirTestCase, # _get_linode_client | pylint: disable=protected-access self.auth._get_linode_client = mock.MagicMock(return_value=self.mock_client) + # pylint: disable=protected-access + def test_api_version_3_detection(self): + path = os.path.join(self.tempdir, 'file_3_auto.ini') + dns_test_common.write({"linode_key": TOKEN_V3}, path) + + config = mock.MagicMock(linode_credentials=path, + linode_propagation_seconds=0) + auth = Authenticator(config, "linode") + auth._setup_credentials() + client = auth._get_linode_client() + self.assertEqual(3, client.api_version) + + # pylint: disable=protected-access + def test_api_version_4_detection(self): + path = os.path.join(self.tempdir, 'file_4_auto.ini') + dns_test_common.write({"linode_key": TOKEN_V4}, path) + + config = mock.MagicMock(linode_credentials=path, + linode_propagation_seconds=0) + auth = Authenticator(config, "linode") + auth._setup_credentials() + client = auth._get_linode_client() + self.assertEqual(4, client.api_version) + + # pylint: disable=protected-access + def test_api_version_3_detection_empty_version(self): + path = os.path.join(self.tempdir, 'file_3_auto_empty.ini') + dns_test_common.write({"linode_key": TOKEN_V3, "linode_version": ""}, path) + + config = mock.MagicMock(linode_credentials=path, + linode_propagation_seconds=0) + auth = Authenticator(config, "linode") + auth._setup_credentials() + client = auth._get_linode_client() + self.assertEqual(3, client.api_version) + + # pylint: disable=protected-access + def test_api_version_4_detection_empty_version(self): + path = os.path.join(self.tempdir, 'file_4_auto_empty.ini') + dns_test_common.write({"linode_key": TOKEN_V4, "linode_version": ""}, path) + + config = mock.MagicMock(linode_credentials=path, + linode_propagation_seconds=0) + auth = Authenticator(config, "linode") + auth._setup_credentials() + client = auth._get_linode_client() + self.assertEqual(4, client.api_version) + + # pylint: disable=protected-access + def test_api_version_3_manual(self): + path = os.path.join(self.tempdir, 'file_3_manual.ini') + dns_test_common.write({"linode_key": TOKEN_V4, "linode_version": 3}, path) + + config = mock.MagicMock(linode_credentials=path, + linode_propagation_seconds=0) + auth = Authenticator(config, "linode") + auth._setup_credentials() + client = auth._get_linode_client() + self.assertEqual(3, client.api_version) + + # pylint: disable=protected-access + def test_api_version_4_manual(self): + path = os.path.join(self.tempdir, 'file_4_manual.ini') + dns_test_common.write({"linode_key": TOKEN_V3, "linode_version": 4}, path) + + config = mock.MagicMock(linode_credentials=path, + linode_propagation_seconds=0) + auth = Authenticator(config, "linode") + auth._setup_credentials() + client = auth._get_linode_client() + self.assertEqual(4, client.api_version) + + # pylint: disable=protected-access + def test_api_version_error(self): + path = os.path.join(self.tempdir, 'file_version_error.ini') + dns_test_common.write({"linode_key": TOKEN_V3, "linode_version": 5}, path) + + config = mock.MagicMock(linode_credentials=path, + linode_propagation_seconds=0) + auth = Authenticator(config, "linode") + auth._setup_credentials() + self.assertRaises(errors.PluginError, auth._get_linode_client) + class LinodeLexiconClientTest(unittest.TestCase, dns_test_common_lexicon.BaseLexiconClientTest): DOMAIN_NOT_FOUND = Exception('Domain not found') @@ -38,7 +123,19 @@ class LinodeLexiconClientTest(unittest.TestCase, dns_test_common_lexicon.BaseLex def setUp(self): from certbot_dns_linode.dns_linode import _LinodeLexiconClient - self.client = _LinodeLexiconClient(TOKEN) + self.client = _LinodeLexiconClient(TOKEN, 3) + + self.provider_mock = mock.MagicMock() + self.client.provider = self.provider_mock + +class Linode4LexiconClientTest(unittest.TestCase, dns_test_common_lexicon.BaseLexiconClientTest): + + DOMAIN_NOT_FOUND = Exception('Domain not found') + + def setUp(self): + from certbot_dns_linode.dns_linode import _LinodeLexiconClient + + self.client = _LinodeLexiconClient(TOKEN, 4) self.provider_mock = mock.MagicMock() self.client.provider = self.provider_mock diff --git a/certbot-dns-linode/local-oldest-requirements.txt b/certbot-dns-linode/local-oldest-requirements.txt index 2b3ba9f32..d48a789bb 100644 --- a/certbot-dns-linode/local-oldest-requirements.txt +++ b/certbot-dns-linode/local-oldest-requirements.txt @@ -1,3 +1,4 @@ # Remember to update setup.py to match the package versions below. acme[dev]==0.31.0 -e .[dev] +dns-lexicon==2.2.3 diff --git a/certbot-dns-linode/setup.py b/certbot-dns-linode/setup.py index e43ab8de9..771e09381 100644 --- a/certbot-dns-linode/setup.py +++ b/certbot-dns-linode/setup.py @@ -7,7 +7,7 @@ version = '0.34.0.dev0' install_requires = [ 'acme>=0.31.0', 'certbot>=0.34.0.dev0', - 'dns-lexicon>=2.2.1', + 'dns-lexicon>=2.2.3', 'mock', 'setuptools', 'zope.interface',