Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[bumpversion]
commit = True
current_version = 2.4.0
current_version = 3.0.0
tag = True
tag_name = {new_version}

Expand Down
10 changes: 5 additions & 5 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,16 @@ indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

[*.md]
indent_size = 2
indent_style = space

[LICENSE.txt]
insert_final_newline = false

[*.{diff,patch}]
trim_trailing_whitespace = false

[*.{json,yaml,yml}]
[*.{json,md,yaml,yml}]
indent_size = 2
indent_style = space

[.{prettierrc,yamllint}]
indent_size = 2
indent_style = space
26 changes: 26 additions & 0 deletions .github/release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
changelog:
exclude:
labels:
- duplicate
- ignore-for-release
- invalid
- maintenance
- question
- wontfix
categories:
- title: Breaking Changes 🛠
labels:
- backwards-incompatible
- breaking
- title: Fixed Bugs 🐛
labels:
- bug
- fix
- title: Exciting New Features 🎉
labels:
- enhancement
- feature
- title: 👒 Dependencies
labels:
- dependencies
1 change: 1 addition & 0 deletions .github/workflows/checks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ jobs:
uses: broadinstitute/shared-workflows/.github/workflows/python-unit-test.yaml@v6.0.0
with:
python_package_name: cert_manager
python_versions: '{ "versions": [ "3.9", "3.10", "3.11", "3.12", "3.13", "3.14" ] }'
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ repos:
- --allow-missing-credentials
- id: detect-private-key
- id: end-of-file-fixer
exclude: '.bumpversion.cfg'
exclude: ".bumpversion.cfg"
- id: mixed-line-ending
- id: name-tests-test
args:
Expand Down
13 changes: 12 additions & 1 deletion cert_manager/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from .acme import ACMEAccount
from .admin import Admin
from .client import Client
from .dcv import DomainControlValidation
from .domain import Domain
from .organization import Organization
from .person import Person
Expand All @@ -12,5 +13,15 @@
from .ssl import SSL

__all__ = [
"ACMEAccount", "Admin", "Client", "Domain", "Organization", "PendingError", "Person", "Report", "SMIME", "SSL"
"ACMEAccount",
"Admin",
"Client",
"Domain",
"DomainControlValidation",
"Organization",
"PendingError",
"Person",
"Report",
"SMIME",
"SSL",
]
2 changes: 1 addition & 1 deletion cert_manager/__version__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
__title__ = "cert_manager"
__description__ = "Python interface to the Sectigo Certificate Manager REST API"
__url__ = "https://github.com/broadinstitute/python-cert_manager"
__version__ = "2.4.0"
__version__ = "3.0.0"
108 changes: 64 additions & 44 deletions cert_manager/_certificates.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,59 +32,66 @@ class Certificates(Endpoint):
def __init__(self, client, endpoint, api_version="v1"):
"""Initialize the class.

:param object client: An instantiated cert_manager.Client object
:param string endpoint: The URL of the API endpoint (ex. "/ssl")
:param string api_version: The API version to use; the default is "v1"
Args:
client: An instantiated cert_manager.Client object
endpoint: The URL of the API endpoint (ex. "/ssl")
api_version: The API version to use; the default is "v1"
"""
super().__init__(client=client, endpoint=endpoint, api_version=api_version)

# Set to None initially. Will be filled in by methods later.
self.__cert_types = None
self.__custom_fields = None
self.__reason_maxlen = 512
self._cert_types = None
self._custom_fields = None
self._reason_maxlen = 512

@property
def types(self):
"""Retrieve all certificate types that are currently available.

:return list: A list of dictionaries of certificate types
Returns:
A list of dictionaries of certificate types
"""
# Only go to the API if we haven't done the API call yet, or if someone
# specifically wants to refresh the internal cache
if not self.__cert_types:
if not self._cert_types:
url = self._url("/types")
result = self._client.get(url)

# Build a dictionary instead of a flat list of dictionaries
self.__cert_types = {}
self._cert_types = {}
for res in result.json():
name = res["name"]
self.__cert_types[name] = {}
self.__cert_types[name]["id"] = res["id"]
self.__cert_types[name]["terms"] = res["terms"]
self._cert_types[name] = {}
self._cert_types[name]["id"] = res["id"]
self._cert_types[name]["terms"] = res["terms"]

return self.__cert_types
return self._cert_types

@property
def custom_fields(self):
"""Retrieve all custom fields defined for SSL certificates.

:return list: A list of dictionaries of custom fields
Returns:
A list of dictionaries of custom fields
"""
# Only go to the API if we haven't done the API call yet, or if someone
# specifically wants to refresh the internal cache
if not self.__custom_fields:
if not self._custom_fields:
url = self._url("/customFields")
result = self._client.get(url)

self.__custom_fields = result.json()
self._custom_fields = result.json()

return self.__custom_fields
return self._custom_fields

def _validate_custom_fields(self, custom_fields):
"""Check the structure and contents of a list of custom fields dicts. Raise exceptions if validation fails.

:raises Exception: if any of the validation steps fail
Args:
custom_fields: A list of dictionaries representing custom fields

Raises:
CustomFieldsError: if any of the validation steps fail
"""
# Make sure all custom fields are valid if present
custom_field_names = [f['name'] for f in self.custom_fields]
Expand Down Expand Up @@ -114,9 +121,11 @@ def collect(self, cert_id, cert_format):

This method will raise a PendingError exception if the certificate is still in a pending state.

:param int cert_id: The certificate ID
:param str cert_format: The format in which to retreive the certificate. Allowed values: *self.valid_formats*
:return str: the string representing the certificate in the requested format
Args:
cert_id: The certificate ID
cert_format: The format in which to retreive the certificate. Allowed values: *self.valid_formats*
Returns:
The string representing the certificate in the requested format
"""
if cert_format not in self.valid_formats:
raise ValueError(f"Invalid cert format {cert_format} provided")
Expand All @@ -135,16 +144,20 @@ def collect(self, cert_id, cert_format):
def enroll(self, **kwargs):
"""Enroll a certificate request with Sectigo to generate a certificate.

:param string cert_type_name: The full cert type name
Note: the name must match names returned from the get_types() method
:param string csr: The Certificate Signing Request (CSR)
:param int term: The length, in days, for the certificate to be issued
:param int org_id: The ID of the organization in which to enroll the certificate
:param list subject_alt_names: A list of Subject Alternative Names
:param list external_requester: One or more e-mail addresses
:param list custom_fields: zero or more objects representing custom fields and their values
Note: each object must have a 'name' key and a 'value' key
:return dict: The certificate_id and the normal status messages for errors
Args:
kwargs: A dictionary of arguments to pass to the API.
Required fields are:
cert_type_name: The full cert type name
Note: the name must match names returned from the get_types() method
csr: The Certificate Signing Request (CSR)
term: The length, in days, for the certificate to be issued
org_id: The ID of the organization in which to enroll the certificate
subject_alt_names: A list of Subject Alternative Names
external_requester: One or more e-mail addresses
custom_fields: zero or more objects representing custom fields and their values
Note: each object must have a 'name' key and a 'value' key
Returns:
The certificate_id and the normal status messages for errors
"""
# Retrieve all the arguments
cert_type_name = kwargs.get("cert_type_name")
Expand Down Expand Up @@ -191,13 +204,17 @@ def enroll(self, **kwargs):
def replace(self, **kwargs):
"""Replace a pre-existing certificate.

:param int cert_id: The certificate ID
:param string csr: The Certificate Signing Request (CSR)
:param string common_name: Certificate common name.
:param str reason: Reason for replacement (up to 512 characters), can be blank: "", but must exist.
:param list subject_alt_names: A list of Subject Alternative Names.
:return: The result of the operation, "Successful" on success
:rtype: dict
Args:
kwargs: A dictionary of arguments to pass to the API.
Required fields are:
cert_id: The certificate ID
csr: The Certificate Signing Request (CSR)
common_name: Certificate common name.
reason: Reason for replacement (up to 512 characters), can be blank: "", but must exist.
subject_alt_names: A list of Subject Alternative Names.

Returns:
An empty dictionary on success
"""
# Retrieve all the arguments
cert_id = kwargs.get("cert_id")
Expand All @@ -222,16 +239,19 @@ def replace(self, **kwargs):
def revoke(self, cert_id, reason=""):
"""Revoke the certificate specified by the certificate ID.

:param int cert_id: The certificate ID
:param str reason: The Reason for revocation.
Reason can be up to 512 characters and cannot be blank (i.e. empty string)
:return dict: The revocation result. "Successful" on success
Args:
cert_id: The certificate ID
reason: The Reason for revocation.
Reason can be up to 512 characters and cannot be blank (i.e. empty string)

