Entrust Datacard - Support for "entrust" provider in openssl_certificate module (#59272)
* Addition of entrust provider to openssl_certificate module * Fix native return values of error messages and JSON response. * Documentation and syntax fixes per ansibot. * Refactored structure of for loop due to ansible test failures in python 2.6 * Remove OCSP functionality for inclusion in possible seperate future pull request. * Remove reissue support. * Indicate the entrust parameters are specific to entrust. * Comment fixes to make it clear module_utils request is used. * Fixes to not_after documentation * Response to pull request comments and cleanup of error handling for bad connections to properly use the 'six' HttpError for compatibility with both Python 2/3 underlying url libraries. * pep8/pycodestyle fixes. * Added code fragment and response to comments. * Update license to simplified BSD * Fixed botmeta typo * Include license text in api.yml * Remove unsupported certificate types, and always submit an explicit organization to match organization in CSR * Fix documentation misquote, add expired to a comment, and fix path check timing. * Update changelogs/fragments/59272-support-for-entrust-provider-in-openssl_certificate_module.yaml Co-Authored-By: Felix Fontein <felix@fontein.de>
This commit is contained in:
parent
ab07c206aa
commit
86366530e8
5 changed files with 655 additions and 4 deletions
3
.github/BOTMETA.yml
vendored
3
.github/BOTMETA.yml
vendored
|
@ -216,6 +216,8 @@ files:
|
||||||
labels: crypto
|
labels: crypto
|
||||||
maintainers: $team_crypto
|
maintainers: $team_crypto
|
||||||
supershipit: felixfontein
|
supershipit: felixfontein
|
||||||
|
$modules/crypto/openssl_certificate.py:
|
||||||
|
maintainers: ctrufan
|
||||||
$modules/database/influxdb/: kamsz
|
$modules/database/influxdb/: kamsz
|
||||||
$modules/database/mssql/mssql_db.py: Jmainguy kenichi-ogawa-1988
|
$modules/database/mssql/mssql_db.py: Jmainguy kenichi-ogawa-1988
|
||||||
$modules/database/mysql/: &mysql
|
$modules/database/mysql/: &mysql
|
||||||
|
@ -668,6 +670,7 @@ files:
|
||||||
labels:
|
labels:
|
||||||
- aws
|
- aws
|
||||||
- cloud
|
- cloud
|
||||||
|
maintainers: ctrufan $team_crypto
|
||||||
$module_utils/facts:
|
$module_utils/facts:
|
||||||
support: core
|
support: core
|
||||||
$module_utils/facts/hardware/aix.py: *aix
|
$module_utils/facts/hardware/aix.py: *aix
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
minor_changes:
|
||||||
|
- openssl_certificate - Add support for a new provider ``entrust`` (https://github.com/ansible/ansible/pull/59272).
|
0
lib/ansible/module_utils/ecs/__init__.py
Normal file
0
lib/ansible/module_utils/ecs/__init__.py
Normal file
351
lib/ansible/module_utils/ecs/api.py
Normal file
351
lib/ansible/module_utils/ecs/api.py
Normal file
|
@ -0,0 +1,351 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# This code is part of Ansible, but is an independent component.
|
||||||
|
# This particular file snippet, and this file snippet only, is licensed under the
|
||||||
|
# Modified BSD License. Modules you write using this snippet, which is embedded
|
||||||
|
# dynamically by Ansible, still belong to the author of the module, and may assign
|
||||||
|
# their own license to the complete work.
|
||||||
|
#
|
||||||
|
# Copyright (c), Entrust Datacard Corporation, 2019
|
||||||
|
# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
|
||||||
|
|
||||||
|
# Redistribution and use in source and binary forms, with or without modification,
|
||||||
|
# are permitted provided that the following conditions are met:
|
||||||
|
# 1. Redistributions of source code must retain the above copyright notice,
|
||||||
|
# this list of conditions and the following disclaimer.
|
||||||
|
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
# this list of conditions and the following disclaimer in the documentation
|
||||||
|
# and/or other materials provided with the distribution.
|
||||||
|
#
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||||
|
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
||||||
|
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||||
|
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||||
|
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
|
||||||
|
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
from ansible.module_utils._text import to_text, to_native
|
||||||
|
from ansible.module_utils.basic import missing_required_lib
|
||||||
|
from ansible.module_utils.six.moves.urllib.parse import urlencode
|
||||||
|
from ansible.module_utils.six.moves.urllib.error import HTTPError
|
||||||
|
from ansible.module_utils.urls import Request
|
||||||
|
|
||||||
|
YAML_IMP_ERR = None
|
||||||
|
try:
|
||||||
|
import yaml
|
||||||
|
except ImportError:
|
||||||
|
YAML_FOUND = False
|
||||||
|
YAML_IMP_ERR = traceback.format_exc()
|
||||||
|
else:
|
||||||
|
YAML_FOUND = True
|
||||||
|
|
||||||
|
valid_file_format = re.compile(r".*(\.)(yml|yaml|json)$")
|
||||||
|
|
||||||
|
|
||||||
|
class SessionConfigurationException(Exception):
|
||||||
|
""" Raised if we cannot configure a session with the API """
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class RestOperationException(Exception):
|
||||||
|
""" Encapsulate a REST API error """
|
||||||
|
|
||||||
|
def __init__(self, error):
|
||||||
|
self.status = to_native(error.get("status", None))
|
||||||
|
self.errors = [to_native(err.get("message")) for err in error.get("errors", {})]
|
||||||
|
self.message = to_native(" ".join(self.errors))
|
||||||
|
|
||||||
|
|
||||||
|
def generate_docstring(operation_spec):
|
||||||
|
"""Generate a docstring for an operation defined in operation_spec (swagger)"""
|
||||||
|
# Description of the operation
|
||||||
|
docs = operation_spec.get("description", "No Description")
|
||||||
|
docs += "\n\n"
|
||||||
|
|
||||||
|
# Parameters of the operation
|
||||||
|
parameters = operation_spec.get("parameters", [])
|
||||||
|
if len(parameters) != 0:
|
||||||
|
docs += "\tArguments:\n\n"
|
||||||
|
for parameter in parameters:
|
||||||
|
docs += "{0} ({1}:{2}): {3}\n".format(
|
||||||
|
parameter.get("name"),
|
||||||
|
parameter.get("type", "No Type"),
|
||||||
|
"Required" if parameter.get("required", False) else "Not Required",
|
||||||
|
parameter.get("description"),
|
||||||
|
)
|
||||||
|
|
||||||
|
return docs
|
||||||
|
|
||||||
|
|
||||||
|
def bind(instance, method, operation_spec):
|
||||||
|
def binding_scope_fn(*args, **kwargs):
|
||||||
|
return method(instance, *args, **kwargs)
|
||||||
|
|
||||||
|
# Make sure we don't confuse users; add the proper name and documentation to the function.
|
||||||
|
# Users can use !help(<function>) to get help on the function from interactive python or pdb
|
||||||
|
operation_name = operation_spec.get("operationId").split("Using")[0]
|
||||||
|
binding_scope_fn.__name__ = str(operation_name)
|
||||||
|
binding_scope_fn.__doc__ = generate_docstring(operation_spec)
|
||||||
|
|
||||||
|
return binding_scope_fn
|
||||||
|
|
||||||
|
|
||||||
|
class RestOperation(object):
|
||||||
|
def __init__(self, session, uri, method, parameters=None):
|
||||||
|
self.session = session
|
||||||
|
self.method = method
|
||||||
|
if parameters is None:
|
||||||
|
self.parameters = {}
|
||||||
|
else:
|
||||||
|
self.parameters = parameters
|
||||||
|
self.url = "{scheme}://{host}{base_path}{uri}".format(scheme="https", host=session._spec.get("host"), base_path=session._spec.get("basePath"), uri=uri)
|
||||||
|
|
||||||
|
def restmethod(self, *args, **kwargs):
|
||||||
|
"""Do the hard work of making the request here"""
|
||||||
|
|
||||||
|
# gather named path parameters and do substitution on the URL
|
||||||
|
if self.parameters:
|
||||||
|
path_parameters = {}
|
||||||
|
body_parameters = {}
|
||||||
|
query_parameters = {}
|
||||||
|
for x in self.parameters:
|
||||||
|
expected_location = x.get("in")
|
||||||
|
key_name = x.get("name", None)
|
||||||
|
key_value = kwargs.get(key_name, None)
|
||||||
|
if expected_location == "path" and key_name and key_value:
|
||||||
|
path_parameters.update({key_name: key_value})
|
||||||
|
elif expected_location == "body" and key_name and key_value:
|
||||||
|
body_parameters.update({key_name: key_value})
|
||||||
|
elif expected_location == "query" and key_name and key_value:
|
||||||
|
query_parameters.update({key_name: key_value})
|
||||||
|
|
||||||
|
if len(body_parameters.keys()) >= 1:
|
||||||
|
body_parameters = body_parameters.get(list(body_parameters.keys())[0])
|
||||||
|
else:
|
||||||
|
body_parameters = None
|
||||||
|
else:
|
||||||
|
path_parameters = {}
|
||||||
|
query_parameters = {}
|
||||||
|
body_parameters = None
|
||||||
|
|
||||||
|
# This will fail if we have not set path parameters with a KeyError
|
||||||
|
url = self.url.format(**path_parameters)
|
||||||
|
if query_parameters:
|
||||||
|
# modify the URL to add path parameters
|
||||||
|
url = url + "?" + urlencode(query_parameters)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if body_parameters:
|
||||||
|
body_parameters_json = json.dumps(body_parameters)
|
||||||
|
response = self.session.request.open(method=self.method, url=url, data=body_parameters_json)
|
||||||
|
else:
|
||||||
|
response = self.session.request.open(method=self.method, url=url)
|
||||||
|
request_error = False
|
||||||
|
except HTTPError as e:
|
||||||
|
# An HTTPError has the same methods available as a valid response from request.open
|
||||||
|
response = e
|
||||||
|
request_error = True
|
||||||
|
|
||||||
|
# Return the result if JSON and success ({} for empty responses)
|
||||||
|
# Raise an exception if there was a failure.
|
||||||
|
try:
|
||||||
|
result_code = response.getcode()
|
||||||
|
result = json.loads(response.read())
|
||||||
|
except ValueError:
|
||||||
|
result = {}
|
||||||
|
|
||||||
|
if result or result == {}:
|
||||||
|
if result_code and result_code < 400:
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
raise RestOperationException(result)
|
||||||
|
|
||||||
|
# Raise a generic RestOperationException if this fails
|
||||||
|
raise RestOperationException({"status": result_code, "errors": [{"message": "REST Operation Failed"}]})
|
||||||
|
|
||||||
|
|
||||||
|
class Resource(object):
|
||||||
|
""" Implement basic CRUD operations against a path. """
|
||||||
|
|
||||||
|
def __init__(self, session):
|
||||||
|
self.session = session
|
||||||
|
self.parameters = {}
|
||||||
|
|
||||||
|
for url in session._spec.get("paths").keys():
|
||||||
|
methods = session._spec.get("paths").get(url)
|
||||||
|
for method in methods.keys():
|
||||||
|
operation_spec = methods.get(method)
|
||||||
|
operation_name = operation_spec.get("operationId", None)
|
||||||
|
parameters = operation_spec.get("parameters")
|
||||||
|
|
||||||
|
if not operation_name:
|
||||||
|
if method.lower() == "post":
|
||||||
|
operation_name = "Create"
|
||||||
|
elif method.lower() == "get":
|
||||||
|
operation_name = "Get"
|
||||||
|
elif method.lower() == "put":
|
||||||
|
operation_name = "Update"
|
||||||
|
elif method.lower() == "delete":
|
||||||
|
operation_name = "Delete"
|
||||||
|
elif method.lower() == "patch":
|
||||||
|
operation_name = "Patch"
|
||||||
|
else:
|
||||||
|
raise SessionConfigurationException(to_native("Invalid REST method type {0}".format(method)))
|
||||||
|
|
||||||
|
# Get the non-parameter parts of the URL and append to the operation name
|
||||||
|
# e.g /application/version -> GetApplicationVersion
|
||||||
|
# e.g. /application/{id} -> GetApplication
|
||||||
|
# This may lead to duplicates, which we must prevent.
|
||||||
|
operation_name += re.sub(r"{(.*)}", "", url).replace("/", " ").title().replace(" ", "")
|
||||||
|
operation_spec["operationId"] = operation_name
|
||||||
|
|
||||||
|
op = RestOperation(session, url, method, parameters)
|
||||||
|
setattr(self, operation_name, bind(self, op.restmethod, operation_spec))
|
||||||
|
|
||||||
|
|
||||||
|
# Session to encapsulate the connection parameters of the module_utils Request object, the api spec, etc
|
||||||
|
class ECSSession(object):
|
||||||
|
def __init__(self, name, **kwargs):
|
||||||
|
"""
|
||||||
|
Initialize our session
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._set_config(name, **kwargs)
|
||||||
|
|
||||||
|
def client(self):
|
||||||
|
resource = Resource(self)
|
||||||
|
return resource
|
||||||
|
|
||||||
|
def _set_config(self, name, **kwargs):
|
||||||
|
headers = {"Content-Type": "application/json"}
|
||||||
|
self.request = Request(headers=headers, timeout=60)
|
||||||
|
|
||||||
|
configurators = [self._read_config_vars]
|
||||||
|
for configurator in configurators:
|
||||||
|
self._config = configurator(name, **kwargs)
|
||||||
|
if self._config:
|
||||||
|
break
|
||||||
|
if self._config is None:
|
||||||
|
raise SessionConfigurationException(to_native("No Configuration Found."))
|
||||||
|
|
||||||
|
# set up auth if passed
|
||||||
|
entrust_api_user = self.get_config("entrust_api_user")
|
||||||
|
entrust_api_key = self.get_config("entrust_api_key")
|
||||||
|
if entrust_api_user and entrust_api_key:
|
||||||
|
self.request.url_username = entrust_api_user
|
||||||
|
self.request.url_password = entrust_api_key
|
||||||
|
else:
|
||||||
|
raise SessionConfigurationException(to_native("User and key must be provided."))
|
||||||
|
|
||||||
|
# set up client certificate if passed (support all-in one or cert + key)
|
||||||
|
entrust_api_cert = self.get_config("entrust_api_cert")
|
||||||
|
entrust_api_cert_key = self.get_config("entrust_api_cert_key")
|
||||||
|
if entrust_api_cert:
|
||||||
|
self.request.client_cert = entrust_api_cert
|
||||||
|
if entrust_api_cert_key:
|
||||||
|
self.request.client_key = entrust_api_cert_key
|
||||||
|
else:
|
||||||
|
raise SessionConfigurationException(to_native("Client certificate for authentication to the API must be provided."))
|
||||||
|
|
||||||
|
# set up the spec
|
||||||
|
entrust_api_specification_path = self.get_config("entrust_api_specification_path")
|
||||||
|
|
||||||
|
if not entrust_api_specification_path.startswith("http") and not os.path.isfile(entrust_api_specification_path):
|
||||||
|
raise SessionConfigurationException(to_native("OpenAPI specification was not found at location {0}.".format(entrust_api_specification_path)))
|
||||||
|
if not valid_file_format.match(entrust_api_specification_path):
|
||||||
|
raise SessionConfigurationException(to_native("OpenAPI specification filename must end in .json, .yml or .yaml"))
|
||||||
|
|
||||||
|
self.verify = True
|
||||||
|
|
||||||
|
if entrust_api_specification_path.startswith("http"):
|
||||||
|
try:
|
||||||
|
http_response = Request().open(method="GET", url=entrust_api_specification_path)
|
||||||
|
http_response_contents = http_response.read()
|
||||||
|
if entrust_api_specification_path.endswith(".json"):
|
||||||
|
self._spec = json.load(http_response_contents)
|
||||||
|
elif entrust_api_specification_path.endswith(".yml") or entrust_api_specification_path.endswith(".yaml"):
|
||||||
|
self._spec = yaml.safe_load(http_response_contents)
|
||||||
|
except HTTPError as e:
|
||||||
|
raise SessionConfigurationException(to_native("Error downloading specification from address '{0}', received error code '{1}'".format(
|
||||||
|
entrust_api_specification_path, e.getcode())))
|
||||||
|
else:
|
||||||
|
with open(entrust_api_specification_path) as f:
|
||||||
|
if ".json" in entrust_api_specification_path:
|
||||||
|
self._spec = json.load(f)
|
||||||
|
elif ".yml" in entrust_api_specification_path or ".yaml" in entrust_api_specification_path:
|
||||||
|
self._spec = yaml.safe_load(f)
|
||||||
|
|
||||||
|
def get_config(self, item):
|
||||||
|
return self._config.get(item, None)
|
||||||
|
|
||||||
|
def _read_config_vars(self, name, **kwargs):
|
||||||
|
""" Read configuration from variables passed to the module. """
|
||||||
|
config = {}
|
||||||
|
|
||||||
|
entrust_api_specification_path = kwargs.get("entrust_api_specification_path")
|
||||||
|
if not entrust_api_specification_path or (not entrust_api_specification_path.startswith("http") and not os.path.isfile(entrust_api_specification_path)):
|
||||||
|
raise SessionConfigurationException(
|
||||||
|
to_native(
|
||||||
|
"Parameter provided for entrust_api_specification_path of value '{0}' was not a valid file path or HTTPS address.".format(
|
||||||
|
entrust_api_specification_path
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
for required_file in ["entrust_api_cert", "entrust_api_cert_key"]:
|
||||||
|
file_path = kwargs.get(required_file)
|
||||||
|
if not file_path or not os.path.isfile(file_path):
|
||||||
|
raise SessionConfigurationException(
|
||||||
|
to_native("Parameter provided for {0} of value '{1}' was not a valid file path.".format(required_file, file_path))
|
||||||
|
)
|
||||||
|
|
||||||
|
for required_var in ["entrust_api_user", "entrust_api_key"]:
|
||||||
|
if not kwargs.get(required_var):
|
||||||
|
raise SessionConfigurationException(to_native("Parameter provided for {0} was missing.".format(required_var)))
|
||||||
|
|
||||||
|
config["entrust_api_cert"] = kwargs.get("entrust_api_cert")
|
||||||
|
config["entrust_api_cert_key"] = kwargs.get("entrust_api_cert_key")
|
||||||
|
config["entrust_api_specification_path"] = kwargs.get("entrust_api_specification_path")
|
||||||
|
config["entrust_api_user"] = kwargs.get("entrust_api_user")
|
||||||
|
config["entrust_api_key"] = kwargs.get("entrust_api_key")
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
def ECSClient(entrust_api_user=None, entrust_api_key=None, entrust_api_cert=None, entrust_api_cert_key=None, entrust_api_specification_path=None):
|
||||||
|
"""Create an ECS client"""
|
||||||
|
|
||||||
|
if not YAML_FOUND:
|
||||||
|
raise SessionConfigurationException(missing_required_lib("PyYAML"), exception=YAML_IMP_ERR)
|
||||||
|
|
||||||
|
if entrust_api_specification_path is None:
|
||||||
|
entrust_api_specification_path = "https://cloud.entrust.net/EntrustCloud/documentation/cms-api-2.1.0.yaml"
|
||||||
|
|
||||||
|
# Not functionally necessary with current uses of this module_util, but better to be explicit for future use cases
|
||||||
|
entrust_api_user = to_text(entrust_api_user)
|
||||||
|
entrust_api_key = to_text(entrust_api_key)
|
||||||
|
entrust_api_cert_key = to_text(entrust_api_cert_key)
|
||||||
|
entrust_api_specification_path = to_text(entrust_api_specification_path)
|
||||||
|
|
||||||
|
return ECSSession(
|
||||||
|
"ecs",
|
||||||
|
entrust_api_user=entrust_api_user,
|
||||||
|
entrust_api_key=entrust_api_key,
|
||||||
|
entrust_api_cert=entrust_api_cert,
|
||||||
|
entrust_api_cert_key=entrust_api_cert_key,
|
||||||
|
entrust_api_specification_path=entrust_api_specification_path,
|
||||||
|
).client()
|
|
@ -19,7 +19,7 @@ version_added: "2.4"
|
||||||
short_description: Generate and/or check OpenSSL certificates
|
short_description: Generate and/or check OpenSSL certificates
|
||||||
description:
|
description:
|
||||||
- This module allows one to (re)generate OpenSSL certificates.
|
- This module allows one to (re)generate OpenSSL certificates.
|
||||||
- It implements a notion of provider (ie. C(selfsigned), C(ownca), C(acme), C(assertonly))
|
- It implements a notion of provider (ie. C(selfsigned), C(ownca), C(acme), C(assertonly), C(entrust))
|
||||||
for your certificate.
|
for your certificate.
|
||||||
- The C(assertonly) provider is intended for use cases where one is only interested in
|
- The C(assertonly) provider is intended for use cases where one is only interested in
|
||||||
checking properties of a supplied certificate.
|
checking properties of a supplied certificate.
|
||||||
|
@ -58,9 +58,11 @@ options:
|
||||||
description:
|
description:
|
||||||
- Name of the provider to use to generate/retrieve the OpenSSL certificate.
|
- Name of the provider to use to generate/retrieve the OpenSSL certificate.
|
||||||
- The C(assertonly) provider will not generate files and fail if the certificate file is missing.
|
- The C(assertonly) provider will not generate files and fail if the certificate file is missing.
|
||||||
|
- "The C(entrust) provider was added for Ansible 2.9 and requires credentials for the
|
||||||
|
L(https://www.entrustdatacard.com/products/categories/ssl-certificates,Entrust Certificate Services) (ECS) API."
|
||||||
type: str
|
type: str
|
||||||
required: true
|
required: true
|
||||||
choices: [ acme, assertonly, ownca, selfsigned ]
|
choices: [ acme, assertonly, entrust, ownca, selfsigned ]
|
||||||
|
|
||||||
force:
|
force:
|
||||||
description:
|
description:
|
||||||
|
@ -298,7 +300,6 @@ options:
|
||||||
type: str
|
type: str
|
||||||
aliases: [ notAfter ]
|
aliases: [ notAfter ]
|
||||||
|
|
||||||
|
|
||||||
valid_in:
|
valid_in:
|
||||||
description:
|
description:
|
||||||
- The certificate must still be valid at this relative time offset from now.
|
- The certificate must still be valid at this relative time offset from now.
|
||||||
|
@ -373,6 +374,97 @@ options:
|
||||||
default: no
|
default: no
|
||||||
version_added: "2.8"
|
version_added: "2.8"
|
||||||
|
|
||||||
|
entrust_cert_type:
|
||||||
|
description:
|
||||||
|
- The type of certificate product to request.
|
||||||
|
- This is only used by the C(entrust) provider.
|
||||||
|
type: str
|
||||||
|
default: STANDARD_SSL
|
||||||
|
choices: [ 'STANDARD_SSL', 'ADVANTAGE_SSL', 'UC_SSL', 'EV_SSL', 'WILDCARD_SSL', 'PRIVATE_SSL', 'PD_SSL', 'CDS_ENT_LITE', 'CDS_ENT_PRO', 'SMIME_ENT' ]
|
||||||
|
version_added: "2.9"
|
||||||
|
|
||||||
|
entrust_requester_email:
|
||||||
|
description:
|
||||||
|
- The email of the requester of the certificate (for tracking purposes).
|
||||||
|
- This is only used by the C(entrust) provider.
|
||||||
|
- This is required if the provider is C(entrust).
|
||||||
|
type: str
|
||||||
|
version_added: "2.9"
|
||||||
|
|
||||||
|
entrust_requester_name:
|
||||||
|
description:
|
||||||
|
- The name of the requester of the certificate (for tracking purposes).
|
||||||
|
- This is only used by the C(entrust) provider.
|
||||||
|
- This is required if the provider is C(entrust).
|
||||||
|
type: str
|
||||||
|
version_added: "2.9"
|
||||||
|
|
||||||
|
entrust_requester_phone:
|
||||||
|
description:
|
||||||
|
- The phone number of the requester of the certificate (for tracking purposes).
|
||||||
|
- This is only used by the C(entrust) provider.
|
||||||
|
- This is required if the provider is C(entrust).
|
||||||
|
type: str
|
||||||
|
version_added: "2.9"
|
||||||
|
|
||||||
|
entrust_api_user:
|
||||||
|
description:
|
||||||
|
- The username for authentication to the Entrust Certificate Services (ECS) API.
|
||||||
|
- This is only used by the C(entrust) provider.
|
||||||
|
- This is required if the provider is C(entrust).
|
||||||
|
type: str
|
||||||
|
version_added: "2.9"
|
||||||
|
|
||||||
|
entrust_api_key:
|
||||||
|
description:
|
||||||
|
- The key (password) for authentication to the Entrust Certificate Services (ECS) API.
|
||||||
|
- This is only used by the C(entrust) provider.
|
||||||
|
- This is required if the provider is C(entrust).
|
||||||
|
type: str
|
||||||
|
version_added: "2.9"
|
||||||
|
|
||||||
|
entrust_api_client_cert_path:
|
||||||
|
description:
|
||||||
|
- The path of the client certificate used to authenticate to the Entrust Certificate Services (ECS) API.
|
||||||
|
- This is only used by the C(entrust) provider.
|
||||||
|
- This is required if the provider is C(entrust).
|
||||||
|
type: path
|
||||||
|
version_added: "2.9"
|
||||||
|
|
||||||
|
entrust_api_client_cert_key_path:
|
||||||
|
description:
|
||||||
|
- The path of the key for the client certificate used to authenticate to the Entrust Certificate Services (ECS) API.
|
||||||
|
- This is only used by the C(entrust) provider.
|
||||||
|
- This is required if the provider is C(entrust).
|
||||||
|
type: path
|
||||||
|
version_added: "2.9"
|
||||||
|
|
||||||
|
entrust_not_after:
|
||||||
|
description:
|
||||||
|
- The point in time at which the certificate stops being valid.
|
||||||
|
- Time can be specified either as relative time or as absolute timestamp.
|
||||||
|
- Time will always be interpreted as UTC.
|
||||||
|
- Note that only the date (day, month, year) is supported for specifying expiry date of the issued certificate.
|
||||||
|
- The full date-time is adjusted to EST (GMT -5:00) before issuance, which may result in a certificate with an expiration date one day
|
||||||
|
earlier than expected if a relative time is used.
|
||||||
|
- The minimum certificate lifetime is 90 days, and maximum is three years.
|
||||||
|
- Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer
|
||||||
|
+ C([w | d | h | m | s]) (e.g. C(+32w1d2h).
|
||||||
|
- If this value is not specified, the certificate will stop being valid 365 days from now.
|
||||||
|
- This is only used by the C(entrust) provider.
|
||||||
|
type: str
|
||||||
|
default: +365d
|
||||||
|
version_added: "2.9"
|
||||||
|
|
||||||
|
entrust_api_specification_path:
|
||||||
|
description:
|
||||||
|
- Path to the specification file defining the Entrust Certificate Services (ECS) API.
|
||||||
|
- Can be used to keep a local copy of the specification to avoid downloading it every time the module is used.
|
||||||
|
- This is only used by the C(entrust) provider.
|
||||||
|
type: path
|
||||||
|
default: https://cloud.entrust.net/EntrustCloud/documentation/cms-api-2.1.0.yaml
|
||||||
|
version_added: "2.9"
|
||||||
|
|
||||||
extends_documentation_fragment: files
|
extends_documentation_fragment: files
|
||||||
notes:
|
notes:
|
||||||
- All ASN.1 TIME values should be specified following the YYYYMMDDHHMMSSZ pattern.
|
- All ASN.1 TIME values should be specified following the YYYYMMDDHHMMSSZ pattern.
|
||||||
|
@ -421,6 +513,21 @@ EXAMPLES = r'''
|
||||||
acme_challenge_path: /etc/ssl/challenges/ansible.com/
|
acme_challenge_path: /etc/ssl/challenges/ansible.com/
|
||||||
force: yes
|
force: yes
|
||||||
|
|
||||||
|
- name: Generate an Entrust certificate via the Entrust Certificate Services (ECS) API
|
||||||
|
openssl_certificate:
|
||||||
|
path: /etc/ssl/crt/ansible.com.crt
|
||||||
|
csr_path: /etc/ssl/csr/ansible.com.csr
|
||||||
|
provider: entrust
|
||||||
|
entrust_requester_name: Jo Doe
|
||||||
|
entrust_requester_email: jdoe@ansible.com
|
||||||
|
entrust_requester_phone: 555-555-5555
|
||||||
|
entrust_cert_type: STANDARD_SSL
|
||||||
|
entrust_api_user: apiusername
|
||||||
|
entrust_api_key: a^lv*32!cd9LnT
|
||||||
|
entrust_api_client_cert_path: /etc/ssl/entrust/ecs-client.crt
|
||||||
|
entrust_api_client_cert_key_path: /etc/ssl/entrust/ecs-key.crt
|
||||||
|
entrust_api_specification_path: /etc/ssl/entrust/api-docs/cms-api-2.1.0.yaml
|
||||||
|
|
||||||
# Examples for some checks one could use the assertonly provider for:
|
# Examples for some checks one could use the assertonly provider for:
|
||||||
|
|
||||||
# How to use the assertonly provider to implement and trigger your own custom certificate generation workflow:
|
# How to use the assertonly provider to implement and trigger your own custom certificate generation workflow:
|
||||||
|
@ -535,6 +642,7 @@ backup_file:
|
||||||
from random import randint
|
from random import randint
|
||||||
import abc
|
import abc
|
||||||
import datetime
|
import datetime
|
||||||
|
import time
|
||||||
import os
|
import os
|
||||||
import traceback
|
import traceback
|
||||||
from distutils.version import LooseVersion
|
from distutils.version import LooseVersion
|
||||||
|
@ -543,6 +651,7 @@ from ansible.module_utils import crypto as crypto_utils
|
||||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||||
from ansible.module_utils._text import to_native, to_bytes, to_text
|
from ansible.module_utils._text import to_native, to_bytes, to_text
|
||||||
from ansible.module_utils.compat import ipaddress as compat_ipaddress
|
from ansible.module_utils.compat import ipaddress as compat_ipaddress
|
||||||
|
from ansible.module_utils.ecs.api import ECSClient, RestOperationException, SessionConfigurationException
|
||||||
|
|
||||||
MINIMAL_CRYPTOGRAPHY_VERSION = '1.6'
|
MINIMAL_CRYPTOGRAPHY_VERSION = '1.6'
|
||||||
MINIMAL_PYOPENSSL_VERSION = '0.15'
|
MINIMAL_PYOPENSSL_VERSION = '0.15'
|
||||||
|
@ -564,7 +673,9 @@ try:
|
||||||
from cryptography import x509
|
from cryptography import x509
|
||||||
from cryptography.hazmat.backends import default_backend
|
from cryptography.hazmat.backends import default_backend
|
||||||
from cryptography.hazmat.primitives.serialization import Encoding
|
from cryptography.hazmat.primitives.serialization import Encoding
|
||||||
|
from cryptography.hazmat.primitives.hashes import SHA1
|
||||||
from cryptography.x509 import NameAttribute, Name
|
from cryptography.x509 import NameAttribute, Name
|
||||||
|
from cryptography.x509.oid import NameOID
|
||||||
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
|
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
|
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
|
||||||
|
@ -1701,6 +1812,167 @@ class AssertOnlyCertificate(AssertOnlyCertificateBase):
|
||||||
return self.cert.get_notBefore(), valid_in_date, self.cert.get_notAfter()
|
return self.cert.get_notBefore(), valid_in_date, self.cert.get_notAfter()
|
||||||
|
|
||||||
|
|
||||||
|
class EntrustCertificate(Certificate):
|
||||||
|
"""Retrieve a certificate using Entrust (ECS)."""
|
||||||
|
|
||||||
|
def __init__(self, module, backend):
|
||||||
|
super(EntrustCertificate, self).__init__(module, backend)
|
||||||
|
self.trackingId = None
|
||||||
|
self.notAfter = self.get_relative_time_option(module.params['entrust_not_after'], 'entrust_not_after')
|
||||||
|
|
||||||
|
if not os.path.exists(self.csr_path):
|
||||||
|
raise CertificateError(
|
||||||
|
'The certificate signing request file {0} does not exist'.format(self.csr_path)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.csr = crypto_utils.load_certificate_request(self.csr_path, backend=self.backend)
|
||||||
|
|
||||||
|
# ECS API defaults to using the validated organization tied to the account.
|
||||||
|
# We want to always force behavior of trying to use the organization provided in the CSR.
|
||||||
|
# To that end we need to parse out the organization from the CSR.
|
||||||
|
self.csr_org = None
|
||||||
|
if self.backend == 'pyopenssl':
|
||||||
|
csr_subject = self.csr.get_subject()
|
||||||
|
csr_subject_components = csr_subject.get_components()
|
||||||
|
for k, v in csr_subject_components:
|
||||||
|
if k.upper() == 'O':
|
||||||
|
# Entrust does not support multiple validated organizations in a single certificate
|
||||||
|
if self.csr_org is not None:
|
||||||
|
module.fail_json(msg=("Entrust provider does not currently support multiple validated organizations. Multiple organizations found in "
|
||||||
|
"Subject DN: '{0}'. ".format(csr_subject)))
|
||||||
|
else:
|
||||||
|
self.csr_org = v
|
||||||
|
elif self.backend == 'cryptography':
|
||||||
|
csr_subject_orgs = self.csr.subject.get_attributes_for_oid(NameOID.ORGANIZATION_NAME)
|
||||||
|
if len(csr_subject_orgs) == 1:
|
||||||
|
self.csr_org = csr_subject_orgs[0].value
|
||||||
|
elif len(csr_subject_orgs) > 1:
|
||||||
|
module.fail_json(msg=("Entrust provider does not currently support multiple validated organizations. Multiple organizations found in "
|
||||||
|
"Subject DN: '{0}'. ".format(self.csr.subject)))
|
||||||
|
# If no organization in the CSR, explicitly tell ECS that it should be blank in issued cert, not defaulted to
|
||||||
|
# organization tied to the account.
|
||||||
|
if self.csr_org is None:
|
||||||
|
self.csr_org = ''
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.ecs_client = ECSClient(
|
||||||
|
entrust_api_user=module.params.get('entrust_api_user'),
|
||||||
|
entrust_api_key=module.params.get('entrust_api_key'),
|
||||||
|
entrust_api_cert=module.params.get('entrust_api_client_cert_path'),
|
||||||
|
entrust_api_cert_key=module.params.get('entrust_api_client_cert_key_path'),
|
||||||
|
entrust_api_specification_path=module.params.get('entrust_api_specification_path')
|
||||||
|
)
|
||||||
|
except SessionConfigurationException as e:
|
||||||
|
module.fail_json(msg='Failed to initialize Entrust Provider: {0}'.format(to_native(e.message)))
|
||||||
|
|
||||||
|
def generate(self, module):
|
||||||
|
|
||||||
|
if not self.check(module, perms_required=False) or self.force:
|
||||||
|
# Read the CSR that was generated for us
|
||||||
|
body = {}
|
||||||
|
with open(self.csr_path, 'r') as csr_file:
|
||||||
|
body['csr'] = csr_file.read()
|
||||||
|
|
||||||
|
body['certType'] = module.params['entrust_cert_type']
|
||||||
|
|
||||||
|
# Handle expiration (30 days if not specified)
|
||||||
|
expiry = self.notAfter
|
||||||
|
if not expiry:
|
||||||
|
gmt_now = datetime.datetime.fromtimestamp(time.mktime(time.gmtime()))
|
||||||
|
expiry = gmt_now + datetime.timedelta(days=365)
|
||||||
|
|
||||||
|
expiry_iso3339 = expiry.strftime("%Y-%m-%dT%H:%M:%S.00Z")
|
||||||
|
body['certExpiryDate'] = expiry_iso3339
|
||||||
|
body['org'] = self.csr_org
|
||||||
|
body['tracking'] = {
|
||||||
|
'requesterName': module.params['entrust_requester_name'],
|
||||||
|
'requesterEmail': module.params['entrust_requester_email'],
|
||||||
|
'requesterPhone': module.params['entrust_requester_phone'],
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = self.ecs_client.NewCertRequest(Body=body)
|
||||||
|
self.trackingId = result.get('trackingId')
|
||||||
|
except RestOperationException as e:
|
||||||
|
module.fail_json(msg='Failed to request new certificate from Entrust Certificate Services (ECS): {0}'.format(to_native(e.message)))
|
||||||
|
|
||||||
|
if self.backup:
|
||||||
|
self.backup_file = module.backup_local(self.path)
|
||||||
|
crypto_utils.write_file(module, to_bytes(result.get('endEntityCert')))
|
||||||
|
self.cert = crypto_utils.load_certificate(self.path, backend=self.backend)
|
||||||
|
self.changed = True
|
||||||
|
|
||||||
|
def check(self, module, perms_required=True):
|
||||||
|
"""Ensure the resource is in its desired state."""
|
||||||
|
|
||||||
|
parent_check = super(EntrustCertificate, self).check(module, perms_required)
|
||||||
|
|
||||||
|
try:
|
||||||
|
cert_details = self._get_cert_details()
|
||||||
|
except RestOperationException as e:
|
||||||
|
module.fail_json(msg='Failed to get status of existing certificate from Entrust Certificate Services (ECS): {0}.'.format(to_native(e.message)))
|
||||||
|
|
||||||
|
# Always issue a new certificate if the certificate is expired, suspended or revoked
|
||||||
|
status = cert_details.get('status', False)
|
||||||
|
if status == 'EXPIRED' or status == 'SUSPENDED' or status == 'REVOKED':
|
||||||
|
return False
|
||||||
|
|
||||||
|
# If the requested cert type was specified and it is for a different certificate type than the initial certificate, a new one is needed
|
||||||
|
if module.params['entrust_cert_type'] and cert_details.get('certType') and module.params['entrust_cert_type'] != cert_details.get('certType'):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return parent_check
|
||||||
|
|
||||||
|
def _get_cert_details(self):
|
||||||
|
cert_details = {}
|
||||||
|
if self.cert:
|
||||||
|
serial_number = None
|
||||||
|
expiry = None
|
||||||
|
if self.backend == 'pyopenssl':
|
||||||
|
serial_number = "{0:X}".format(self.cert.get_serial_number())
|
||||||
|
time_string = to_native(self.cert.get_notAfter())
|
||||||
|
expiry = datetime.datetime.strptime(time_string, "%Y%m%d%H%M%SZ")
|
||||||
|
elif self.backend == 'cryptography':
|
||||||
|
serial_number = "{0:X}".format(self.cert.serial_number)
|
||||||
|
expiry = self.cert.not_valid_after
|
||||||
|
|
||||||
|
# get some information about the expiry of this certificate
|
||||||
|
expiry_iso3339 = expiry.strftime("%Y-%m-%dT%H:%M:%S.00Z")
|
||||||
|
cert_details['expiresAfter'] = expiry_iso3339
|
||||||
|
|
||||||
|
# If a trackingId is not already defined (from the result of a generate)
|
||||||
|
# use the serial number to identify the tracking Id
|
||||||
|
if self.trackingId is None and serial_number is not None:
|
||||||
|
cert_results = self.ecs_client.GetCertificates(serialNumber=serial_number).get('certificates', {})
|
||||||
|
|
||||||
|
# Finding 0 or more than 1 result is a very unlikely use case, it simply means we cannot perform additional checks
|
||||||
|
# on the 'state' as returned by Entrust Certificate Services (ECS). The general certificate validity is
|
||||||
|
# still checked as it is in the rest of the module.
|
||||||
|
if len(cert_results) == 1:
|
||||||
|
self.trackingId = cert_results[0].get('trackingId')
|
||||||
|
|
||||||
|
if self.trackingId is not None:
|
||||||
|
cert_details.update(self.ecs_client.GetCertificate(trackingId=self.trackingId))
|
||||||
|
|
||||||
|
return cert_details
|
||||||
|
|
||||||
|
def dump(self, check_mode=False):
|
||||||
|
|
||||||
|
result = {
|
||||||
|
'changed': self.changed,
|
||||||
|
'filename': self.path,
|
||||||
|
'privatekey': self.privatekey_path,
|
||||||
|
'csr': self.csr_path,
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.backup_file:
|
||||||
|
result['backup_file'] = self.backup_file
|
||||||
|
|
||||||
|
result.update(self._get_cert_details())
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
class AcmeCertificate(Certificate):
|
class AcmeCertificate(Certificate):
|
||||||
"""Retrieve a certificate using the ACME protocol."""
|
"""Retrieve a certificate using the ACME protocol."""
|
||||||
|
|
||||||
|
@ -1777,7 +2049,7 @@ def main():
|
||||||
argument_spec=dict(
|
argument_spec=dict(
|
||||||
state=dict(type='str', default='present', choices=['present', 'absent']),
|
state=dict(type='str', default='present', choices=['present', 'absent']),
|
||||||
path=dict(type='path', required=True),
|
path=dict(type='path', required=True),
|
||||||
provider=dict(type='str', choices=['acme', 'assertonly', 'ownca', 'selfsigned']),
|
provider=dict(type='str', choices=['acme', 'assertonly', 'entrust', 'ownca', 'selfsigned']),
|
||||||
force=dict(type='bool', default=False,),
|
force=dict(type='bool', default=False,),
|
||||||
csr_path=dict(type='path'),
|
csr_path=dict(type='path'),
|
||||||
backup=dict(type='bool', default=False),
|
backup=dict(type='bool', default=False),
|
||||||
|
@ -1826,9 +2098,28 @@ def main():
|
||||||
acme_accountkey_path=dict(type='path'),
|
acme_accountkey_path=dict(type='path'),
|
||||||
acme_challenge_path=dict(type='path'),
|
acme_challenge_path=dict(type='path'),
|
||||||
acme_chain=dict(type='bool', default=False),
|
acme_chain=dict(type='bool', default=False),
|
||||||
|
|
||||||
|
# provider: entrust
|
||||||
|
entrust_cert_type=dict(type='str', default='STANDARD_SSL',
|
||||||
|
choices=['STANDARD_SSL', 'ADVANTAGE_SSL', 'UC_SSL', 'EV_SSL', 'WILDCARD_SSL',
|
||||||
|
'PRIVATE_SSL', 'PD_SSL', 'CDS_ENT_LITE', 'CDS_ENT_PRO', 'SMIME_ENT']),
|
||||||
|
entrust_requester_email=dict(type='str'),
|
||||||
|
entrust_requester_name=dict(type='str'),
|
||||||
|
entrust_requester_phone=dict(type='str'),
|
||||||
|
entrust_api_user=dict(type='str'),
|
||||||
|
entrust_api_key=dict(type='str', no_log=True),
|
||||||
|
entrust_api_client_cert_path=dict(type='path'),
|
||||||
|
entrust_api_client_cert_key_path=dict(type='path', no_log=True),
|
||||||
|
entrust_api_specification_path=dict(type='path', default='https://cloud.entrust.net/EntrustCloud/documentation/cms-api-2.1.0.yaml'),
|
||||||
|
entrust_not_after=dict(type='str', default='+365d'),
|
||||||
),
|
),
|
||||||
supports_check_mode=True,
|
supports_check_mode=True,
|
||||||
add_file_common_args=True,
|
add_file_common_args=True,
|
||||||
|
required_if=[
|
||||||
|
['provider', 'entrust', ['entrust_requester_email', 'entrust_requester_name', 'entrust_requester_phone',
|
||||||
|
'entrust_api_user', 'entrust_api_key', 'entrust_api_client_cert_path',
|
||||||
|
'entrust_api_client_cert_key_path']]
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -1887,6 +2178,8 @@ def main():
|
||||||
certificate = AcmeCertificate(module, 'pyopenssl')
|
certificate = AcmeCertificate(module, 'pyopenssl')
|
||||||
elif provider == 'ownca':
|
elif provider == 'ownca':
|
||||||
certificate = OwnCACertificate(module)
|
certificate = OwnCACertificate(module)
|
||||||
|
elif provider == 'entrust':
|
||||||
|
certificate = EntrustCertificate(module, 'pyopenssl')
|
||||||
else:
|
else:
|
||||||
certificate = AssertOnlyCertificate(module)
|
certificate = AssertOnlyCertificate(module)
|
||||||
elif backend == 'cryptography':
|
elif backend == 'cryptography':
|
||||||
|
@ -1902,6 +2195,8 @@ def main():
|
||||||
certificate = AcmeCertificate(module, 'cryptography')
|
certificate = AcmeCertificate(module, 'cryptography')
|
||||||
elif provider == 'ownca':
|
elif provider == 'ownca':
|
||||||
certificate = OwnCACertificateCryptography(module)
|
certificate = OwnCACertificateCryptography(module)
|
||||||
|
elif provider == 'entrust':
|
||||||
|
certificate = EntrustCertificate(module, 'cryptography')
|
||||||
else:
|
else:
|
||||||
certificate = AssertOnlyCertificateCryptography(module)
|
certificate = AssertOnlyCertificateCryptography(module)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue