mirror of
https://github.com/certbot/certbot.git
synced 2026-07-30 18:04:31 +02:00
Impelment account deactivation [revision requested] (#3571)
Impelment account deactivation
This commit is contained in:
committed by
Brad Warren
parent
0fa307806e
commit
49d46ef99a
@@ -33,3 +33,5 @@ tags
|
|||||||
tests/letstest/letest-*/
|
tests/letstest/letest-*/
|
||||||
tests/letstest/*.pem
|
tests/letstest/*.pem
|
||||||
tests/letstest/venv/
|
tests/letstest/venv/
|
||||||
|
|
||||||
|
.venv
|
||||||
|
|||||||
+14
-2
@@ -132,12 +132,24 @@ class Client(object): # pylint: disable=too-many-instance-attributes
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
update = regr.body if update is None else update
|
update = regr.body if update is None else update
|
||||||
updated_regr = self._send_recv_regr(
|
body = messages.UpdateRegistration(**dict(update))
|
||||||
regr, body=messages.UpdateRegistration(**dict(update)))
|
updated_regr = self._send_recv_regr(regr, body=body)
|
||||||
if updated_regr != regr:
|
if updated_regr != regr:
|
||||||
raise errors.UnexpectedUpdate(regr)
|
raise errors.UnexpectedUpdate(regr)
|
||||||
return updated_regr
|
return updated_regr
|
||||||
|
|
||||||
|
def deactivate_registration(self, regr):
|
||||||
|
"""Deactivate registration.
|
||||||
|
|
||||||
|
:param messages.RegistrationResource regr: The Registration Resource
|
||||||
|
to be deactivated.
|
||||||
|
|
||||||
|
:returns: The Registration resource that was deactivated.
|
||||||
|
:rtype: `.RegistrationResource`
|
||||||
|
|
||||||
|
"""
|
||||||
|
return self.update_registration(regr, update={'status': 'deactivated'})
|
||||||
|
|
||||||
def query_registration(self, regr):
|
def query_registration(self, regr):
|
||||||
"""Query server about registration.
|
"""Query server about registration.
|
||||||
|
|
||||||
|
|||||||
@@ -124,6 +124,20 @@ class ClientTest(unittest.TestCase):
|
|||||||
self.assertRaises(
|
self.assertRaises(
|
||||||
errors.UnexpectedUpdate, self.client.update_registration, self.regr)
|
errors.UnexpectedUpdate, self.client.update_registration, self.regr)
|
||||||
|
|
||||||
|
def test_deactivate_account(self):
|
||||||
|
self.response.headers['Location'] = self.regr.uri
|
||||||
|
self.response.json.return_value = self.regr.body.to_json()
|
||||||
|
self.assertEqual(self.regr,
|
||||||
|
self.client.deactivate_registration(self.regr))
|
||||||
|
|
||||||
|
def test_deactivate_account_bad_registration_returned(self):
|
||||||
|
self.response.headers['Location'] = self.regr.uri
|
||||||
|
self.response.json.return_value = "some wrong registration thing"
|
||||||
|
self.assertRaises(
|
||||||
|
errors.UnexpectedUpdate,
|
||||||
|
self.client.deactivate_registration,
|
||||||
|
self.regr)
|
||||||
|
|
||||||
def test_query_registration(self):
|
def test_query_registration(self):
|
||||||
self.response.json.return_value = self.regr.body.to_json()
|
self.response.json.return_value = self.regr.body.to_json()
|
||||||
self.assertEqual(self.regr, self.client.query_registration(self.regr))
|
self.assertEqual(self.regr, self.client.query_registration(self.regr))
|
||||||
|
|||||||
@@ -250,6 +250,7 @@ class Registration(ResourceBody):
|
|||||||
agreement = jose.Field('agreement', omitempty=True)
|
agreement = jose.Field('agreement', omitempty=True)
|
||||||
authorizations = jose.Field('authorizations', omitempty=True)
|
authorizations = jose.Field('authorizations', omitempty=True)
|
||||||
certificates = jose.Field('certificates', omitempty=True)
|
certificates = jose.Field('certificates', omitempty=True)
|
||||||
|
status = jose.Field('status', omitempty=True)
|
||||||
|
|
||||||
class Authorizations(jose.JSONObjectWithFields):
|
class Authorizations(jose.JSONObjectWithFields):
|
||||||
"""Authorizations granted to Account in the process of registration.
|
"""Authorizations granted to Account in the process of registration.
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import datetime
|
|||||||
import hashlib
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
import socket
|
import socket
|
||||||
|
|
||||||
from cryptography.hazmat.primitives import serialization
|
from cryptography.hazmat.primitives import serialization
|
||||||
@@ -197,6 +198,18 @@ class AccountFileStorage(interfaces.AccountStorage):
|
|||||||
"""
|
"""
|
||||||
self._save(account, regr_only=True)
|
self._save(account, regr_only=True)
|
||||||
|
|
||||||
|
def delete(self, account_id):
|
||||||
|
"""Delete registration info from disk
|
||||||
|
|
||||||
|
:param account_id: id of account which should be deleted
|
||||||
|
|
||||||
|
"""
|
||||||
|
account_dir_path = self._account_dir_path(account_id)
|
||||||
|
if not os.path.isdir(account_dir_path):
|
||||||
|
raise errors.AccountNotFound(
|
||||||
|
"Account at %s does not exist" % account_dir_path)
|
||||||
|
shutil.rmtree(account_dir_path)
|
||||||
|
|
||||||
def _save(self, account, regr_only):
|
def _save(self, account, regr_only):
|
||||||
account_dir_path = self._account_dir_path(account.id)
|
account_dir_path = self._account_dir_path(account.id)
|
||||||
util.make_or_verify_dir(account_dir_path, 0o700, os.geteuid(),
|
util.make_or_verify_dir(account_dir_path, 0o700, os.geteuid(),
|
||||||
|
|||||||
+9
-2
@@ -366,6 +366,10 @@ VERB_HELP = [
|
|||||||
"short": "Register for account with Let's Encrypt / other ACME server",
|
"short": "Register for account with Let's Encrypt / other ACME server",
|
||||||
"opts": "Options for account registration & modification"
|
"opts": "Options for account registration & modification"
|
||||||
}),
|
}),
|
||||||
|
("unregister", {
|
||||||
|
"short": "Irrevocably deactivate your account",
|
||||||
|
"opts": "Options for account deactivation."
|
||||||
|
}),
|
||||||
("install", {
|
("install", {
|
||||||
"short": "Install an arbitrary cert in a server",
|
"short": "Install an arbitrary cert in a server",
|
||||||
"opts": "Options for modifying how a cert is deployed"
|
"opts": "Options for modifying how a cert is deployed"
|
||||||
@@ -414,6 +418,7 @@ class HelpfulArgumentParser(object):
|
|||||||
"install": main.install,
|
"install": main.install,
|
||||||
"plugins": main.plugins_cmd,
|
"plugins": main.plugins_cmd,
|
||||||
"register": main.register,
|
"register": main.register,
|
||||||
|
"unregister": main.unregister,
|
||||||
"renew": main.renew,
|
"renew": main.renew,
|
||||||
"revoke": main.revoke,
|
"revoke": main.revoke,
|
||||||
"rollback": main.rollback,
|
"rollback": main.rollback,
|
||||||
@@ -871,7 +876,9 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
|
|||||||
help="With the register verb, indicates that details associated "
|
help="With the register verb, indicates that details associated "
|
||||||
"with an existing registration, such as the e-mail address, "
|
"with an existing registration, such as the e-mail address, "
|
||||||
"should be updated, rather than registering a new account.")
|
"should be updated, rather than registering a new account.")
|
||||||
helpful.add(["register", "automation"], "-m", "--email", help=config_help("email"))
|
helpful.add(
|
||||||
|
["register", "unregister", "automation"], "-m", "--email",
|
||||||
|
help=config_help("email"))
|
||||||
helpful.add(
|
helpful.add(
|
||||||
["automation", "certonly", "run"],
|
["automation", "certonly", "run"],
|
||||||
"--keep-until-expiring", "--keep", "--reinstall",
|
"--keep-until-expiring", "--keep", "--reinstall",
|
||||||
@@ -913,7 +920,7 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
|
|||||||
"automation", "--agree-tos", dest="tos", action="store_true",
|
"automation", "--agree-tos", dest="tos", action="store_true",
|
||||||
help="Agree to the ACME Subscriber Agreement (default: Ask)")
|
help="Agree to the ACME Subscriber Agreement (default: Ask)")
|
||||||
helpful.add(
|
helpful.add(
|
||||||
"automation", "--account", metavar="ACCOUNT_ID",
|
["unregister", "automation"], "--account", metavar="ACCOUNT_ID",
|
||||||
help="Account ID to use")
|
help="Account ID to use")
|
||||||
helpful.add(
|
helpful.add(
|
||||||
"automation", "--duplicate", dest="duplicate", action="store_true",
|
"automation", "--duplicate", dest="duplicate", action="store_true",
|
||||||
|
|||||||
+1
-1
@@ -172,7 +172,7 @@ def perform_registration(acme, config):
|
|||||||
|
|
||||||
|
|
||||||
class Client(object):
|
class Client(object):
|
||||||
"""ACME protocol client.
|
"""Certbot's client.
|
||||||
|
|
||||||
:ivar .IConfig config: Client configuration.
|
:ivar .IConfig config: Client configuration.
|
||||||
:ivar .Account account: Account registered with `register`.
|
:ivar .Account account: Account registered with `register`.
|
||||||
|
|||||||
+32
-3
@@ -406,6 +406,35 @@ def _init_le_client(config, authenticator, installer):
|
|||||||
return client.Client(config, acc, authenticator, installer, acme=acme)
|
return client.Client(config, acc, authenticator, installer, acme=acme)
|
||||||
|
|
||||||
|
|
||||||
|
def unregister(config, unused_plugins):
|
||||||
|
"""Deactivate account on server"""
|
||||||
|
account_storage = account.AccountFileStorage(config)
|
||||||
|
accounts = account_storage.find_all()
|
||||||
|
reporter_util = zope.component.getUtility(interfaces.IReporter)
|
||||||
|
|
||||||
|
if not accounts:
|
||||||
|
return "Could not find existing account to deactivate."
|
||||||
|
yesno = zope.component.getUtility(interfaces.IDisplay).yesno
|
||||||
|
prompt = ("Are you sure you would like to irrevocably deactivate "
|
||||||
|
"your account?")
|
||||||
|
wants_deactivate = yesno(prompt, yes_label='Deactivate', no_label='Abort',
|
||||||
|
default=True)
|
||||||
|
|
||||||
|
if not wants_deactivate:
|
||||||
|
return "Deactivation aborted."
|
||||||
|
|
||||||
|
acc, acme = _determine_account(config)
|
||||||
|
acme_client = client.Client(config, acc, None, None, acme=acme)
|
||||||
|
|
||||||
|
# delete on boulder
|
||||||
|
acme_client.acme.deactivate_registration(acc.regr)
|
||||||
|
account_files = account.AccountFileStorage(config)
|
||||||
|
# delete local account files
|
||||||
|
account_files.delete(config.account)
|
||||||
|
|
||||||
|
reporter_util.add_message("Account deactivated.", reporter_util.MEDIUM_PRIORITY)
|
||||||
|
|
||||||
|
|
||||||
def register(config, unused_plugins):
|
def register(config, unused_plugins):
|
||||||
"""Create or modify accounts on the server."""
|
"""Create or modify accounts on the server."""
|
||||||
|
|
||||||
@@ -413,6 +442,8 @@ def register(config, unused_plugins):
|
|||||||
# exist or not.
|
# exist or not.
|
||||||
account_storage = account.AccountFileStorage(config)
|
account_storage = account.AccountFileStorage(config)
|
||||||
accounts = account_storage.find_all()
|
accounts = account_storage.find_all()
|
||||||
|
reporter_util = zope.component.getUtility(interfaces.IReporter)
|
||||||
|
add_msg = lambda m: reporter_util.add_message(m, reporter_util.MEDIUM_PRIORITY)
|
||||||
|
|
||||||
# registering a new account
|
# registering a new account
|
||||||
if not config.update_registration:
|
if not config.update_registration:
|
||||||
@@ -443,9 +474,7 @@ def register(config, unused_plugins):
|
|||||||
acc.regr = acme_client.acme.update_registration(acc.regr.update(
|
acc.regr = acme_client.acme.update_registration(acc.regr.update(
|
||||||
body=acc.regr.body.update(contact=('mailto:' + config.email,))))
|
body=acc.regr.body.update(contact=('mailto:' + config.email,))))
|
||||||
account_storage.save_regr(acc)
|
account_storage.save_regr(acc)
|
||||||
reporter_util = zope.component.getUtility(interfaces.IReporter)
|
add_msg("Your e-mail address was updated to {0}.".format(config.email))
|
||||||
msg = "Your e-mail address was updated to {0}.".format(config.email)
|
|
||||||
reporter_util.add_message(msg, reporter_util.MEDIUM_PRIORITY)
|
|
||||||
|
|
||||||
|
|
||||||
def install(config, plugins):
|
def install(config, plugins):
|
||||||
|
|||||||
@@ -190,6 +190,14 @@ class AccountFileStorageTest(unittest.TestCase):
|
|||||||
self.assertRaises(
|
self.assertRaises(
|
||||||
errors.AccountStorageError, self.storage.save, self.acc)
|
errors.AccountStorageError, self.storage.save, self.acc)
|
||||||
|
|
||||||
|
def test_delete(self):
|
||||||
|
self.storage.save(self.acc)
|
||||||
|
self.storage.delete(self.acc.id)
|
||||||
|
self.assertRaises(errors.AccountNotFound, self.storage.load, self.acc.id)
|
||||||
|
|
||||||
|
def test_delete_no_account(self):
|
||||||
|
self.assertRaises(errors.AccountNotFound, self.storage.delete, self.acc.id)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main() # pragma: no cover
|
unittest.main() # pragma: no cover
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
"""Tests for certbot.main."""
|
"""Tests for certbot.main."""
|
||||||
|
# pylint: disable=too-many-lines
|
||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
|
|
||||||
import itertools
|
import itertools
|
||||||
@@ -1164,6 +1165,68 @@ class MainTest(unittest.TestCase): # pylint: disable=too-many-public-methods
|
|||||||
email in mock_utility().add_message.call_args[0][0])
|
email in mock_utility().add_message.call_args[0][0])
|
||||||
|
|
||||||
|
|
||||||
|
class UnregisterTest(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.patchers = {
|
||||||
|
'_determine_account': mock.patch('certbot.main._determine_account'),
|
||||||
|
'account': mock.patch('certbot.main.account'),
|
||||||
|
'client': mock.patch('certbot.main.client'),
|
||||||
|
'get_utility': test_util.patch_get_utility()}
|
||||||
|
self.mocks = dict((k, v.start()) for k, v in self.patchers.items())
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
for patch in self.patchers.values():
|
||||||
|
patch.stop()
|
||||||
|
|
||||||
|
def test_abort_unregister(self):
|
||||||
|
self.mocks['account'].AccountFileStorage.return_value = mock.Mock()
|
||||||
|
|
||||||
|
util_mock = self.mocks['get_utility'].return_value
|
||||||
|
util_mock.yesno.return_value = False
|
||||||
|
|
||||||
|
config = mock.Mock()
|
||||||
|
unused_plugins = mock.Mock()
|
||||||
|
|
||||||
|
res = main.unregister(config, unused_plugins)
|
||||||
|
self.assertEqual(res, "Deactivation aborted.")
|
||||||
|
|
||||||
|
def test_unregister(self):
|
||||||
|
mocked_storage = mock.MagicMock()
|
||||||
|
mocked_storage.find_all.return_value = ["an account"]
|
||||||
|
|
||||||
|
self.mocks['account'].AccountFileStorage.return_value = mocked_storage
|
||||||
|
self.mocks['_determine_account'].return_value = (mock.MagicMock(), "foo")
|
||||||
|
|
||||||
|
acme_client = mock.MagicMock()
|
||||||
|
self.mocks['client'].Client.return_value = acme_client
|
||||||
|
|
||||||
|
config = mock.MagicMock()
|
||||||
|
unused_plugins = mock.MagicMock()
|
||||||
|
|
||||||
|
res = main.unregister(config, unused_plugins)
|
||||||
|
|
||||||
|
self.assertTrue(res is None)
|
||||||
|
self.assertTrue(acme_client.acme.deactivate_registration.called)
|
||||||
|
m = "Account deactivated."
|
||||||
|
self.assertTrue(m in self.mocks['get_utility']().add_message.call_args[0][0])
|
||||||
|
|
||||||
|
def test_unregister_no_account(self):
|
||||||
|
mocked_storage = mock.MagicMock()
|
||||||
|
mocked_storage.find_all.return_value = []
|
||||||
|
self.mocks['account'].AccountFileStorage.return_value = mocked_storage
|
||||||
|
|
||||||
|
acme_client = mock.MagicMock()
|
||||||
|
self.mocks['client'].Client.return_value = acme_client
|
||||||
|
|
||||||
|
config = mock.MagicMock()
|
||||||
|
unused_plugins = mock.MagicMock()
|
||||||
|
|
||||||
|
res = main.unregister(config, unused_plugins)
|
||||||
|
m = "Could not find existing account to deactivate."
|
||||||
|
self.assertEqual(res, m)
|
||||||
|
self.assertFalse(acme_client.acme.deactivate_registration.called)
|
||||||
|
|
||||||
|
|
||||||
class TestHandleException(unittest.TestCase):
|
class TestHandleException(unittest.TestCase):
|
||||||
"""Test main._handle_exception"""
|
"""Test main._handle_exception"""
|
||||||
@mock.patch('certbot.main.sys')
|
@mock.patch('certbot.main.sys')
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
# Note: this script is called by Boulder integration test suite!
|
# Note: this script is called by Boulder integration test suite!
|
||||||
|
|
||||||
. ./tests/integration/_common.sh
|
. ./tests/integration/_common.sh
|
||||||
export PATH="/usr/sbin:$PATH" # /usr/sbin/nginx
|
export PATH="$PATH:/usr/sbin" # /usr/sbin/nginx
|
||||||
|
|
||||||
export GOPATH="${GOPATH:-/tmp/go}"
|
export GOPATH="${GOPATH:-/tmp/go}"
|
||||||
export PATH="$GOPATH/bin:$PATH"
|
export PATH="$GOPATH/bin:$PATH"
|
||||||
@@ -161,7 +161,8 @@ common revoke --cert-path "$root/conf/live/le.wtf/cert.pem"
|
|||||||
common revoke --cert-path "$root/conf/live/le1.wtf/cert.pem"
|
common revoke --cert-path "$root/conf/live/le1.wtf/cert.pem"
|
||||||
# revoke by cert key
|
# revoke by cert key
|
||||||
common revoke --cert-path "$root/conf/live/le2.wtf/cert.pem" \
|
common revoke --cert-path "$root/conf/live/le2.wtf/cert.pem" \
|
||||||
--key-path "$root/conf/live/le2.wtf/privkey.pem"
|
--key-path "$root/conf/live/le2.wtf/privkey.pem"
|
||||||
|
|
||||||
# Get new certs to test revoke with a reason, by account and by cert key
|
# Get new certs to test revoke with a reason, by account and by cert key
|
||||||
common --domains le1.wtf
|
common --domains le1.wtf
|
||||||
common revoke --cert-path "$root/conf/live/le1.wtf/cert.pem" \
|
common revoke --cert-path "$root/conf/live/le1.wtf/cert.pem" \
|
||||||
@@ -170,6 +171,9 @@ common --domains le2.wtf
|
|||||||
common revoke --cert-path "$root/conf/live/le2.wtf/cert.pem" \
|
common revoke --cert-path "$root/conf/live/le2.wtf/cert.pem" \
|
||||||
--key-path "$root/conf/live/le2.wtf/privkey.pem" \
|
--key-path "$root/conf/live/le2.wtf/privkey.pem" \
|
||||||
--reason keyCompromise
|
--reason keyCompromise
|
||||||
|
|
||||||
|
common unregister
|
||||||
|
|
||||||
if type nginx;
|
if type nginx;
|
||||||
then
|
then
|
||||||
. ./certbot-nginx/tests/boulder-integration.sh
|
. ./certbot-nginx/tests/boulder-integration.sh
|
||||||
|
|||||||
Reference in New Issue
Block a user