pax_global_header00006660000000000000000000000064147243263520014522gustar00rootroot0000000000000052 comment=335199311b82c45b1f021cba98af9d28e218c025 certbot-dns-infomaniak-0.2.3/000077500000000000000000000000001472432635200160625ustar00rootroot00000000000000certbot-dns-infomaniak-0.2.3/.github/000077500000000000000000000000001472432635200174225ustar00rootroot00000000000000certbot-dns-infomaniak-0.2.3/.github/workflows/000077500000000000000000000000001472432635200214575ustar00rootroot00000000000000certbot-dns-infomaniak-0.2.3/.github/workflows/python-app.yml000066400000000000000000000031671472432635200243100ustar00rootroot00000000000000# This workflow will install Python dependencies, run tests and lint with a single version of Python # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions name: Python application on: push: {} pull_request: branches: [ main ] jobs: build: runs-on: ubuntu-latest strategy: matrix: python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Cache pip uses: actions/cache@v2 with: # This path is specific to Ubuntu path: ~/.cache/pip # Look to see if there is a cache hit for the corresponding requirements file key: pip-${{ matrix.python-version }}-${{ hashFiles('requirements.txt') }} restore-keys: | pip-${{ matrix.python-version }}- - name: Install dependencies run: | python -m pip install --upgrade pip pip install flake8 pytest if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --ignore=W503 --statistics - name: Test with pytest run: | pytest certbot-dns-infomaniak-0.2.3/.github/workflows/python-publish.yml000066400000000000000000000027561472432635200252010ustar00rootroot00000000000000# This workflow will upload a Python Package using Twine when a release is created name: Upload Python Package on: push: # Sequence of patterns matched against refs/tags tags: - '*' # Push events to matching *, i.e. 1.0, 20.15.10 jobs: build: runs-on: ubuntu-latest steps: - name: Get the version id: get_version run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//} - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 with: python-version: '3.x' - name: Install dependencies run: | python -m pip install --upgrade pip pip install setuptools wheel twine - name: Build and publish env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.TWINE_TOKEN }} run: | sed -i.bak 's/^version =.*/version = "${{ steps.get_version.outputs.VERSION }}"/' setup.py python setup.py sdist bdist_wheel twine upload dist/* - name: Create Release id: create_release uses: actions/create-release@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token with: tag_name: ${{ github.ref }} release_name: ${{ github.ref }} body: | Release created, package download: https://pypi.org/project/certbot-dns-infomaniak/${{ steps.get_version.outputs.VERSION }}/ draft: false prerelease: false certbot-dns-infomaniak-0.2.3/.gitignore000066400000000000000000000037631472432635200200630ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder .pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: # .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Cython debug symbols cython_debug/ certbot-dns-infomaniak-0.2.3/LICENSE000066400000000000000000000261351472432635200170760ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. certbot-dns-infomaniak-0.2.3/README.rst000066400000000000000000000062501472432635200175540ustar00rootroot00000000000000certbot-dns-infomaniak ====================== Infomaniak_ DNS Authenticator plugin for certbot_ This plugin enables usage of Infomaniak public API to complete ``dns-01`` challenges. .. _Infomaniak: https://www.infomaniak.com/ .. _certbot: https://certbot.eff.org/ Issue a token ------------- At your Infomaniak manager dashboard_, to to the API section and generate a token with "Domain" scope .. _dashboard: https://manager.infomaniak.com/v3/infomaniak-api Installation ------------ .. code-block:: bash pip install certbot-dns-infomaniak Usage ----- Via environment variable ^^^^^^^^^^^^^^^^^^^^^^^^ .. code-block:: bash export INFOMANIAK_API_TOKEN=xxx certbot certonly \ --authenticator dns-infomaniak \ --server https://acme-v02.api.letsencrypt.org/directory \ --agree-tos \ --rsa-key-size 4096 \ -d 'death.star' If certbot requires elevated rights, the following command must be used instead: .. code-block:: bash export INFOMANIAK_API_TOKEN=xxx sudo --preserve-env=INFOMANIAK_API_TOKEN certbot certonly \ --authenticator dns-infomaniak \ --server https://acme-v02.api.letsencrypt.org/directory \ --agree-tos \ --rsa-key-size 4096 \ -d 'death.star' Via INI file ^^^^^^^^^^^^ Certbot will emit a warning if it detects that the credentials file can be accessed by other users on your system. The warning reads "Unsafe permissions on credentials configuration file", followed by the path to the credentials 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). =================================== ========================================== ``--authenticator dns-infomaniak`` select the authenticator plugin (Required) ``--dns-infomaniak-credentials`` Infomaniak Token credentials INI file. (Required) =================================== ========================================== An example ``credentials.ini`` file: .. code-block:: ini dns_infomaniak_token = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX To start using DNS authentication for Infomaniak, pass the following arguments on certbot's command line: .. code-block:: bash certbot certonly \ --authenticator dns-infomaniak \ --dns-infomaniak-credentials \ --server https://acme-v02.api.letsencrypt.org/directory \ --agree-tos \ --rsa-key-size 4096 \ -d 'death.star' Automatic renewal ----------------- By default, certbot installs a service that periodically renews its certificates automatically. In order to do this, the command must know the API key, otherwise it will fail silently. In order to enable automatic renewal for your wildcard certificates, you will need to edit ``/lib/systemd/system/certbot.service``. In there, add the following line in ``Service``, with replaced with your actual token: .. code-block:: bash Environment="INFOMANIAK_API_TOKEN=" Acknowledgments --------------- Based on certbot-dns-ispconfig plugin at https://github.com/m42e/certbot-dns-ispconfig/ certbot-dns-infomaniak-0.2.3/certbot_dns_infomaniak/000077500000000000000000000000001472432635200225645ustar00rootroot00000000000000certbot-dns-infomaniak-0.2.3/certbot_dns_infomaniak/__init__.py000066400000000000000000000003161472432635200246750ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2021-2022 Rene Luria # SPDX-License-Identifier: Apache-2.0 """ This plugin enables usage of Infomaniak public API to complete``dns-01`` challenges. """ certbot-dns-infomaniak-0.2.3/certbot_dns_infomaniak/dns_infomaniak.py000066400000000000000000000224521472432635200261230ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2021-2022 Rene Luria # SPDX-FileCopyrightText: 2021-2022 Yannik Roth # SPDX-FileCopyrightText: 2021 Romain Autran # SPDX-License-Identifier: Apache-2.0 """DNS Authenticator for Infomaniak""" import json import logging import idna import requests from certbot import errors from certbot.plugins import dns_common try: import certbot.compat.os as os except ImportError: import os logger = logging.getLogger(__name__) class Authenticator(dns_common.DNSAuthenticator): """DNS Authenticator for Infomaniak This plugin enables usage of Infomaniak public API to complete``dns-01`` challenges.""" description = "Automates dns-01 challenges using Infomaniak API" def __init__(self, *args, **kwargs): # super(Authenticator, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs) self.token = "" self.credentials = None @classmethod def add_parser_arguments(cls, add): # pylint: disable=arguments-differ super(Authenticator, cls).add_parser_arguments( add, default_propagation_seconds=120 ) add("credentials", help="Infomaniak credentials INI file.") def more_info(self): # pylint: disable=missing-docstring,no-self-use return self.description def _setup_credentials(self): token = os.getenv("INFOMANIAK_API_TOKEN") if token is None: self.credentials = self._configure_credentials( "credentials", "Infomaniak credentials INI file", { "token": "Infomaniak API token.", }, ) if not self.credentials: raise errors.PluginError("INFOMANIAK API Token not defined") self.token = self.credentials.conf("token") else: self.token = token def _perform(self, domain, validation_name, validation): decoded_domain = idna.decode(domain) try: self._api_client().add_txt_record(decoded_domain, validation_name, validation) except ValueError as err: raise errors.PluginError("Cannot add txt record: {err}".format(err=err)) def _cleanup(self, domain, validation_name, validation): decoded_domain = idna.decode(domain) try: self._api_client().del_txt_record(decoded_domain, validation_name, validation) except ValueError as err: raise errors.PluginError("Cannot del txt record: {err}".format(err=err)) def _api_client(self): return _APIDomain(self.token) class _APIDomain: baseUrl = "https://api.infomaniak.com" def __init__(self, token): """Initialize class managing a domain within Infomaniak API :param str token: oauth2 token to consume Infomaniak API """ self.token = token self.session = requests.Session() self.session.headers.update({"Authorization": "Bearer {token}".format(token=self.token)}) def _get_request(self, url, payload=None): """Performs a GET request against API :param str url: relative url :param dict payload : body of request """ url = self.baseUrl + url logger.debug("GET %s", url) with self.session.get(url, params=payload) as req: try: result = req.json() except json.decoder.JSONDecodeError as exc: raise errors.PluginError("no JSON in API response") from exc if result["result"] == "success": return result["data"] if result["error"]["code"] == "not_authorized": raise errors.PluginError("cannot authenticate") raise errors.PluginError( "error in API request: {} / {}".format( result["error"]["code"], result["error"]["description"] ) ) def _post_request(self, url, payload): """Performs a POST request :param str url: relative url :param dict payload : body of request """ url = self.baseUrl + url headers = {"Content-Type": "application/json"} json_data = json.dumps(payload) logger.debug("POST %s", url) with self.session.post(url, data=json_data, headers=headers) as req: try: result = req.json() except json.decoder.JSONDecodeError as exc: raise errors.PluginError("no JSON in API response") from exc if result["result"] == "success": return result["data"] raise errors.PluginError( "error in API request: {} / {}".format( result["error"]["code"], result["error"]["description"] ) ) def _delete_request(self, url): """Performs a POST request :param str url: relative url """ url = self.baseUrl + url logger.debug("DELETE %s", url) with self.session.delete(url) as req: try: result = req.json() except json.decoder.JSONDecodeError as exc: raise errors.PluginError("no JSON in API response") from exc if result["result"] == "success": return result["data"] raise errors.PluginError( "error in API request: {} / {}".format( result["error"]["code"], result["error"]["description"] ) ) def _get_records(self, domain, domain_id, record): """Find record matching arguments :param str domain: domain name :param int domain_id: domain id :param dict record: dict describing records- keys are type, source and target :returns: records list :rtype: list """ for needed in ["type", "source", "target"]: if needed not in record: raise ValueError("{} not provided in record dict".format(needed)) if record["source"] == ".": fqdn = domain else: fqdn = "{source}.{domain}".format(source=record["source"], domain=domain) return list( filter( lambda x: ( x["source_idn"] == fqdn and x["type"] == record["type"] and x["target"] == record["target"] ), self._get_request("/1/domain/{domain_id}/dns/record?search={fqdn}&filter[types][]={type}".format(domain_id=domain_id, fqdn=fqdn, type=record["type"])), ) ) def _find_zone(self, domain): """Finds the corresponding DNS zone through the API :param str domain: domain name :returns: id and zone name """ while "." in domain: result = self._get_request( "/1/product?service_name=domain&customer_name={domain}".format(domain=domain), ) if len(result) == 1: return ( result[0]["id"], domain, ) domain = domain[domain.find(".") + 1:] raise errors.PluginError("Domain not found") def add_txt_record(self, domain, source, target, ttl=300): """Add a TXT DNS record to a domain :param str domain: domain name to lookup :param str source: record key in zone (left prefix before domain) :param str target: value of record :param int ttl: optional ttl of record to create """ logger.debug("add_txt_record %s %s %s", domain, source, target) (domain_id, domain_name) = self._find_zone(domain) logger.debug("%s / %s", domain_id, domain_name) if source.endswith("." + idna.encode(domain_name).decode("ascii")): relative_source = source[:source.rfind("." + idna.encode(domain_name).decode("ascii"))] else: relative_source = source logger.debug("add_txt_record %s %s %s", domain_name, relative_source, target) data = {"type": "TXT", "source": relative_source, "target": target, "ttl": ttl} self._post_request("/1/domain/{domain_id}/dns/record".format(domain_id=domain_id), data) def del_txt_record(self, domain, source, target): """Delete a TXT DNS record from a domain :param str source: record key in zone (left prefix before domain) :param str target: value of record """ logger.debug("del_txt_record %s %s %s", domain, source, target) (domain_id, domain_name) = self._find_zone(domain) if source.endswith("." + idna.encode(domain_name).decode("ascii")): relative_source = source[:source.rfind("." + idna.encode(domain_name).decode("ascii"))] else: relative_source = source logger.debug("del_txt_record %s %s %s", domain_name, relative_source, target) records = self._get_records( domain_name, domain_id, {"type": "TXT", "source": relative_source, "target": target}, ) if records is None: raise errors.PluginError("Record not found") if len(records) > 1: raise errors.PluginError("Several records match") record_id = records[0]["id"] self._delete_request("/1/domain/{domain_id}/dns/record/{record_id}".format( domain_id=domain_id, record_id=record_id)) certbot-dns-infomaniak-0.2.3/certbot_dns_infomaniak/dns_infomaniak_test.py000066400000000000000000000175361472432635200271710ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2021-2022 Rene Luria # SPDX-License-Identifier: Apache-2.0 """Tests for certbot_dns_infomaniak.dns_infomaniak.""" import unittest import logging from unittest import mock import requests_mock import sys import io from certbot.errors import PluginError try: import certbot.compat.os as os except ImportError: import os from certbot.plugins import dns_test_common from certbot.plugins.dns_test_common import DOMAIN from certbot.tests import util as test_util from certbot_dns_infomaniak.dns_infomaniak import _APIDomain logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) FAKE_TOKEN = "xxxx" class AuthenticatorTest( test_util.TempDirTestCase, dns_test_common.BaseAuthenticatorTest ): """Class to test the Authenticator class""" def setUp(self): super(AuthenticatorTest, self).setUp() self.config = mock.MagicMock() os.environ["INFOMANIAK_API_TOKEN"] = FAKE_TOKEN from certbot_dns_infomaniak.dns_infomaniak import Authenticator self.auth = Authenticator(self.config, "infomaniak") self.mock_client = mock.MagicMock(default_propagation_seconds=15) self.auth._api_client = mock.MagicMock(return_value=self.mock_client) try: from certbot.display.util import notify # noqa: F401 notify_patch = mock.patch('certbot._internal.main.display_util.notify') self.mock_notify = notify_patch.start() self.addCleanup(notify_patch.stop) self.old_stdout = sys.stdout sys.stdout = io.StringIO() except ImportError: self.old_stdout = sys.stdout def tearDown(self): sys.stdout = self.old_stdout def test_perform(self): """Tests the perform function to see if client method is called""" self.auth.perform([self.achall]) expected = [ mock.call.add_txt_record(DOMAIN, "_acme-challenge." + DOMAIN, mock.ANY) ] self.assertEqual(expected, self.mock_client.mock_calls) def test_cleanup(self): """Tests mthe cleanup method to see if client method is called""" # _attempt_cleanup | pylint: disable=protected-access self.auth._attempt_cleanup = True self.auth.cleanup([self.achall]) expected = [ mock.call.del_txt_record(DOMAIN, "_acme-challenge." + DOMAIN, mock.ANY) ] self.assertEqual(expected, self.mock_client.mock_calls) class APIDomainTest(unittest.TestCase): """Class to test the _APIDomain class""" record_name = "foo" record_content = "bar" record_ttl = 42 def setUp(self): self.adapter = requests_mock.Adapter() self.client = _APIDomain(FAKE_TOKEN) self.client.baseUrl = "mock://endpoint" self.client.session.mount("mock", self.adapter) def _register_response(self, url, data=None, method=requests_mock.ANY): """Registers a reply in the requests mock :param str url: url to register response :param dict data: data to return :param str method: method for which response is registered (default to all) """ resp = {"result": "success", "data": data} self.adapter.register_uri( method, self.client.baseUrl + url, json=resp, ) def _register_error(self, url, code, description): """Registers an error reply in the requests mock :param str url: url to register response :param int code: error code :param str description: error description """ resp = {"result": "error", "error": {"code": code, "description": description}} self.adapter.register_uri( requests_mock.ANY, self.client.baseUrl + url, json=resp, ) def test_add_txt_record(self): """add_txt_record with normal params should succeed""" self._register_response( "/1/product?service_name=domain&customer_name={domain}".format(domain=DOMAIN), data=[ { "id": 654321, "account_id": 1234, "service_id": 14, "service_name": "domain", "customer_name": DOMAIN, } ], ) self._register_response("/1/domain/654321/dns/record", "1001234", "POST") self.client.add_txt_record( DOMAIN, self.record_name, self.record_content, self.record_ttl ) def test_add_txt_record_fail_to_find_domain(self): """add_txt_record with non existing domain should fail""" self._register_response( "/1/product?service_name=domain&customer_name={domain}".format(domain=DOMAIN), data=[], ) with self.assertRaises(PluginError): self.client.add_txt_record( DOMAIN, self.record_name, self.record_content, self.record_ttl ) def test_add_txt_record_fail_to_authenticate(self): """add_txt_record with wrong token should fail""" self._register_error( "/1/product?service_name=domain&customer_name={domain}".format(domain=DOMAIN), "not_authorized", "Authorization required", ) with self.assertRaises(PluginError): self.client.add_txt_record( DOMAIN, self.record_name, self.record_content, self.record_ttl ) def test_del_txt_record(self): """del_txt_record with normal params should succeed""" self._register_response( "/1/product?service_name=domain&customer_name={domain}".format(domain=DOMAIN), data=[ { "id": "654321", "account_id": "1234", "service_id": "14", "service_name": "domain", "customer_name": DOMAIN, } ], ) self._register_response( "/1/domain/654321/dns/record", [ { "id": "11110", "source": ".", "source_idn": DOMAIN, "type": "NS", "ttl": 3600, "target": "ns1.death.star", }, { "id": "11111", "source": self.record_name, "source_idn": "{name}.{domain}".format(name=self.record_name, domain=DOMAIN), "type": "TXT", "ttl": self.record_ttl, "target": self.record_content, }, ], ) self._register_response( "/1/domain/654321/dns/record/11111", True, "DELETE", ) self.client.del_txt_record( DOMAIN, "{name}.{domain}".format(name=self.record_name, domain=DOMAIN), self.record_content, ) def test_del_txt_record_fail_to_find_domain(self): """del_txt_record with non existing domain should fail""" self._register_response( "/1/product?service_name=domain&customer_name={domain}".format(domain=DOMAIN), data=[], ) with self.assertRaises(PluginError): self.client.del_txt_record( DOMAIN, self.record_name, self.record_content ) def test_del_txt_record_fail_to_authenticate(self): """del_txt_recod with wrong token should fail""" self._register_error( "/1/product?service_name=domain&customer_name={domain}".format(domain=DOMAIN), "not_authorized", "Authorization required", ) with self.assertRaises(PluginError): self.client.del_txt_record( DOMAIN, self.record_name, self.record_content ) if __name__ == "__main__": unittest.main() # pragma: no cover certbot-dns-infomaniak-0.2.3/requirements.txt000066400000000000000000000000671472432635200213510ustar00rootroot00000000000000certbot>=0.31.0 setuptools requests requests-mock idna certbot-dns-infomaniak-0.2.3/setup.cfg000066400000000000000000000000341472432635200177000ustar00rootroot00000000000000[bdist_wheel] universal = 1 certbot-dns-infomaniak-0.2.3/setup.py000066400000000000000000000040501472432635200175730ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2021-2022 Rene Luria # SPDX-FileCopyrightText: 2021-2022 Yannik Roth # SPDX-License-Identifier: Apache-2.0 from setuptools import setup from setuptools import find_packages from os import path version = "0.0.0" install_requires = [ "certbot>=0.31.0", "setuptools", "requests", "requests-mock", "idna", ] # read the contents of your README file this_directory = path.abspath(path.dirname(__file__)) with open(path.join(this_directory, "README.rst")) as f: long_description = f.read() setup( name="certbot-dns-infomaniak", version=version, description="Infomaniak DNS Authenticator plugin for Certbot", long_description=long_description, long_description_content_type="text/x-rst", url="https://github.com/infomaniak/certbot-dns-infomaniak", author="Rene Luria", author_email="rene.luria@infomaniak.com", license="Apache License 2.0", python_requires=">=3.5.0", classifiers=[ "Development Status :: 3 - Alpha", "Environment :: Plugins", "Intended Audience :: System Administrators", "License :: OSI Approved :: Apache Software License", "Operating System :: POSIX :: Linux", "Programming Language :: Python", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet :: WWW/HTTP", "Topic :: Security", "Topic :: System :: Installation/Setup", "Topic :: System :: Networking", "Topic :: System :: Systems Administration", "Topic :: Utilities", ], packages=find_packages(), include_package_data=True, install_requires=install_requires, entry_points={ "certbot.plugins": [ "dns-infomaniak = certbot_dns_infomaniak.dns_infomaniak:Authenticator" ] }, test_suite="certbot_dns_infomaniak", )