mirror of
https://github.com/certbot/certbot.git
synced 2026-04-22 22:59:39 -04:00
Impelment account deactivation [revision requested] (#3571)
Impelment account deactivation
This commit is contained in:
parent
0fa307806e
commit
49d46ef99a
11 changed files with 163 additions and 10 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -33,3 +33,5 @@ tags
|
|||
tests/letstest/letest-*/
|
||||
tests/letstest/*.pem
|
||||
tests/letstest/venv/
|
||||
|
||||
.venv
|
||||
|
|
|
|||
|
|
@ -132,12 +132,24 @@ class Client(object): # pylint: disable=too-many-instance-attributes
|
|||
|
||||
"""
|
||||
update = regr.body if update is None else update
|
||||
updated_regr = self._send_recv_regr(
|
||||
regr, body=messages.UpdateRegistration(**dict(update)))
|
||||
body = messages.UpdateRegistration(**dict(update))
|
||||
updated_regr = self._send_recv_regr(regr, body=body)
|
||||
if updated_regr != regr:
|
||||
raise errors.UnexpectedUpdate(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):
|
||||
"""Query server about registration.
|
||||
|
||||
|
|
|
|||
|
|
@ -124,6 +124,20 @@ class ClientTest(unittest.TestCase):
|
|||
self.assertRaises(
|
||||
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):
|
||||
self.response.json.return_value = self.regr.body.to_json()
|
||||
self.assertEqual(self.regr, self.client.query_registration(self.regr))
|
||||
|
|
|
|||
|
|
@ -250,6 +250,7 @@ class Registration(ResourceBody):
|
|||
agreement = jose.Field('agreement', omitempty=True)
|
||||
authorizations = jose.Field('authorizations', omitempty=True)
|
||||
certificates = jose.Field('certificates', omitempty=True)
|
||||
status = jose.Field('status', omitempty=True)
|
||||
|
||||
class Authorizations(jose.JSONObjectWithFields):
|
||||
"""Authorizations granted to Account in the process of registration.
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import datetime
|
|||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import socket
|
||||
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
|
|
@ -197,6 +198,18 @@ class AccountFileStorage(interfaces.AccountStorage):
|
|||
"""
|
||||
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):
|
||||
account_dir_path = self._account_dir_path(account.id)
|
||||
util.make_or_verify_dir(account_dir_path, 0o700, os.geteuid(),
|
||||
|
|
|
|||
|
|
@ -366,6 +366,10 @@ VERB_HELP = [
|
|||
"short": "Register for account with Let's Encrypt / other ACME server",
|
||||
"opts": "Options for account registration & modification"
|
||||
}),
|
||||
("unregister", {
|
||||
"short": "Irrevocably deactivate your account",
|
||||
"opts": "Options for account deactivation."
|
||||
}),
|
||||
("install", {
|
||||
"short": "Install an arbitrary cert in a server",
|
||||
"opts": "Options for modifying how a cert is deployed"
|
||||
|
|
@ -414,6 +418,7 @@ class HelpfulArgumentParser(object):
|
|||
"install": main.install,
|
||||
"plugins": main.plugins_cmd,
|
||||
"register": main.register,
|
||||
"unregister": main.unregister,
|
||||
"renew": main.renew,
|
||||
"revoke": main.revoke,
|
||||
"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 "
|
||||
"with an existing registration, such as the e-mail address, "
|
||||
"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(
|
||||
["automation", "certonly", "run"],
|
||||
"--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",
|
||||
help="Agree to the ACME Subscriber Agreement (default: Ask)")
|
||||
helpful.add(
|
||||
"automation", "--account", metavar="ACCOUNT_ID",
|
||||
["unregister", "automation"], "--account", metavar="ACCOUNT_ID",
|
||||
help="Account ID to use")
|
||||
helpful.add(
|
||||
"automation", "--duplicate", dest="duplicate", action="store_true",
|
||||
|
|
|
|||
|
|
@ -172,7 +172,7 @@ def perform_registration(acme, config):
|
|||
|
||||
|
||||
class Client(object):
|
||||
"""ACME protocol client.
|
||||
"""Certbot's client.
|
||||
|
||||
:ivar .IConfig config: Client configuration.
|
||||
:ivar .Account account: Account registered with `register`.
|
||||
|
|
|
|||
|
|
@ -406,6 +406,35 @@ def _init_le_client(config, authenticator, installer):
|
|||
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):
|
||||
"""Create or modify accounts on the server."""
|
||||
|
||||
|
|
@ -413,6 +442,8 @@ def register(config, unused_plugins):
|
|||
# exist or not.
|
||||
account_storage = account.AccountFileStorage(config)
|
||||
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
|
||||
if not config.update_registration:
|
||||
|
|
@ -443,9 +474,7 @@ def register(config, unused_plugins):
|
|||
acc.regr = acme_client.acme.update_registration(acc.regr.update(
|
||||
body=acc.regr.body.update(contact=('mailto:' + config.email,))))
|
||||
account_storage.save_regr(acc)
|
||||
reporter_util = zope.component.getUtility(interfaces.IReporter)
|
||||
msg = "Your e-mail address was updated to {0}.".format(config.email)
|
||||
reporter_util.add_message(msg, reporter_util.MEDIUM_PRIORITY)
|
||||
add_msg("Your e-mail address was updated to {0}.".format(config.email))
|
||||
|
||||
|
||||
def install(config, plugins):
|
||||
|
|
|
|||
|
|
@ -190,6 +190,14 @@ class AccountFileStorageTest(unittest.TestCase):
|
|||
self.assertRaises(
|
||||
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__":
|
||||
unittest.main() # pragma: no cover
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
"""Tests for certbot.main."""
|
||||
# pylint: disable=too-many-lines
|
||||
from __future__ import print_function
|
||||
|
||||
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])
|
||||
|
||||
|
||||
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):
|
||||
"""Test main._handle_exception"""
|
||||
@mock.patch('certbot.main.sys')
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
# Note: this script is called by Boulder integration test suite!
|
||||
|
||||
. ./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 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"
|
||||
# revoke by cert key
|
||||
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
|
||||
common --domains le1.wtf
|
||||
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" \
|
||||
--key-path "$root/conf/live/le2.wtf/privkey.pem" \
|
||||
--reason keyCompromise
|
||||
|
||||
common unregister
|
||||
|
||||
if type nginx;
|
||||
then
|
||||
. ./certbot-nginx/tests/boulder-integration.sh
|
||||
|
|
|
|||
Loading…
Reference in a new issue