Returns:
An empty dictionary on success
"""
url = self._url(f"/revoke/{cert_id}")

# Sectigo has a 512 character limit on the "reason" message, so catch that here.
if not reason or len(reason) >= self.__reason_maxlen:
raise ValueError(f"Sectigo limit: reason must be > 0 character and < {self.__reason_maxlen} characters")
if not reason or len(reason) >= self._reason_maxlen:
raise ValueError(f"Sectigo limit: reason must be > 0 character and < {self._reason_maxlen} characters")

data = {"reason": reason}

Expand Down
33 changes: 19 additions & 14 deletions cert_manager/_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ class Endpoint:
def __init__(self, client, endpoint, api_version="v1"):
"""Initialize the class.

:param object client: An instantiated cert_manager.Client object
:param string endpoint: The API endpoint you are accessing (for example: "/ssl")
:param string api_version: The API version to use; the default is "v1"
Args:
client: An instantiated cert_manager.Client object
endpoint: The API endpoint you are accessing (for example: "/ssl")
api_version: The API version to use; the default is "v1"
"""
self._client = client
self._api_version = api_version
Expand All @@ -35,14 +36,15 @@ def api_url(self):
def create_api_url(base_url, service, version):
"""Build the entire Certificate Manager API URL for the service and version.

:param str base_url: The base URL you have i.e. for https://hard.cert-manager.com/api/ssl/v1/ the base URL
would be https://hard.cert-manager.com/api
:param str service: The API service to use i.e. for https://hard.cert-manager.com/api/ssl/v1/ the service would
be /ssl
:param str version: The API version to use i.e. for https://hard.cert-manager.com/api/ssl/v1/ the version would
be /v1
:return: The full URL
:rtype: str
Args:
base_url: The base URL you have
i.e. for https://hard.cert-manager.com/api/ssl/v1/ the base URL would be https://hard.cert-manager.com/api
service: The API service to use
i.e. for https://hard.cert-manager.com/api/ssl/v1/ the service would be /ssl
version: The API version to use
i.e. for https://hard.cert-manager.com/api/ssl/v1/ the version would be /v1
Returns:
The full URL
"""
url = base_url.rstrip("/")
url += "/" + service.strip("/")
Expand All @@ -54,9 +56,12 @@ def create_api_url(base_url, service, version):
def _url(self, *args):
"""Build the endpoint URL based on the API URL inside this object.

:param str suffix: The suffix of the URL you wish to create i.e. for
https://hard.cert-manager.com/api/ssl/v1/types the suffix would be /types
:return str: The full URL
Args:
args: A list of suffixes of the URL you wish to create
i.e. for https://hard.cert-manager.com/api/ssl/v1/types the suffix would be /types

Returns:
The full URL
"""
url = self._api_url.rstrip("/")
for suffix in args:
Expand Down
15 changes: 10 additions & 5 deletions cert_manager/_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ def traffic_log(traffic_logger=None):

Note: The "DEBUG" level should *never* be used in production.

:param obj traffic_logger: a logging.Logger to use for logging messages.
Args:
traffic_logger (logging.Logger): a logging.Logger to use for logging messages.
"""
def decorator(func):
"""Wrap the actual decorator so a reference to the function can be returned."""
Expand Down Expand Up @@ -81,7 +82,9 @@ def version_hack(service, version="v1"):
temporarily change the version to something other than what the object was initialized with so that the internal
*self.api_url* will be correct.

:param version: API version string to use. If None, 'v1'
Args:
service: The API service to use.
version: API version string to use. If None, 'v1'
"""
def decorator(func):
"""Wrap the actual decorator so a reference to the function can be returned."""
Expand Down Expand Up @@ -125,10 +128,12 @@ def decorator(*args, **kwargs):
The `size` and `position` parameters passed through `kwargs` to this function will be used
by the pagination wrapper to page through results.

:param list args: Positional parameters to pass to the wrapped function
:param dict kwargs: A dictionary with any parameters to add to the request URL
Args:
args: Positional parameters to pass to the wrapped function
kwargs: A dictionary with any parameters to add to the request URL

:return obj: Yield results from the wrapped function's response for each request
Returns:
Yield (generator) results from the wrapped function's response for each request
"""
size = kwargs.pop("size", 200) # max seems to be 200 by default
position = kwargs.pop("position", 0) # 0-..
Expand Down
Loading
Loading