acme: further deprecations (#9395)

* acme: deprecate acme.fields.Resource and .resource

* acme: deprecate .messages.OLD_ERROR_PREFIX

* acme: deprecate .messages.Directory.register

* acme: clean up deprecations

* dont use unscoped filterwarnings

* change deprecation approach for acme.fields

* warn on non-string keys in acme.messages.Directory

* remove leaked filterwarnings in BackwardsCompatibleClientV2Test

* remove non-string lookups of acme.messages.Directory
This commit is contained in:
alexzorin 2022-09-02 23:55:04 +10:00 committed by GitHub
parent f7e61edcb2
commit c20d40ddba
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 211 additions and 93 deletions

View file

@ -56,7 +56,9 @@ class ChallengeResponse(ResourceMixin, TypeMixin, jose.TypedJSONObjectWithFields
"""ACME challenge response."""
TYPES: Dict[str, Type['ChallengeResponse']] = {}
resource_type = 'challenge'
resource: str = fields.resource(resource_type)
with warnings.catch_warnings():
warnings.filterwarnings('ignore', 'resource attribute in acme.fields', DeprecationWarning)
resource: str = fields.resource(resource_type)
class UnrecognizedChallenge(Challenge):

View file

@ -306,7 +306,7 @@ class Client(ClientBase):
"""
new_reg = messages.NewRegistration() if new_reg is None else new_reg
response = self._post(self.directory[new_reg], new_reg)
response = self._post(self.directory['new-reg'], new_reg)
# TODO: handle errors
assert response.status_code == http_client.CREATED
@ -612,7 +612,7 @@ class Client(ClientBase):
:raises .ClientError: If revocation is unsuccessful.
"""
self._revoke(cert, rsn, self.directory[messages.Revocation])
self._revoke(cert, rsn, self.directory['revoke-cert'])
class ClientV2(ClientBase):

View file

@ -1,8 +1,12 @@
"""ACME JSON fields."""
import datetime
from typing import Any
import logging
import sys
from types import ModuleType
from typing import Any
from typing import cast
from typing import List
import warnings
import josepy as jose
import pyrfc3339
@ -52,7 +56,11 @@ class RFC3339Field(jose.Field):
class Resource(jose.Field):
"""Resource MITM field."""
"""Resource MITM field.
.. deprecated: 1.30.0
"""
def __init__(self, resource_type: str, *args: Any, **kwargs: Any) -> None:
self.resource_type = resource_type
@ -78,5 +86,40 @@ def rfc3339(json_name: str, omitempty: bool = False) -> Any:
def resource(resource_type: str) -> Any:
"""Generates a type-friendly Resource field."""
"""Generates a type-friendly Resource field.
.. deprecated: 1.30.0
"""
return Resource(resource_type)
# This class takes a similar approach to the cryptography project to deprecate attributes
# in public modules. See the _ModuleWithDeprecation class here:
# https://github.com/pyca/cryptography/blob/91105952739442a74582d3e62b3d2111365b0dc7/src/cryptography/utils.py#L129
class _FieldsDeprecationModule: # pragma: no cover
"""
Internal class delegating to a module, and displaying warnings when
module attributes deprecated in acme.fields are accessed.
"""
def __init__(self, module: ModuleType) -> None:
self.__dict__['_module'] = module
def __getattr__(self, attr: str) -> None:
if attr in ('Resource', 'resource'):
warnings.warn('{0} attribute in acme.fields module is deprecated '
'and will be removed soon.'.format(attr),
DeprecationWarning, stacklevel=2)
return getattr(self._module, attr)
def __setattr__(self, attr: str, value: Any) -> None:
setattr(self._module, attr, value)
def __delattr__(self, attr: str) -> None:
delattr(self._module, attr)
def __dir__(self) -> List[str]:
return ['_module'] + dir(self._module)
sys.modules[__name__] = cast(ModuleType, _FieldsDeprecationModule(sys.modules[__name__]))

View file

@ -2,7 +2,9 @@
import datetime
from collections.abc import Hashable
import json
from types import ModuleType
from typing import Any
from typing import cast
from typing import Dict
from typing import Iterator
from typing import List
@ -14,6 +16,7 @@ from typing import Type
from typing import TYPE_CHECKING
from typing import TypeVar
from typing import Union
import sys
import warnings
import josepy as jose
@ -24,7 +27,7 @@ from acme import fields
from acme import jws
from acme import util
with warnings.catch_warnings():
warnings.filterwarnings("ignore", category=DeprecationWarning)
warnings.filterwarnings("ignore", ".*acme.mixins", category=DeprecationWarning)
from acme.mixins import ResourceMixin
if TYPE_CHECKING:
@ -277,6 +280,10 @@ class Directory(jose.JSONDeSerializable):
def register(cls,
resource_body_cls: Type[GenericHasResourceType]) -> Type[GenericHasResourceType]:
"""Register resource."""
warnings.warn(
"acme.messages.Directory.register is deprecated and will be removed in the next "
"major release of Certbot", DeprecationWarning, stacklevel=2
)
resource_type = resource_body_cls.resource_type
assert resource_type not in cls._REGISTERED_TYPES
cls._REGISTERED_TYPES[resource_type] = resource_body_cls
@ -295,6 +302,12 @@ class Directory(jose.JSONDeSerializable):
raise AttributeError(str(error))
def __getitem__(self, name: Union[str, HasResourceType, Type[HasResourceType]]) -> Any:
if not isinstance(name, str):
warnings.warn(
"Looking up acme.messages.Directory resources by non-string keys is deprecated "
"and will be removed in the next major release of Certbot",
DeprecationWarning, stacklevel=2
)
try:
return self._jobj[self._canon_key(name)]
except KeyError:
@ -462,19 +475,6 @@ class Registration(ResourceBody):
return self._filter_contact(self.email_prefix)
@Directory.register
class NewRegistration(ResourceMixin, Registration):
"""New registration."""
resource_type = 'new-reg'
resource: str = fields.resource(resource_type)
class UpdateRegistration(ResourceMixin, Registration):
"""Update registration."""
resource_type = 'reg'
resource: str = fields.resource(resource_type)
class RegistrationResource(ResourceWithURI):
"""Registration Resource.
@ -616,14 +616,14 @@ class Authorization(ResourceBody):
"""
warnings.warn(
"acme.messages.Authorization.combinations is deprecated and will be "
"removed in a future release.", DeprecationWarning)
"removed in a future release.", DeprecationWarning, stacklevel=2)
return self._combinations
@combinations.setter
def combinations(self, combos: Tuple[Tuple[int, ...], ...]) -> None: # pragma: no cover
warnings.warn(
"acme.messages.Authorization.combinations is deprecated and will be "
"removed in a future release.", DeprecationWarning)
"removed in a future release.", DeprecationWarning, stacklevel=2)
self._combinations = combos
@property
@ -635,22 +635,11 @@ class Authorization(ResourceBody):
"""
warnings.warn(
"acme.messages.Authorization.resolved_combinations is deprecated and will be "
"removed in a future release.", DeprecationWarning)
return tuple(tuple(self.challenges[idx] for idx in combo)
for combo in self.combinations) # pylint: disable=not-an-iterable
@Directory.register
class NewAuthorization(ResourceMixin, Authorization):
"""New authorization."""
resource_type = 'new-authz'
resource: str = fields.resource(resource_type)
class UpdateAuthorization(ResourceMixin, Authorization):
"""Update authorization."""
resource_type = 'authz'
resource: str = fields.resource(resource_type)
"removed in a future release.", DeprecationWarning, stacklevel=2)
with warnings.catch_warnings():
warnings.filterwarnings('ignore', '.*combinations', DeprecationWarning)
return tuple(tuple(self.challenges[idx] for idx in combo)
for combo in self.combinations) # pylint: disable=not-an-iterable
class AuthorizationResource(ResourceWithURI):
@ -664,19 +653,6 @@ class AuthorizationResource(ResourceWithURI):
new_cert_uri: str = jose.field('new_cert_uri', omitempty=True)
@Directory.register
class CertificateRequest(ResourceMixin, jose.JSONObjectWithFields):
"""ACME new-cert request.
:ivar jose.ComparableX509 csr:
`OpenSSL.crypto.X509Req` wrapped in `.ComparableX509`
"""
resource_type = 'new-cert'
resource: str = fields.resource(resource_type)
csr: jose.ComparableX509 = jose.field('csr', decoder=jose.decode_csr, encoder=jose.encode_csr)
class CertificateResource(ResourceWithURI):
"""Certificate Resource.
@ -690,21 +666,6 @@ class CertificateResource(ResourceWithURI):
authzrs: Tuple[AuthorizationResource, ...] = jose.field('authzrs')
@Directory.register
class Revocation(ResourceMixin, jose.JSONObjectWithFields):
"""Revocation message.
:ivar jose.ComparableX509 certificate: `OpenSSL.crypto.X509` wrapped in
`jose.ComparableX509`
"""
resource_type = 'revoke-cert'
resource: str = fields.resource(resource_type)
certificate: jose.ComparableX509 = jose.field(
'certificate', decoder=jose.decode_cert, encoder=jose.encode_cert)
reason: int = jose.field('reason')
class Order(ResourceBody):
"""Order Resource Body.
@ -756,7 +717,98 @@ class OrderResource(ResourceWithURI):
omitempty=True)
@Directory.register
class NewOrder(Order):
"""New order."""
resource_type = 'new-order'
with warnings.catch_warnings():
warnings.filterwarnings("ignore", "acme.messages.Directory.register", DeprecationWarning)
warnings.filterwarnings("ignore", "resource attribute in acme.fields", DeprecationWarning)
@Directory.register
class NewOrder(Order):
"""New order."""
resource_type = 'new-order'
@Directory.register
class Revocation(ResourceMixin, jose.JSONObjectWithFields):
"""Revocation message.
:ivar jose.ComparableX509 certificate: `OpenSSL.crypto.X509` wrapped in
`jose.ComparableX509`
"""
resource_type = 'revoke-cert'
resource: str = fields.resource(resource_type)
certificate: jose.ComparableX509 = jose.field(
'certificate', decoder=jose.decode_cert, encoder=jose.encode_cert)
reason: int = jose.field('reason')
@Directory.register
class CertificateRequest(ResourceMixin, jose.JSONObjectWithFields):
"""ACME new-cert request.
:ivar jose.ComparableX509 csr:
`OpenSSL.crypto.X509Req` wrapped in `.ComparableX509`
"""
resource_type = 'new-cert'
resource: str = fields.resource(resource_type)
csr: jose.ComparableX509 = jose.field('csr', decoder=jose.decode_csr,
encoder=jose.encode_csr)
@Directory.register
class NewAuthorization(ResourceMixin, Authorization):
"""New authorization."""
resource_type = 'new-authz'
resource: str = fields.resource(resource_type)
class UpdateAuthorization(ResourceMixin, Authorization):
"""Update authorization."""
resource_type = 'authz'
resource: str = fields.resource(resource_type)
@Directory.register
class NewRegistration(ResourceMixin, Registration):
"""New registration."""
resource_type = 'new-reg'
resource: str = fields.resource(resource_type)
class UpdateRegistration(ResourceMixin, Registration):
"""Update registration."""
resource_type = 'reg'
resource: str = fields.resource(resource_type)
# This class takes a similar approach to the cryptography project to deprecate attributes
# in public modules. See the _ModuleWithDeprecation class here:
# https://github.com/pyca/cryptography/blob/91105952739442a74582d3e62b3d2111365b0dc7/src/cryptography/utils.py#L129
class _MessagesDeprecationModule: # pragma: no cover
"""
Internal class delegating to a module, and displaying warnings when
module attributes deprecated in acme.messages are accessed.
"""
def __init__(self, module: ModuleType) -> None:
self.__dict__['_module'] = module
def __getattr__(self, attr: str) -> None:
if attr == 'OLD_ERROR_PREFIX':
warnings.warn('{0} attribute in acme.messages module is deprecated '
'and will be removed soon.'.format(attr),
DeprecationWarning, stacklevel=2)
return getattr(self._module, attr)
def __setattr__(self, attr: str, value: Any) -> None:
setattr(self._module, attr, value)
def __delattr__(self, attr: str) -> None:
delattr(self._module, attr)
def __dir__(self) -> List[str]:
return ['_module'] + dir(self._module)
# Patching ourselves to warn about acme.messages.OLD_ERROR_PREFIX deprecation and planned removal.
sys.modules[__name__] = cast(ModuleType, _MessagesDeprecationModule(sys.modules[__name__]))

View file

@ -3,11 +3,11 @@
import copy
import datetime
import http.client as http_client
import ipaddress
import json
import unittest
from typing import Dict
from unittest import mock
import warnings
import josepy as jose
import OpenSSL
@ -17,9 +17,17 @@ from acme import challenges
from acme import errors
from acme import jws as acme_jws
from acme import messages
from acme.client import ClientNetwork
from acme.client import ClientV2
from acme.mixins import VersionedLEACMEMixin
import messages_test
import test_util
# Remove the following in Certbot 2.0:
with warnings.catch_warnings():
warnings.filterwarnings('ignore', '.* in acme.client', DeprecationWarning)
from acme.client import BackwardsCompatibleClientV2
from acme.client import Client
CERT_DER = test_util.load_vector('cert.der')
CERT_SAN_PEM = test_util.load_vector('cert-san.pem')
@ -87,12 +95,17 @@ class ClientTestBase(unittest.TestCase):
# Reason code for revocation
self.rsn = 1
class BackwardsCompatibleClientV2Test(ClientTestBase):
"""Tests for acme.client.BackwardsCompatibleClientV2."""
def setUp(self):
super().setUp()
# For some reason, required to suppress warnings on mock.patch('acme.client.Client')
self.warning_cap = warnings.catch_warnings()
self.warning_cap.__enter__()
warnings.filterwarnings('ignore', '.*acme.client', DeprecationWarning)
# contains a loaded cert
self.certr = messages.CertificateResource(
body=messages_test.CERT)
@ -114,15 +127,17 @@ class BackwardsCompatibleClientV2Test(ClientTestBase):
self.orderr = messages.OrderResource(
csr_pem=CSR_SAN_PEM)
def tearDown(self) -> None:
self.warning_cap.__exit__()
return super().tearDown()
def _init(self):
uri = 'http://www.letsencrypt-demo.org/directory'
from acme.client import BackwardsCompatibleClientV2
return BackwardsCompatibleClientV2(net=self.net,
key=KEY, server=uri)
def test_init_downloads_directory(self):
uri = 'http://www.letsencrypt-demo.org/directory'
from acme.client import BackwardsCompatibleClientV2
BackwardsCompatibleClientV2(net=self.net,
key=KEY, server=uri)
self.net.get.assert_called_once_with(uri)
@ -336,13 +351,11 @@ class ClientTest(ClientTestBase):
uri='https://www.letsencrypt-demo.org/acme/cert/1',
cert_chain_uri='https://www.letsencrypt-demo.org/ca')
from acme.client import Client
self.client = Client(
directory=self.directory, key=KEY, alg=jose.RS256, net=self.net)
def test_init_downloads_directory(self):
uri = 'http://www.letsencrypt-demo.org/directory'
from acme.client import Client
self.client = Client(
directory=uri, key=KEY, alg=jose.RS256, net=self.net)
self.net.get.assert_called_once_with(uri)
@ -351,7 +364,6 @@ class ClientTest(ClientTestBase):
def test_init_without_net(self, mock_net):
mock_net.return_value = mock.sentinel.net
alg = jose.RS256
from acme.client import Client
self.client = Client(
directory=self.directory, key=KEY, alg=alg)
mock_net.called_once_with(KEY, alg=alg, verify_ssl=True)
@ -723,7 +735,6 @@ class ClientV2Test(ClientTestBase):
self.directory = DIRECTORY_V2
from acme.client import ClientV2
self.client = ClientV2(self.directory, self.net)
self.new_reg = self.new_reg.update(terms_of_service_agreed=True)
@ -948,7 +959,6 @@ class ClientNetworkTest(unittest.TestCase):
self.verify_ssl = mock.MagicMock()
self.wrap_in_jws = mock.MagicMock(return_value=mock.sentinel.wrapped)
from acme.client import ClientNetwork
self.net = ClientNetwork(
key=KEY, alg=jose.RS256, verify_ssl=self.verify_ssl,
user_agent='acme-python-test')
@ -1179,7 +1189,6 @@ class ClientNetworkWithMockedResponseTest(unittest.TestCase):
"""Tests for acme.client.ClientNetwork which mock out response."""
def setUp(self):
from acme.client import ClientNetwork
self.net = ClientNetwork(key=None, alg=None)
self.response = mock.MagicMock(ok=True, status_code=http_client.OK)
@ -1342,7 +1351,6 @@ class ClientNetworkSourceAddressBindingTest(unittest.TestCase):
self.source_address = "8.8.8.8"
def test_source_address_set(self):
from acme.client import ClientNetwork
with mock.patch('warnings.warn') as mock_warn:
net = ClientNetwork(key=None, alg=None, source_address=self.source_address)
mock_warn.assert_called_once()
@ -1353,7 +1361,6 @@ class ClientNetworkSourceAddressBindingTest(unittest.TestCase):
def test_behavior_assumption(self):
"""This is a test that guardrails the HTTPAdapter behavior so that if the default for
a Session() changes, the assumptions here aren't violated silently."""
from acme.client import ClientNetwork
# Source address not specified, so the default adapter type should be bound -- this
# test should fail if the default adapter type is changed by requests
net = ClientNetwork(key=None, alg=None)

