New acme_account_facts module. (#44301)

This commit is contained in:
Felix Fontein 2018-08-17 15:32:09 +02:00 committed by René Moser
parent bc4f7abe96
commit a99cfc1814
7 changed files with 321 additions and 4 deletions

View file

@ -681,13 +681,17 @@ class ACMEAccount(object):
if self.version == 1: if self.version == 1:
data['resource'] = 'reg' data['resource'] = 'reg'
result, info = self.send_signed_request(self.uri, data) result, info = self.send_signed_request(self.uri, data)
if info['status'] == 403 and result.get('type') == 'urn:ietf:params:acme:error:unauthorized': if info['status'] in (400, 403) and result.get('type') == 'urn:ietf:params:acme:error:unauthorized':
# Returned when account is deactivated
return None
if info['status'] in (400, 404) and result.get('type') == 'urn:ietf:params:acme:error:accountDoesNotExist':
# Returned when account does not exist
return None return None
if info['status'] < 200 or info['status'] >= 300: if info['status'] < 200 or info['status'] >= 300:
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): def init_account(self, contact, agreement=None, terms_agreed=False, allow_creation=True, update_contact=True, remove_account_uri_if_not_exists=False):
''' '''
Create or update an account on the ACME server. For ACME v1, Create or update 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
@ -717,7 +721,11 @@ class ACMEAccount(object):
if not update_contact: 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.)
self.get_account_data() if self.get_account_data() is None:
if remove_account_uri_if_not_exists and not allow_creation:
self.uri = None
return False
raise ModuleFailException("Account is deactivated or does not exist!")
else: else:
new_account = self._new_reg( new_account = self._new_reg(
contact, contact,
@ -733,7 +741,7 @@ class ACMEAccount(object):
if not allow_creation: if not allow_creation:
self.uri = None self.uri = None
return False return False
raise ModuleFailException("Account is deactivated!") raise ModuleFailException("Account is deactivated or does not exist!")
# ...and check if update is necessary # ...and check if update is necessary
if result.get('contact', []) != contact: if result.get('contact', []) != contact:

View file

@ -0,0 +1,154 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# (c) 2018 Felix Fontein <felix@fontein.de>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = '''
---
module: acme_account_facts
author: "Felix Fontein (@felixfontein)"
version_added: "2.7"
short_description: Retrieves information on ACME accounts.
description:
- "Allows to retrieve information on accounts with Let's Encrypt.
Let's Encrypt is a free, automated, and open certificate authority
(CA), run for the public's benefit. For details see U(https://letsencrypt.org)."
- "The M(acme_account) module allows to modify, create and delete ACME accounts."
- "This module only works with the ACME v2 protocol."
extends_documentation_fragment:
- acme
'''
EXAMPLES = '''
- name: Check whether an account with the given account key exists
acme_account_facts:
account_key_src: /etc/pki/cert/private/account.key
register: account_data
- name: Verify that account exists
assert:
that:
- account_data.exists
- name: Print account URI
debug: var=account_data.account_uri
- name: Print account contacts
debug: var=account_data.account.contact
- name: Check whether the account exists and is accessible with the given account key
acme_account_facts:
account_key_content: "{{ acme_account_key }}"
account_uri: "{{ acme_account_uri }}"
register: account_data
- name: Verify that account exists
assert:
that:
- account_data.exists
- name: Print account contacts
debug: var=account_data.account.contact
'''
RETURN = '''
exists:
description: Whether the account exists.
returned: always
type: bool
account_uri:
description: ACME account URI, or None if account does not exist.
returned: always
type: string
account:
description: The account information, as retrieved from the ACME server.
returned: if account exists
type: complex
contains:
contact:
description: the challenge resource that must be created for validation
returned: always
type: list
sample: "['mailto:me@example.com', 'tel:00123456789']"
status:
description: the account's status
returned: always
type: str
choices: ['valid', 'deactivated', 'revoked']
sample: valid
orders:
description: a URL where a list of orders can be retrieved for this account
returned: always
type: str
sample: https://example.ca/account/1/orders
'''
from ansible.module_utils.acme import (
ModuleFailException, ACMEAccount, set_crypto_backend,
)
from ansible.module_utils.basic import AnsibleModule
def main():
module = AnsibleModule(
argument_spec=dict(
account_key_src=dict(type='path', aliases=['account_key']),
account_key_content=dict(type='str', no_log=True),
account_uri=dict(required=False, type='str'),
acme_directory=dict(required=False, default='https://acme-staging.api.letsencrypt.org/directory', type='str'),
acme_version=dict(required=False, default=1, choices=[1, 2], type='int'),
validate_certs=dict(required=False, default=True, type='bool'),
select_crypto_backend=dict(required=False, choices=['auto', 'openssl', 'cryptography'], default='auto', type='str'),
),
required_one_of=(
['account_key_src', 'account_key_content'],
),
mutually_exclusive=(
['account_key_src', 'account_key_content'],
),
supports_check_mode=True,
)
set_crypto_backend(module)
if not module.params.get('validate_certs'):
module.warn(warning='Disabling certificate validation for communications with ACME endpoint. ' +
'This should only be done for testing against a local ACME server for ' +
'development purposes, but *never* for production purposes.')
if module.params.get('acme_version') < 2:
module.fail_json(msg='The acme_account module requires the ACME v2 protocol!')
try:
account = ACMEAccount(module)
# Check whether account exists
changed = account.init_account(
[],
allow_creation=False,
update_contact=False,
remove_account_uri_if_not_exists=True,
)
if changed:
raise AssertionError('Unwanted account change')
if account.uri is None:
# Account does exist
module.exit_json(changed=False, exists=False, account_uri=None)
else:
# Account exists: retrieve account information
data = account.get_account_data()
# Make sure promised data is there
if 'contact' not in data:
data['contact'] = []
module.exit_json(changed=False, exists=True, account_uri=account.uri, account=data)
except ModuleFailException as e:
e.do_fail(module)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,2 @@
shippable/cloud/group1
cloud/acme

View file

@ -0,0 +1,2 @@
dependencies:
- setup_acme

View file

@ -0,0 +1,82 @@
---
- name: Generate account key
command: openssl ecparam -name prime256v1 -genkey -out {{ output_dir }}/accountkey.pem
- name: Generate second account key
command: openssl ecparam -name prime256v1 -genkey -out {{ output_dir }}/accountkey2.pem
- name: Parse account key (to ease debugging some test failures)
command: openssl ec -in {{ output_dir }}/accountkey.pem -noout -text
- name: Check that account does not exist
acme_account_facts:
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
register: account_not_created
- name: Create it now
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
- name: Check that account exists
acme_account_facts:
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
register: account_created
- name: Clear email address
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: []
- name: Check that account was modified
acme_account_facts:
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
account_uri: "{{ account_created.account_uri }}"
register: account_modified
- name: Check with wrong account URI
acme_account_facts:
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
account_uri: "{{ account_created.account_uri }}test1234doesnotexists"
register: account_not_exist
- name: Check with wrong account key
acme_account_facts:
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
account_uri: "{{ account_created.account_uri }}"
ignore_errors: yes
register: account_wrong_key

View file

@ -0,0 +1,31 @@
---
- block:
- name: Running tests with OpenSSL backend
include_tasks: impl.yml
vars:
select_crypto_backend: openssl
- import_tasks: ../tests/validate.yml
# Old 0.9.8 versions have insufficient CLI support for signing with EC keys
when: openssl_version.stdout is version('1.0.0', '>=')
- name: Remove output directory
file:
path: "{{ output_dir }}"
state: absent
- name: Re-create output directory
file:
path: "{{ output_dir }}"
state: directory
- block:
- name: Running tests with cryptography backend
include_tasks: impl.yml
vars:
select_crypto_backend: cryptography
- import_tasks: ../tests/validate.yml
when: cryptography_version.stdout is version('1.5', '>=')

View file

@ -0,0 +1,38 @@
---
- name: Validate that account wasn't there
assert:
that:
- not account_not_created.exists
- account_not_created.account_uri is none
- "'account' not in account_not_created"
- name: Validate that account was created
assert:
that:
- account_created.exists
- account_created.account_uri is not none
- "'account' in account_created"
- "'contact' in account_created.account"
- account_created.account.contact | length == 1
- "account_created.account.contact[0] == 'mailto:example@example.org'"
- name: Validate that account email was removed
assert:
that:
- account_modified.exists
- account_modified.account_uri is not none
- "'account' in account_modified"
- "'contact' in account_modified.account"
- account_modified.account.contact | length == 0
- name: Validate that account does not exist with wrong account URI
assert:
that:
- not account_not_exist.exists
- account_not_exist.account_uri is none
- "'account' not in account_not_exist"
- name: Validate that account cannot be accessed with wrong key
assert:
that:
- account_wrong_key is failed