#!/usr/bin/python
# -*- coding: utf-8 -*-

# Copyright (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
# Copyright (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
# Copyright (2) 2020, Felix Fontein <felix@fontein.de>
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later

from __future__ import absolute_import, division, print_function


__metaclass__ = type


DOCUMENTATION = r"""
module: x509_certificate_pipe
short_description: Generate and/or check OpenSSL certificates
version_added: 1.3.0
description:
  - It implements a notion of provider (one of V(selfsigned), V(ownca), V(entrust)) for your certificate.
author:
  - Yanis Guenane (@Spredzy)
  - Markus Teufelberger (@MarkusTeufelberger)
  - Felix Fontein (@felixfontein)
extends_documentation_fragment:
  - community.crypto.attributes
  - community.crypto.module_certificate
  - community.crypto.module_certificate.backend_entrust_documentation
  - community.crypto.module_certificate.backend_ownca_documentation
  - community.crypto.module_certificate.backend_selfsigned_documentation
attributes:
  check_mode:
    support: full
    details:
      - Currently in check mode, private keys will not be (re-)generated, only the changed status is set. This will change
        in community.crypto 3.0.0.
      - From community.crypto 3.0.0 on, the module will ignore check mode and always behave as if check mode is not active.
        If you think this breaks your use-case of this module, please create an issue in the community.crypto repository.
options:
  provider:
    description:
      - Name of the provider to use to generate/retrieve the OpenSSL certificate.
      - The V(entrust) provider requires credentials for the L(Entrust Certificate Services,
        https://www.entrustdatacard.com/products/categories/ssl-certificates) (ECS) API.
    type: str
    choices: [entrust, ownca, selfsigned]
    required: true

  content:
    description:
      - The existing certificate.
    type: str

seealso:
  - module: community.crypto.x509_certificate
"""

EXAMPLES = r"""
---
- name: Generate a Self Signed OpenSSL certificate
  community.crypto.x509_certificate_pipe:
    provider: selfsigned
    privatekey_path: /etc/ssl/private/ansible.com.pem
    csr_path: /etc/ssl/csr/ansible.com.csr
  register: result
- name: Print the certificate
  ansible.builtin.debug:
    var: result.certificate

# In the following example, both CSR and certificate file are stored on the
# machine where ansible-playbook is executed, while the OwnCA data (certificate,
# private key) are stored on the remote machine.

- name: (1/2) Generate an OpenSSL Certificate with the CSR provided inline
  community.crypto.x509_certificate_pipe:
    provider: ownca
    content: "{{ lookup('ansible.builtin.file', '/etc/ssl/csr/www.ansible.com.crt') }}"
    csr_content: "{{ lookup('ansible.builtin.file', '/etc/ssl/csr/www.ansible.com.csr') }}"
    ownca_cert: /path/to/ca_cert.crt
    ownca_privatekey: /path/to/ca_cert.key
    ownca_privatekey_passphrase: hunter2
  register: result

- name: (2/2) Store certificate
  ansible.builtin.copy:
    dest: /etc/ssl/csr/www.ansible.com.crt
    content: "{{ result.certificate }}"
  delegate_to: localhost
  when: result is changed

# In the following example, the certificate from another machine is signed by
# our OwnCA whose private key and certificate are only available on this
# machine (where ansible-playbook is executed), without having to write
# the certificate file to disk on localhost. The CSR could have been
# provided by community.crypto.openssl_csr_pipe earlier, or also have been
# read from the remote machine.

- name: (1/3) Read certificate's contents from remote machine
  ansible.builtin.slurp:
    src: /etc/ssl/csr/www.ansible.com.crt
  register: certificate_content

- name: (2/3) Generate an OpenSSL Certificate with the CSR provided inline
  community.crypto.x509_certificate_pipe:
    provider: ownca
    content: "{{ certificate_content.content | b64decode }}"
    csr_content: "{{ the_csr }}"
    ownca_cert: /path/to/ca_cert.crt
    ownca_privatekey: /path/to/ca_cert.key
    ownca_privatekey_passphrase: hunter2
  delegate_to: localhost
  register: result

- name: (3/3) Store certificate
  ansible.builtin.copy:
    dest: /etc/ssl/csr/www.ansible.com.crt
    content: "{{ result.certificate }}"
  when: result is changed
"""

RETURN = r"""
certificate:
  description: The (current or generated) certificate's content.
  returned: changed or success
  type: str
"""


from ansible.module_utils.common.text.converters import to_native
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
    OpenSSLObjectError,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate import (
    get_certificate_argument_spec,
    select_backend,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate_entrust import (
    EntrustCertificateProvider,
    add_entrust_provider_to_argument_spec,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate_ownca import (
    OwnCACertificateProvider,
    add_ownca_provider_to_argument_spec,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate_selfsigned import (
    SelfSignedCertificateProvider,
    add_selfsigned_provider_to_argument_spec,
)


class GenericCertificate(object):
    """Retrieve a certificate using the given module backend."""

    def __init__(self, module, module_backend):
        self.check_mode = module.check_mode
        self.module = module
        self.module_backend = module_backend
        self.changed = False
        if module.params["content"] is not None:
            self.module_backend.set_existing(module.params["content"].encode("utf-8"))

    def generate(self, module):
        if self.module_backend.needs_regeneration():
            if not self.check_mode:
                self.module_backend.generate_certificate()
            else:
                self.module.deprecate(
                    "Check mode support for x509_certificate_pipe will change in community.crypto 3.0.0"
                    " to behave the same as without check mode. You can get that behavior right now"
                    " by adding `check_mode: false` to the x509_certificate_pipe task. If you think this"
                    " breaks your use-case of this module, please create an issue in the"
                    " community.crypto repository",
                    version="3.0.0",
                    collection_name="community.crypto",
                )
            self.changed = True

    def dump(self, check_mode=False):
        result = self.module_backend.dump(include_certificate=True)
        result.update(
            {
                "changed": self.changed,
            }
        )
        return result


def main():
    argument_spec = get_certificate_argument_spec()
    argument_spec.argument_spec["provider"]["required"] = True
    add_entrust_provider_to_argument_spec(argument_spec)
    add_ownca_provider_to_argument_spec(argument_spec)
    add_selfsigned_provider_to_argument_spec(argument_spec)
    argument_spec.argument_spec.update(
        dict(
            content=dict(type="str"),
        )
    )
    module = argument_spec.create_ansible_module(
        supports_check_mode=True,
    )

    try:
        provider = module.params["provider"]
        provider_map = {
            "entrust": EntrustCertificateProvider,
            "ownca": OwnCACertificateProvider,
            "selfsigned": SelfSignedCertificateProvider,
        }

        backend = module.params["select_crypto_backend"]
        module_backend = select_backend(module, backend, provider_map[provider]())
        certificate = GenericCertificate(module, module_backend)
        certificate.generate(module)
        result = certificate.dump()
        module.exit_json(**result)
    except OpenSSLObjectError as exc:
        module.fail_json(msg=to_native(exc))


if __name__ == "__main__":
    main()
