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
|
||||
maintainers: $team_crypto
|
||||
supershipit: felixfontein
|
||||
$modules/crypto/openssl_certificate.py:
|
||||
maintainers: ctrufan
|
||||
$modules/database/influxdb/: kamsz
|
||||
$modules/database/mssql/mssql_db.py: Jmainguy kenichi-ogawa-1988
|
||||
$modules/database/mysql/: &mysql
|
||||
|
@ -668,6 +670,7 @@ files:
|
|||
labels:
|
||||
- aws
|
||||
- cloud
|
||||
maintainers: ctrufan $team_crypto
|
||||
$module_utils/facts:
|
||||
support: core
|
||||
$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
|
||||
description:
|
||||
- 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.
|
||||
- The C(assertonly) provider is intended for use cases where one is only interested in
|
||||
checking properties of a supplied certificate.
|
||||
|
@ -58,9 +58,11 @@ options:
|
|||
description:
|
||||
- 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(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
|
||||
required: true
|
||||
choices: [ acme, assertonly, ownca, selfsigned ]
|
||||
choices: [ acme, assertonly, entrust, ownca, selfsigned ]
|
||||
|
||||
force:
|
||||
description:
|
||||
|
@ -298,7 +300,6 @@ options:
|
|||
type: str
|
||||
aliases: [ notAfter ]
|
||||
|
||||
|
||||
valid_in:
|
||||
description:
|
||||
- The certificate must still be valid at this relative time offset from now.
|
||||
|
@ -373,6 +374,97 @@ options:
|
|||
default: no
|
||||
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
|
||||
notes:
|
||||
- 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/
|
||||
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:
|
||||
|
||||
# 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
|
||||
import abc
|
||||
import datetime
|
||||
import time
|
||||
import os
|
||||
import traceback
|
||||
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._text import to_native, to_bytes, to_text
|
||||
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_PYOPENSSL_VERSION = '0.15'
|
||||
|
@ -564,7 +673,9 @@ try:
|
|||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives.serialization import Encoding
|
||||
from cryptography.hazmat.primitives.hashes import SHA1
|
||||
from cryptography.x509 import NameAttribute, Name
|
||||
from cryptography.x509.oid import NameOID
|
||||
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
|
||||
except ImportError:
|
||||
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()
|
||||
|
||||
|
||||
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):
|
||||
"""Retrieve a certificate using the ACME protocol."""
|
||||
|
||||
|
@ -1777,7 +2049,7 @@ def main():
|
|||
argument_spec=dict(
|
||||
state=dict(type='str', default='present', choices=['present', 'absent']),
|
||||
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,),
|
||||
csr_path=dict(type='path'),
|
||||
backup=dict(type='bool', default=False),
|
||||
|
@ -1826,9 +2098,28 @@ def main():
|
|||
acme_accountkey_path=dict(type='path'),
|
||||
acme_challenge_path=dict(type='path'),
|
||||
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,
|
||||
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:
|
||||
|
@ -1887,6 +2178,8 @@ def main():
|
|||
certificate = AcmeCertificate(module, 'pyopenssl')
|
||||
elif provider == 'ownca':
|
||||
certificate = OwnCACertificate(module)
|
||||
elif provider == 'entrust':
|
||||
certificate = EntrustCertificate(module, 'pyopenssl')
|
||||
else:
|
||||
certificate = AssertOnlyCertificate(module)
|
||||
elif backend == 'cryptography':
|
||||
|
@ -1902,6 +2195,8 @@ def main():
|
|||
certificate = AcmeCertificate(module, 'cryptography')
|
||||
elif provider == 'ownca':
|
||||
certificate = OwnCACertificateCryptography(module)
|
||||
elif provider == 'entrust':
|
||||
certificate = EntrustCertificate(module, 'cryptography')
|
||||
else:
|
||||
certificate = AssertOnlyCertificateCryptography(module)
|
||||
|
||||
|
|
Loading…
Reference in a new issue