diff --git a/lib/ansible/modules/crypto/acme/acme_account_info.py b/lib/ansible/modules/crypto/acme/acme_account_info.py index 991352207a4..760d5d0e4b2 100644 --- a/lib/ansible/modules/crypto/acme/acme_account_info.py +++ b/lib/ansible/modules/crypto/acme/acme_account_info.py @@ -28,6 +28,21 @@ notes: - "The M(acme_account) module allows to modify, create and delete ACME accounts." - "This module was called C(acme_account_facts) before Ansible 2.8. The usage did not change." +options: + retrieve_orders: + description: + - "Whether to retrieve the list of order URLs or order objects, if provided + by the ACME server." + - "A value of C(ignore) will not fetch the list of orders." + - "Currently, Let's Encrypt does not return orders, so the C(orders) result + will always be empty." + type: str + choices: + - ignore + - url_list + - object_list + default: ignore + version_added: "2.9" seealso: - module: acme_account description: Allows to create, modify or delete an ACME account. @@ -90,7 +105,10 @@ account: choices: ['valid', 'deactivated', 'revoked'] sample: valid orders: - description: a URL where a list of orders can be retrieved for this account + description: + - A URL where a list of orders can be retrieved for this account. + - Use the I(retrieve_orders) option to query this URL and retrieve the + complete list of orders. returned: always type: str sample: https://example.ca/account/1/orders @@ -99,15 +117,126 @@ account: returned: always type: str sample: https://example.ca/account/1/orders + +orders: + description: + - "The list of orders." + - "If I(retrieve_orders) is C(url_list), this will be a list of URLs." + - "If I(retrieve_orders) is C(object_list), this will be a list of objects." + type: list + returned: if account exists, I(retrieve_orders) is not C(ignore), and server supports order listing + contains: + status: + description: The order's status. + type: str + choices: + - pending + - ready + - processing + - valid + - invalid + expires: + description: + - When the order expires. + - Timestamp should be formatted as described in RFC3339. + - Only required to be included in result when I(status) is C(pending) or C(valid). + type: str + returned: when server gives expiry date + identifiers: + description: + - List of identifiers this order is for. + type: list + contains: + type: + description: Type of identifier. C(dns) or C(ip). + type: str + value: + description: Name of identifier. Hostname or IP address. + type: str + wildcard: + description: "Whether I(value) is actually a wildcard. The wildcard + prefix C(*.) is not included in I(value) if this is C(true)." + type: bool + returned: required to be included if the identifier is wildcarded + notBefore: + description: + - The requested value of the C(notBefore) field in the certificate. + - Date should be formatted as described in RFC3339. + - Server is not required to return this. + type: str + returned: when server returns this + notAfter: + description: + - The requested value of the C(notAfter) field in the certificate. + - Date should be formatted as described in RFC3339. + - Server is not required to return this. + type: str + returned: when server returns this + error: + description: + - In case an error occured during processing, this contains information about the error. + - The field is structured as a problem document (RFC7807). + type: complex + returned: when an error occurred + authorizations: + description: + - A list of URLs for authorizations for this order. + type: list + finalize: + description: + - A URL used for finalizing an ACME order. + type: str + certificate: + description: + - The URL for retrieving the certificate. + type: str + returned: when certificate was issued ''' from ansible.module_utils.acme import ( - ModuleFailException, ACMEAccount, set_crypto_backend, + ModuleFailException, ACMEAccount, set_crypto_backend, process_links, ) from ansible.module_utils.basic import AnsibleModule +def get_orders_list(module, account, orders_url): + ''' + Retrieves orders list (handles pagination). + ''' + orders = [] + while orders_url: + # Get part of orders list + res, info = account.get_request(orders_url, parse_json_result=True, fail_on_error=True) + if not res.get('orders'): + if orders: + module.warn('When retrieving orders list part {0}, got empty result list'.format(orders_url)) + break + # Add order URLs to result list + orders.extend(res['orders']) + # Extract URL of next part of results list + new_orders_url = [] + + def f(link, relation): + if relation == 'next': + new_orders_url.append(link) + + process_links(info, f) + new_orders_url.append(None) + previous_orders_url, orders_url = orders_url, new_orders_url.pop(0) + if orders_url == previous_orders_url: + # Prevent infinite loop + orders_url = None + return orders + + +def get_order(account, order_url): + ''' + Retrieve order data. + ''' + return account.get_request(order_url, parse_json_result=True, fail_on_error=True)[0] + + def main(): module = AnsibleModule( argument_spec=dict( @@ -118,6 +247,7 @@ def main(): acme_version=dict(type='int', default=1, choices=[1, 2]), validate_certs=dict(type='bool', default=True), select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'openssl', 'cryptography']), + retrieve_orders=dict(type='str', default='ignore', choices=['ignore', 'url_list', 'object_list']), ), required_one_of=( ['account_key_src', 'account_key_content'], @@ -159,6 +289,13 @@ def main(): account_data['contact'] = [] account_data['public_account_key'] = account.key_data['jwk'] result['account'] = account_data + # Retrieve orders list + if account_data.get('orders') and module.params['retrieve_orders'] != 'ignore': + orders = get_orders_list(module, account, account_data['orders']) + if module.params['retrieve_orders'] == 'url_list': + result['orders'] = orders + else: + result['orders'] = [get_order(account, order) for order in orders] module.exit_json(**result) except ModuleFailException as e: e.do_fail(module) diff --git a/test/integration/targets/acme_certificate/aliases b/test/integration/targets/acme_certificate/aliases index d7936330302..81c6d993991 100644 --- a/test/integration/targets/acme_certificate/aliases +++ b/test/integration/targets/acme_certificate/aliases @@ -1,2 +1,3 @@ shippable/cloud/group1 cloud/acme +acme_account_info diff --git a/test/integration/targets/acme_certificate/tasks/impl.yml b/test/integration/targets/acme_certificate/tasks/impl.yml index 979b3aee606..6e2ec61b5ac 100644 --- a/test/integration/targets/acme_certificate/tasks/impl.yml +++ b/test/integration/targets/acme_certificate/tasks/impl.yml @@ -299,3 +299,49 @@ - name: Dumping cert 8 command: openssl x509 -in "{{ output_dir }}/cert-8.pem" -noout -text register: cert_8_text +## GET ACCOUNT ORDERS ######################################################################### +- name: Don't retrieve orders + acme_account_info: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_src: "{{ output_dir }}/account-ec256.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: no + retrieve_orders: ignore + register: account_orders_not +- name: Retrieve orders as URL list (1/2) + acme_account_info: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_src: "{{ output_dir }}/account-ec256.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: no + retrieve_orders: url_list + register: account_orders_urls +- name: Retrieve orders as URL list (2/2) + acme_account_info: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_src: "{{ output_dir }}/account-ec384.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: no + retrieve_orders: url_list + register: account_orders_urls2 +- name: Retrieve orders as object list (1/2) + acme_account_info: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_src: "{{ output_dir }}/account-ec256.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: no + retrieve_orders: object_list + register: account_orders_full +- name: Retrieve orders as object list (2/2) + acme_account_info: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_src: "{{ output_dir }}/account-ec384.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: no + retrieve_orders: object_list + register: account_orders_full2 diff --git a/test/integration/targets/acme_certificate/tests/validate.yml b/test/integration/targets/acme_certificate/tests/validate.yml index 5264dcf4099..d2a3bcc39b8 100644 --- a/test/integration/targets/acme_certificate/tests/validate.yml +++ b/test/integration/targets/acme_certificate/tests/validate.yml @@ -83,3 +83,37 @@ assert: that: - "'DNS:example.org' in cert_6_text.stdout" + +- name: Validate that orders were not retrieved + assert: + that: + - "'account' in account_orders_not" + - "'orders' not in account_orders_not" + +- name: Validate that orders were retrieved as list of URLs (1/2) + assert: + that: + - "'account' in account_orders_urls" + - "'orders' in account_orders_urls" + - "account_orders_urls.orders[0] is string" + +- name: Validate that orders were retrieved as list of URLs (2/2) + assert: + that: + - "'account' in account_orders_urls2" + - "'orders' in account_orders_urls2" + - "account_orders_urls2.orders[0] is string" + +- name: Validate that orders were retrieved as list of objects (1/2) + assert: + that: + - "'account' in account_orders_full" + - "'orders' in account_orders_full" + - "account_orders_full.orders[0].status is string" + +- name: Validate that orders were retrieved as list of objects (2/2) + assert: + that: + - "'account' in account_orders_full2" + - "'orders' in account_orders_full2" + - "account_orders_full2.orders[0].status is string"