mirror of
https://github.com/ansible-collections/community.crypto.git
synced 2026-05-06 21:33:00 +00:00
Make all module_utils and plugin_utils private (#887)
* Add leading underscore. Remove deprecated module utils. * Document module and plugin utils as private. Add changelog fragment. * Convert relative to absolute imports. * Remove unnecessary imports.
This commit is contained in:
224
plugins/module_utils/_acme/orders.py
Normal file
224
plugins/module_utils/_acme/orders.py
Normal file
@@ -0,0 +1,224 @@
|
||||
# Copyright (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
|
||||
# Copyright (c) 2021 Felix Fontein <felix@fontein.de>
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
# Note that this module util is **PRIVATE** to the collection. It can have breaking changes at any time.
|
||||
# Do not use this from other collections or standalone plugins/modules!
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
import typing as t
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils._acme.challenges import (
|
||||
Authorization,
|
||||
normalize_combined_identifier,
|
||||
)
|
||||
from ansible_collections.community.crypto.plugins.module_utils._acme.errors import (
|
||||
ACMEProtocolException,
|
||||
ModuleFailException,
|
||||
)
|
||||
from ansible_collections.community.crypto.plugins.module_utils._acme.utils import (
|
||||
nopad_b64,
|
||||
)
|
||||
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
from ansible_collections.community.crypto.plugins.module_utils._acme.acme import (
|
||||
ACMEClient,
|
||||
)
|
||||
|
||||
|
||||
_Order = t.TypeVar("_Order", bound="Order")
|
||||
|
||||
|
||||
class Order:
|
||||
def __init__(self, url: str) -> None:
|
||||
self.url = url
|
||||
|
||||
self.data: dict[str, t.Any] | None = None
|
||||
|
||||
self.status = None
|
||||
self.identifiers: list[tuple[str, str]] = []
|
||||
self.replaces_cert_id = None
|
||||
self.finalize_uri = None
|
||||
self.certificate_uri = None
|
||||
self.authorization_uris: list[str] = []
|
||||
self.authorizations: dict[str, Authorization] = {}
|
||||
|
||||
def _setup(self, client: ACMEClient, data: dict[str, t.Any]) -> None:
|
||||
self.data = data
|
||||
|
||||
self.status = data["status"]
|
||||
self.identifiers = []
|
||||
for identifier in data["identifiers"]:
|
||||
self.identifiers.append((identifier["type"], identifier["value"]))
|
||||
self.replaces_cert_id = data.get("replaces")
|
||||
self.finalize_uri = data.get("finalize")
|
||||
self.certificate_uri = data.get("certificate")
|
||||
self.authorization_uris = data["authorizations"]
|
||||
self.authorizations = {}
|
||||
|
||||
@classmethod
|
||||
def from_json(
|
||||
cls: t.Type[_Order], client: ACMEClient, data: dict[str, t.Any], url: str
|
||||
) -> _Order:
|
||||
result = cls(url)
|
||||
result._setup(client, data)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def from_url(cls: t.Type[_Order], client: ACMEClient, url: str) -> _Order:
|
||||
result = cls(url)
|
||||
result.refresh(client)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls: t.Type[_Order],
|
||||
client: ACMEClient,
|
||||
identifiers: list[tuple[str, str]],
|
||||
replaces_cert_id: str | None = None,
|
||||
profile: str | None = None,
|
||||
) -> _Order:
|
||||
"""
|
||||
Start a new certificate order (ACME v2 protocol).
|
||||
https://tools.ietf.org/html/rfc8555#section-7.4
|
||||
"""
|
||||
acme_identifiers = []
|
||||
for identifier_type, identifier in identifiers:
|
||||
acme_identifiers.append(
|
||||
{
|
||||
"type": identifier_type,
|
||||
"value": identifier,
|
||||
}
|
||||
)
|
||||
new_order: dict[str, t.Any] = {"identifiers": acme_identifiers}
|
||||
if replaces_cert_id is not None:
|
||||
new_order["replaces"] = replaces_cert_id
|
||||
if profile is not None:
|
||||
new_order["profile"] = profile
|
||||
result, info = client.send_signed_request(
|
||||
client.directory["newOrder"],
|
||||
new_order,
|
||||
error_msg="Failed to start new order",
|
||||
expected_status_codes=[201],
|
||||
)
|
||||
return cls.from_json(client, result, info["location"])
|
||||
|
||||
@classmethod
|
||||
def create_with_error_handling(
|
||||
cls: t.Type[_Order],
|
||||
client: ACMEClient,
|
||||
identifiers: list[tuple[str, str]],
|
||||
error_strategy: t.Literal[
|
||||
"auto", "fail", "always", "retry_without_replaces_cert_id"
|
||||
] = "auto",
|
||||
error_max_retries: int = 3,
|
||||
replaces_cert_id: str | None = None,
|
||||
profile: str | None = None,
|
||||
message_callback: t.Callable[[str], None] | None = None,
|
||||
) -> _Order:
|
||||
"""
|
||||
error_strategy can be one of the following strings:
|
||||
|
||||
* ``fail``: simply fail. (Same behavior as ``Order.create()``.)
|
||||
* ``retry_without_replaces_cert_id``: if ``replaces_cert_id`` is not ``None``, set it to ``None`` and retry.
|
||||
The only exception is an error of type ``urn:ietf:params:acme:error:alreadyReplaced``, that indicates that
|
||||
the certificate was already replaced.
|
||||
* ``auto``: try to be clever. Right now this is identical to ``retry_without_replaces_cert_id``, but that can
|
||||
change at any time in the future.
|
||||
* ``always``: always retry until ``error_max_retries`` has been reached.
|
||||
"""
|
||||
tries = 0
|
||||
while True:
|
||||
tries += 1
|
||||
try:
|
||||
return cls.create(
|
||||
client,
|
||||
identifiers,
|
||||
replaces_cert_id=replaces_cert_id,
|
||||
profile=profile,
|
||||
)
|
||||
except ACMEProtocolException as exc:
|
||||
if tries <= error_max_retries + 1 and error_strategy != "fail":
|
||||
if error_strategy == "always":
|
||||
continue
|
||||
|
||||
if (
|
||||
error_strategy in ("auto", "retry_without_replaces_cert_id")
|
||||
and replaces_cert_id is not None
|
||||
and not (
|
||||
exc.error_code == 409
|
||||
and exc.error_type
|
||||
== "urn:ietf:params:acme:error:alreadyReplaced"
|
||||
)
|
||||
):
|
||||
if message_callback:
|
||||
message_callback(
|
||||
f"Stop passing `replaces={replaces_cert_id}` due to error {exc.error_code} {exc.error_type} when creating ACME order"
|
||||
)
|
||||
replaces_cert_id = None
|
||||
continue
|
||||
|
||||
raise
|
||||
|
||||
def refresh(self, client: ACMEClient) -> bool:
|
||||
result, dummy = client.get_request(self.url)
|
||||
changed = self.data != result
|
||||
self._setup(client, result)
|
||||
return changed
|
||||
|
||||
def load_authorizations(self, client: ACMEClient) -> None:
|
||||
for auth_uri in self.authorization_uris:
|
||||
authz = Authorization.from_url(client, auth_uri)
|
||||
self.authorizations[
|
||||
normalize_combined_identifier(authz.combined_identifier)
|
||||
] = authz
|
||||
|
||||
def wait_for_finalization(self, client: ACMEClient) -> None:
|
||||
while True:
|
||||
self.refresh(client)
|
||||
if self.status in ["valid", "invalid", "pending", "ready"]:
|
||||
break
|
||||
time.sleep(2)
|
||||
|
||||
if self.status != "valid":
|
||||
raise ACMEProtocolException(
|
||||
client.module,
|
||||
f'Failed to wait for order to complete; got status "{self.status}"',
|
||||
content_json=self.data,
|
||||
)
|
||||
|
||||
def finalize(self, client: ACMEClient, csr_der: bytes, wait: bool = True) -> None:
|
||||
"""
|
||||
Create a new certificate based on the csr.
|
||||
Return the certificate object as dict
|
||||
https://tools.ietf.org/html/rfc8555#section-7.4
|
||||
"""
|
||||
if self.finalize_uri is None:
|
||||
raise ModuleFailException("finalize_uri must be set")
|
||||
new_cert = {
|
||||
"csr": nopad_b64(csr_der),
|
||||
}
|
||||
result, info = client.send_signed_request(
|
||||
self.finalize_uri,
|
||||
new_cert,
|
||||
error_msg="Failed to finalizing order",
|
||||
expected_status_codes=[200],
|
||||
)
|
||||
# It is not clear from the RFC whether the finalize call returns the order object or not.
|
||||
# Instead of using the result, we call self.refresh(client) below.
|
||||
|
||||
if wait:
|
||||
self.wait_for_finalization(client)
|
||||
else:
|
||||
self.refresh(client)
|
||||
if self.status not in ["procesing", "valid", "invalid"]:
|
||||
raise ACMEProtocolException(
|
||||
client.module,
|
||||
f'Failed to finalize order; got status "{self.status}"',
|
||||
info=info,
|
||||
content_json=result,
|
||||
)
|
||||
Reference in New Issue
Block a user