ACME: add diff to acme_account, account_public_key to acme_account_facts, and general refactoring (#49410)

* Only one exit point.

* Refactoring account handling.

* Add diff support for acme_account.

* Insert public_account_key into acme_account_facts result and into acme_account diff.

* Add changelog.
This commit is contained in:
Felix Fontein 2018-12-02 18:40:14 +01:00 committed by René Moser
parent 62dd1fe29e
commit b0c7efcc6b
9 changed files with 305 additions and 104 deletions

View file

@ -0,0 +1,3 @@
minor_changes:
- "acme_account: add support for diff mode."
- "acme_account_facts: also return ``public_account_key`` in JWK format."

View file

@ -646,12 +646,14 @@ class ACMEAccount(object):
def _new_reg(self, contact=None, agreement=None, terms_agreed=False, allow_creation=True): def _new_reg(self, contact=None, agreement=None, terms_agreed=False, allow_creation=True):
''' '''
Registers a new ACME account. Returns True if the account was Registers a new ACME account. Returns a pair ``(created, data)``.
created and False if it already existed (e.g. it was not newly Here, ``created`` is ``True`` if the account was created and
created). ``False`` if it already existed (e.g. it was not newly created),
or does not exist. In case the account was created or exists,
``data`` contains the account data; otherwise, it is ``None``.
https://tools.ietf.org/html/draft-ietf-acme-acme-14#section-7.3 https://tools.ietf.org/html/draft-ietf-acme-acme-14#section-7.3
''' '''
contact = [] if contact is None else contact contact = contact or []
if self.version == 1: if self.version == 1:
new_reg = { new_reg = {
@ -668,6 +670,7 @@ class ACMEAccount(object):
'contact': contact 'contact': contact
} }
if not allow_creation: if not allow_creation:
# https://tools.ietf.org/html/draft-ietf-acme-acme-14#section-7.3.1
new_reg['onlyReturnExisting'] = True new_reg['onlyReturnExisting'] = True
if terms_agreed: if terms_agreed:
new_reg['termsOfServiceAgreed'] = True new_reg['termsOfServiceAgreed'] = True
@ -679,7 +682,7 @@ class ACMEAccount(object):
# Account did not exist # Account did not exist
if 'location' in info: if 'location' in info:
self.set_account_uri(info['location']) self.set_account_uri(info['location'])
return True return True, result
elif info['status'] == (409 if self.version == 1 else 200): elif info['status'] == (409 if self.version == 1 else 200):
# Account did exist # Account did exist
if result.get('status') == 'deactivated': if result.get('status') == 'deactivated':
@ -689,22 +692,22 @@ class ACMEAccount(object):
# "Once an account is deactivated, the server MUST NOT accept further # "Once an account is deactivated, the server MUST NOT accept further
# requests authorized by that account's key." # requests authorized by that account's key."
if not allow_creation: if not allow_creation:
return False return False, None
else: else:
raise ModuleFailException("Account is deactivated") raise ModuleFailException("Account is deactivated")
if 'location' in info: if 'location' in info:
self.set_account_uri(info['location']) self.set_account_uri(info['location'])
return False return False, result
elif info['status'] == 400 and result['type'] == 'urn:ietf:params:acme:error:accountDoesNotExist' and not allow_creation: elif info['status'] == 400 and result['type'] == 'urn:ietf:params:acme:error:accountDoesNotExist' and not allow_creation:
# Account does not exist (and we didn't try to create it) # Account does not exist (and we didn't try to create it)
return False return False, None
else: else:
raise ModuleFailException("Error registering: {0} {1}".format(info['status'], result)) raise ModuleFailException("Error registering: {0} {1}".format(info['status'], result))
def get_account_data(self): def get_account_data(self):
''' '''
Retrieve account information. Can only be called when the account Retrieve account information. Can only be called when the account
URI is already known (such as after calling init_account). URI is already known (such as after calling setup_account).
Return None if the account was deactivated, or a dict otherwise. Return None if the account was deactivated, or a dict otherwise.
''' '''
if self.uri is None: if self.uri is None:
@ -732,66 +735,82 @@ class ACMEAccount(object):
raise ModuleFailException("Error getting account data from {2}: {0} {1}".format(info['status'], result, self.uri)) raise ModuleFailException("Error getting account data from {2}: {0} {1}".format(info['status'], result, self.uri))
return result return result
def init_account(self, contact, agreement=None, terms_agreed=False, allow_creation=True, update_contact=True, remove_account_uri_if_not_exists=False): def setup_account(self, contact=None, agreement=None, terms_agreed=False, allow_creation=True, remove_account_uri_if_not_exists=False):
''' '''
Create or update an account on the ACME server. For ACME v1, Detect or create an account on the ACME server. For ACME v1,
as the only way (without knowing an account URI) to test if an as the only way (without knowing an account URI) to test if an
account exists is to try and create one with the provided account account exists is to try and create one with the provided account
key, this method will always result in an account being present key, this method will always result in an account being present
(except on error situations). For ACME v2, a new account will (except on error situations). For ACME v2, a new account will
only be created if allow_creation is set to True. only be created if ``allow_creation`` is set to True.
For ACME v2, check_mode is fully respected. For ACME v1, the account For ACME v2, ``check_mode`` is fully respected. For ACME v1, the
might be created if it does not yet exist. account might be created if it does not yet exist.
If the account already exists and if update_contact is set to Return a pair ``(created, account_data)``. Here, ``created`` will
True, this method will update the contact information. be ``True`` in case the account was created or would be created
(check mode). ``account_data`` will be the current account data,
or ``None`` if the account does not exist.
Return True in case something changed (account was created, contact The account URI will be stored in ``self.uri``; if it is ``None``,
info updated) or would be changed (check_mode). The account URI the account does not exist.
will be stored in self.uri; if it is None, the account does not
exist.
https://tools.ietf.org/html/draft-ietf-acme-acme-14#section-7.3 https://tools.ietf.org/html/draft-ietf-acme-acme-14#section-7.3
''' '''
new_account = True
changed = False
if self.uri is not None: if self.uri is not None:
new_account = False created = False
if not update_contact: # Verify that the account key belongs to the URI.
# Verify that the account key belongs to the URI. # (If update_contact is True, this will be done below.)
# (If update_contact is True, this will be done below.) account_data = self.get_account_data()
if self.get_account_data() is None: if account_data is None:
if remove_account_uri_if_not_exists and not allow_creation: if remove_account_uri_if_not_exists and not allow_creation:
self.uri = None self.uri = None
return False else:
raise ModuleFailException("Account is deactivated or does not exist!") raise ModuleFailException("Account is deactivated or does not exist!")
else: else:
new_account = self._new_reg( created, account_data = self._new_reg(
contact, contact,
agreement=agreement, agreement=agreement,
terms_agreed=terms_agreed, terms_agreed=terms_agreed,
allow_creation=allow_creation and not self.module.check_mode allow_creation=allow_creation and not self.module.check_mode
) )
if self.module.check_mode and self.uri is None and allow_creation: if self.module.check_mode and self.uri is None and allow_creation:
return True created = True
if not new_account and self.uri and update_contact: account_data = {
result = self.get_account_data() 'contact': contact or []
if result is None: }
if not allow_creation: return created, account_data
self.uri = None
return False
raise ModuleFailException("Account is deactivated or does not exist!")
# ...and check if update is necessary def update_account(self, account_data, contact=None):
if result.get('contact', []) != contact: '''
if not self.module.check_mode: Update an account on the ACME server. Check mode is fully respected.
upd_reg = result
upd_reg['contact'] = contact The current account data must be provided as ``account_data``.
result, dummy = self.send_signed_request(self.uri, upd_reg)
changed = True Return a pair ``(updated, account_data)``, where ``updated`` is
return new_account or changed ``True`` in case something changed (contact info updated) or
would be changed (check mode), and ``account_data`` the updated
account data.
https://tools.ietf.org/html/draft-ietf-acme-acme-14#section-7.3.2
'''
# Create request
update_request = {}
if contact is not None and account_data.get('contact', []) != contact:
update_request['contact'] = list(contact)
# No change?
if not update_request:
return False, dict(account_data)
# Apply change
if self.module.check_mode:
account_data = dict(account_data)
account_data.update(update_request)
else:
account_data, dummy = self.send_signed_request(self.uri, update_request)
return True, account_data
def cryptography_get_csr_domains(module, csr_filename): def cryptography_get_csr_domains(module, csr_filename):

View file

@ -166,43 +166,51 @@ def main():
try: try:
account = ACMEAccount(module) account = ACMEAccount(module)
changed = False
state = module.params.get('state') state = module.params.get('state')
diff_before = {}
diff_after = {}
if state == 'absent': if state == 'absent':
changed = account.init_account( created, account_data = account.setup_account(allow_creation=False)
[], if account_data:
allow_creation=False, diff_before = dict(account_data)
update_contact=False, diff_before['public_account_key'] = account.key_data['jwk']
) if created:
if changed: raise AssertionError('Unwanted account creation')
raise AssertionError('Unwanted account change') if account_data is not None:
if account.uri is not None: # Account is not yet deactivated
# Account does exist if not module.check_mode:
account_data = account.get_account_data() # Deactivate it
if account_data is not None: payload = {
# Account is not yet deactivated 'status': 'deactivated'
if not module.check_mode: }
# Deactivate it result, info = account.send_signed_request(account.uri, payload)
payload = { if info['status'] != 200:
'status': 'deactivated' raise ModuleFailException('Error deactivating account: {0} {1}'.format(info['status'], result))
} changed = True
result, info = account.send_signed_request(account.uri, payload)
if info['status'] != 200:
raise ModuleFailException('Error deactivating account: {0} {1}'.format(info['status'], result))
module.exit_json(changed=True, account_uri=account.uri)
module.exit_json(changed=False, account_uri=account.uri)
elif state == 'present': elif state == 'present':
allow_creation = module.params.get('allow_creation') allow_creation = module.params.get('allow_creation')
# Make sure contact is a list of strings (unfortunately, Ansible doesn't do that for us) # Make sure contact is a list of strings (unfortunately, Ansible doesn't do that for us)
contact = [str(v) for v in module.params.get('contact')] contact = [str(v) for v in module.params.get('contact')]
terms_agreed = module.params.get('terms_agreed') terms_agreed = module.params.get('terms_agreed')
changed = account.init_account( created, account_data = account.setup_account(
contact, contact,
terms_agreed=terms_agreed, terms_agreed=terms_agreed,
allow_creation=allow_creation, allow_creation=allow_creation,
) )
if account.uri is None: if account_data is None:
raise ModuleFailException(msg='Account does not exist or is deactivated.') raise ModuleFailException(msg='Account does not exist or is deactivated.')
module.exit_json(changed=changed, account_uri=account.uri) if created:
diff_before = {}
else:
diff_before = dict(account_data)
diff_before['public_account_key'] = account.key_data['jwk']
updated = False
if not created:
updated, account_data = account.update_account(account_data, contact)
changed = created or updated
diff_after = dict(account_data)
diff_after['public_account_key'] = account.key_data['jwk']
elif state == 'changed_key': elif state == 'changed_key':
# Parse new account key # Parse new account key
error, new_key_data = account.parse_key( error, new_key_data = account.parse_key(
@ -212,15 +220,13 @@ def main():
if error: if error:
raise ModuleFailException("error while parsing account key: %s" % error) raise ModuleFailException("error while parsing account key: %s" % error)
# Verify that the account exists and has not been deactivated # Verify that the account exists and has not been deactivated
changed = account.init_account( created, account_data = account.setup_account(allow_creation=False)
[], if created:
allow_creation=False, raise AssertionError('Unwanted account creation')
update_contact=False, if account_data is None:
)
if changed:
raise AssertionError('Unwanted account change')
if account.uri is None or account.get_account_data() is None:
raise ModuleFailException(msg='Account does not exist or is deactivated.') raise ModuleFailException(msg='Account does not exist or is deactivated.')
diff_before = dict(account_data)
diff_before['public_account_key'] = account.key_data['jwk']
# Now we can start the account key rollover # Now we can start the account key rollover
if not module.check_mode: if not module.check_mode:
# Compose inner signed message # Compose inner signed message
@ -241,7 +247,25 @@ def main():
result, info = account.send_signed_request(url, data) result, info = account.send_signed_request(url, data)
if info['status'] != 200: if info['status'] != 200:
raise ModuleFailException('Error account key rollover: {0} {1}'.format(info['status'], result)) raise ModuleFailException('Error account key rollover: {0} {1}'.format(info['status'], result))
module.exit_json(changed=True, account_uri=account.uri) if module._diff:
account.key_data = new_key_data
account.jws_header['alg'] = new_key_data['alg']
diff_after = account.get_account_data()
elif module._diff:
# Kind of fake diff_after
diff_after = dict(diff_before)
diff_after['public_account_key'] = new_key_data['jwk']
changed = True
result = {
'changed': changed,
'account_uri': account.uri,
}
if module._diff:
result['diff'] = {
'before': diff_before,
'after': diff_after,
}
module.exit_json(**result)
except ModuleFailException as e: except ModuleFailException as e:
e.do_fail(module) e.do_fail(module)

View file

@ -89,6 +89,11 @@ account:
returned: always returned: always
type: str type: str
sample: https://example.ca/account/1/orders sample: https://example.ca/account/1/orders
public_account_key:
description: the public account key as a L(JSON Web Key,https://tools.ietf.org/html/rfc7517).
returned: always
type: str
sample: https://example.ca/account/1/orders
''' '''
from ansible.module_utils.acme import ( from ansible.module_utils.acme import (
@ -129,24 +134,25 @@ def main():
try: try:
account = ACMEAccount(module) account = ACMEAccount(module)
# Check whether account exists # Check whether account exists
changed = account.init_account( created, account_data = account.setup_account(
[], [],
allow_creation=False, allow_creation=False,
update_contact=False,
remove_account_uri_if_not_exists=True, remove_account_uri_if_not_exists=True,
) )
if changed: if created:
raise AssertionError('Unwanted account change') raise AssertionError('Unwanted account creation')
if account.uri is None: result = {
# Account does exist 'changed': False,
module.exit_json(changed=False, exists=False, account_uri=None) 'exists': account.uri is not None,
else: 'account_uri': account.uri,
# Account exists: retrieve account information }
data = account.get_account_data() if account.uri is not None:
# Make sure promised data is there # Make sure promised data is there
if 'contact' not in data: if 'contact' not in account_data:
data['contact'] = [] account_data['contact'] = []
module.exit_json(changed=False, exists=True, account_uri=account.uri, account=data) account_data['public_account_key'] = account.key_data['jwk']
result['account'] = account_data
module.exit_json(**result)
except ModuleFailException as e: except ModuleFailException as e:
e.do_fail(module) e.do_fail(module)

View file

@ -406,16 +406,21 @@ class ACMEClient(object):
contact = [] contact = []
if module.params['account_email']: if module.params['account_email']:
contact.append('mailto:' + module.params['account_email']) contact.append('mailto:' + module.params['account_email'])
self.changed = self.account.init_account( created, account_data = self.account.setup_account(
contact, contact,
agreement=module.params.get('agreement'), agreement=module.params.get('agreement'),
terms_agreed=module.params.get('terms_agreed'), terms_agreed=module.params.get('terms_agreed'),
allow_creation=modify_account, allow_creation=modify_account,
update_contact=modify_account
) )
if account_data is None:
raise ModuleFailException(msg='Account does not exist or is deactivated.')
updated = False
if not created and account_data and modify_account:
updated, account_data = self.account.update_account(account_data, contact)
self.changed = created or updated
else: else:
# This happens if modify_account is False and the ACME v1 # This happens if modify_account is False and the ACME v1
# protocol is used. In this case, we do not call init_account() # protocol is used. In this case, we do not call setup_account()
# to avoid accidental creation of an account. This is OK # to avoid accidental creation of an account. This is OK
# since for ACME v1, the account URI is not needed to send a # since for ACME v1, the account URI is not needed to send a
# signed ACME request. # signed ACME request.

View file

@ -177,13 +177,11 @@ def main():
result, info = account.send_signed_request(endpoint, payload, key_data=private_key_data, jws_header=jws_header) result, info = account.send_signed_request(endpoint, payload, key_data=private_key_data, jws_header=jws_header)
else: else:
# Step 1: get hold of account URI # Step 1: get hold of account URI
changed = account.init_account( created, account_data = account.setup_account(allow_creation=False)
[], if created:
allow_creation=False, raise AssertionError('Unwanted account creation')
update_contact=False, if account_data is None:
) raise ModuleFailException(msg='Account does not exist or is deactivated.')
if changed:
raise AssertionError('Unwanted account change')
# Step 2: sign revokation request with account key # Step 2: sign revokation request with account key
result, info = account.send_signed_request(endpoint, payload) result, info = account.send_signed_request(endpoint, payload)
if info['status'] != 200: if info['status'] != 200:

View file

@ -16,6 +16,22 @@
ignore_errors: yes ignore_errors: yes
register: account_not_created register: account_not_created
- name: Create it now (check mode, diff)
acme_account:
select_crypto_backend: "{{ select_crypto_backend }}"
account_key_src: "{{ output_dir }}/accountkey.pem"
acme_version: 2
acme_directory: https://{{ acme_host }}:14000/dir
validate_certs: no
state: present
allow_creation: yes
terms_agreed: yes
contact:
- mailto:example@example.org
check_mode: yes
diff: yes
register: account_created_check
- name: Create it now - name: Create it now
acme_account: acme_account:
select_crypto_backend: "{{ select_crypto_backend }}" select_crypto_backend: "{{ select_crypto_backend }}"
@ -30,6 +46,35 @@
- mailto:example@example.org - mailto:example@example.org
register: account_created register: account_created
- name: Create it now (idempotent)
acme_account:
select_crypto_backend: "{{ select_crypto_backend }}"
account_key_src: "{{ output_dir }}/accountkey.pem"
acme_version: 2
acme_directory: https://{{ acme_host }}:14000/dir
validate_certs: no
state: present
allow_creation: yes
terms_agreed: yes
contact:
- mailto:example@example.org
register: account_created_idempotent
- name: Change email address (check mode, diff)
acme_account:
select_crypto_backend: "{{ select_crypto_backend }}"
account_key_content: "{{ lookup('file', output_dir ~ '/accountkey.pem') }}"
acme_version: 2
acme_directory: https://{{ acme_host }}:14000/dir
validate_certs: no
state: present
# allow_creation: no
contact:
- mailto:example@example.com
check_mode: yes
diff: yes
register: account_modified_check
- name: Change email address - name: Change email address
acme_account: acme_account:
select_crypto_backend: "{{ select_crypto_backend }}" select_crypto_backend: "{{ select_crypto_backend }}"
@ -70,6 +115,20 @@
ignore_errors: yes ignore_errors: yes
register: account_modified_wrong_uri register: account_modified_wrong_uri
- name: Clear contact email addresses (check mode, diff)
acme_account:
select_crypto_backend: "{{ select_crypto_backend }}"
account_key_src: "{{ output_dir }}/accountkey.pem"
acme_version: 2
acme_directory: https://{{ acme_host }}:14000/dir
validate_certs: no
state: present
# allow_creation: no
contact: []
check_mode: yes
diff: yes
register: account_modified_2_check
- name: Clear contact email addresses - name: Clear contact email addresses
acme_account: acme_account:
select_crypto_backend: "{{ select_crypto_backend }}" select_crypto_backend: "{{ select_crypto_backend }}"
@ -100,6 +159,21 @@
- name: Parse account key (to ease debugging some test failures) - name: Parse account key (to ease debugging some test failures)
command: openssl ec -in {{ output_dir }}/accountkey2.pem -noout -text command: openssl ec -in {{ output_dir }}/accountkey2.pem -noout -text
- name: Change account key (check mode, diff)
acme_account:
select_crypto_backend: "{{ select_crypto_backend }}"
account_key_src: "{{ output_dir }}/accountkey.pem"
acme_version: 2
acme_directory: https://{{ acme_host }}:14000/dir
validate_certs: no
new_account_key_src: "{{ output_dir }}/accountkey2.pem"
state: changed_key
contact:
- mailto:example@example.com
check_mode: yes
diff: yes
register: account_change_key_check
- name: Change account key - name: Change account key
acme_account: acme_account:
select_crypto_backend: "{{ select_crypto_backend }}" select_crypto_backend: "{{ select_crypto_backend }}"
@ -113,6 +187,18 @@
- mailto:example@example.com - mailto:example@example.com
register: account_change_key register: account_change_key
- name: Deactivate account (check mode, diff)
acme_account:
select_crypto_backend: "{{ select_crypto_backend }}"
account_key_src: "{{ output_dir }}/accountkey2.pem"
acme_version: 2
acme_directory: https://{{ acme_host }}:14000/dir
validate_certs: no
state: absent
check_mode: yes
diff: yes
register: account_deactivate_check
- name: Deactivate account - name: Deactivate account
acme_account: acme_account:
select_crypto_backend: "{{ select_crypto_backend }}" select_crypto_backend: "{{ select_crypto_backend }}"

View file

@ -3,6 +3,18 @@
assert: assert:
that: that:
- account_not_created is failed - account_not_created is failed
- account_not_created.msg == 'Account does not exist or is deactivated.'
- name: Validate that account was created in the second step (check mode)
assert:
that:
- account_created_check is changed
- account_created_check.account_uri is none
- "'diff' in account_created_check"
- "account_created_check.diff.before == {}"
- "'after' in account_created_check.diff"
- account_created_check.diff.after.contact | length == 1
- account_created_check.diff.after.contact[0] == 'mailto:example@example.org'
- name: Validate that account was created in the second step - name: Validate that account was created in the second step
assert: assert:
@ -10,6 +22,23 @@
- account_created is changed - account_created is changed
- account_created.account_uri is not none - account_created.account_uri is not none
- name: Validate that account was created in the second step (idempotency)
assert:
that:
- account_created_idempotent is not changed
- account_created_idempotent.account_uri is not none
- name: Validate that email address was changed (check mode)
assert:
that:
- account_modified_check is changed
- account_modified_check.account_uri is not none
- "'diff' in account_modified_check"
- account_modified_check.diff.before.contact | length == 1
- account_modified_check.diff.before.contact[0] == 'mailto:example@example.org'
- account_modified_check.diff.after.contact | length == 1
- account_modified_check.diff.after.contact[0] == 'mailto:example@example.com'
- name: Validate that email address was changed - name: Validate that email address was changed
assert: assert:
that: that:
@ -27,6 +56,16 @@
that: that:
- account_modified_wrong_uri is failed - account_modified_wrong_uri is failed
- name: Validate that email address was cleared (check mode)
assert:
that:
- account_modified_2_check is changed
- account_modified_2_check.account_uri is not none
- "'diff' in account_modified_2_check"
- account_modified_2_check.diff.before.contact | length == 1
- account_modified_2_check.diff.before.contact[0] == 'mailto:example@example.com'
- account_modified_2_check.diff.after.contact | length == 0
- name: Validate that email address was cleared - name: Validate that email address was cleared
assert: assert:
that: that:
@ -39,12 +78,29 @@
- account_modified_2_idempotent is not changed - account_modified_2_idempotent is not changed
- account_modified_2_idempotent.account_uri is not none - account_modified_2_idempotent.account_uri is not none
- name: Validate that the account key was changed (check mode)
assert:
that:
- account_change_key_check is changed
- account_change_key_check.account_uri is not none
- "'diff' in account_change_key_check"
- account_change_key_check.diff.before.public_account_key != account_change_key_check.diff.after.public_account_key
- name: Validate that the account key was changed - name: Validate that the account key was changed
assert: assert:
that: that:
- account_change_key is changed - account_change_key is changed
- account_change_key.account_uri is not none - account_change_key.account_uri is not none
- name: Validate that the account was deactivated (check mode)
assert:
that:
- account_deactivate_check is changed
- account_deactivate_check.account_uri is not none
- "'diff' in account_deactivate_check"
- "account_deactivate_check.diff.before != {}"
- "account_deactivate_check.diff.after == {}"
- name: Validate that the account was deactivated - name: Validate that the account was deactivated
assert: assert:
that: that:
@ -61,8 +117,10 @@
assert: assert:
that: that:
- account_not_created_2 is failed - account_not_created_2 is failed
- account_not_created_2.msg == 'Account does not exist or is deactivated.'
- name: Validate that the account is gone (old account key) - name: Validate that the account is gone (old account key)
assert: assert:
that: that:
- account_not_created_3 is failed - account_not_created_3 is failed
- account_not_created_3.msg == 'Account does not exist or is deactivated.'

View file

@ -13,6 +13,7 @@
- account_created.account_uri is not none - account_created.account_uri is not none
- "'account' in account_created" - "'account' in account_created"
- "'contact' in account_created.account" - "'contact' in account_created.account"
- "'public_account_key' in account_created.account"
- account_created.account.contact | length == 1 - account_created.account.contact | length == 1
- "account_created.account.contact[0] == 'mailto:example@example.org'" - "account_created.account.contact[0] == 'mailto:example@example.org'"
@ -23,6 +24,7 @@
- account_modified.account_uri is not none - account_modified.account_uri is not none
- "'account' in account_modified" - "'account' in account_modified"
- "'contact' in account_modified.account" - "'contact' in account_modified.account"
- "'public_account_key' in account_modified.account"
- account_modified.account.contact | length == 0 - account_modified.account.contact | length == 0
- name: Validate that account does not exist with wrong account URI - name: Validate that account does not exist with wrong account URI