Impelment account deactivation [revision requested] (#3571)

Impelment account deactivation
This commit is contained in:
Blake Griffith 2017-01-17 16:00:07 -08:00 committed by Brad Warren
parent 0fa307806e
commit 49d46ef99a
11 changed files with 163 additions and 10 deletions

2
.gitignore vendored
View file

@ -33,3 +33,5 @@ tags
tests/letstest/letest-*/
tests/letstest/*.pem
tests/letstest/venv/
.venv

View file

@ -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.

View file

@ -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))

View file

@ -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.

View file

@ -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(),

View file

@ -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",

View file

@ -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`.

View file

@ -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):

View file

@ -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

View file

@ -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')

View file

@ -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