View file

@ -1,6 +1,7 @@
"""Tests for acme.fields."""
import datetime
import unittest
import warnings
import josepy as jose
import pytz
@ -58,8 +59,10 @@ class ResourceTest(unittest.TestCase):
"""Tests for acme.fields.Resource."""
def setUp(self):
from acme.fields import Resource
self.field = Resource('x')
with warnings.catch_warnings():
warnings.filterwarnings('ignore', '.*Resource', DeprecationWarning)
from acme.fields import Resource
self.field = Resource('x')
def test_decode_good(self):
self.assertEqual('x', self.field.decode('x'))

View file

@ -2,6 +2,7 @@
from typing import Dict
import unittest
from unittest import mock
import warnings
import josepy as jose
@ -150,8 +151,10 @@ class DirectoryTest(unittest.TestCase):
def test_getitem(self):
self.assertEqual('reg', self.dir['new-reg'])
from acme.messages import NewRegistration
self.assertEqual('reg', self.dir[NewRegistration])
self.assertEqual('reg', self.dir[NewRegistration()])
with warnings.catch_warnings():
warnings.filterwarnings('ignore', '.* non-string keys', DeprecationWarning)
self.assertEqual('reg', self.dir[NewRegistration])
self.assertEqual('reg', self.dir[NewRegistration()])
def test_getitem_fails_with_key_error(self):
self.assertRaises(KeyError, self.dir.__getitem__, 'foo')
@ -407,10 +410,12 @@ class AuthorizationTest(unittest.TestCase):
hash(Authorization.from_json(self.jobj_from))
def test_resolved_combinations(self):
self.assertEqual(self.authz.resolved_combinations, (
(self.challbs[0],),
(self.challbs[1],),
))
with warnings.catch_warnings():
warnings.filterwarnings('ignore', '.*resolved_combinations', DeprecationWarning)
self.assertEqual(self.authz.resolved_combinations, (
(self.challbs[0],),
(self.challbs[1],),
))
class AuthorizationResourceTest(unittest.TestCase):

View file

@ -11,8 +11,14 @@ Certbot adheres to [Semantic Versioning](https://semver.org/).
### Changed
* `acme.client.ClientBase`, `acme.messages.Authorization.resolved_combinations`,
`acme.messages.Authorization.combinations` and `acme.mixins` are deprecated and
will be removed in a future release.
`acme.messages.Authorization.combinations`, `acme.mixins`, `acme.fields.resource`,
and `acme.fields.Resource` are deprecated and will be removed in a future release.
* `acme.messages.OLD_ERROR_PREFIX` (`urn:acme:error:`) is deprecated and support for
the old ACME error prefix in Certbot will be removed in the next major release of
Certbot.
* `acme.messages.Directory.register` is deprecated and will be removed in the next
major release of Certbot. Furthermore, `.Directory` will only support lookups
by the exact resource name string in the ACME directory (e.g. `directory['newOrder']`).
* The `certbot-dns-cloudxns` plugin is now deprecated and will be removed in the
next major release of Certbot.
* The `source_address` argument for `acme.client.ClientNetwork` is deprecated