Skip to content

Commit dc13d08

Browse files
committed
feat: Add the ability to use OAuth2 to authenticate to the REST API
fix: All dunder variables with single fix: Missing DomainControlValidation in module init feat: Add unit tests for new auth method chore: Update support files feat: Update README chore: Update Poetry and the package version feat: Add a mise.toml file chore: Migrate all docblocks to Google style
1 parent d0df813 commit dc13d08

28 files changed

+1225
-736
lines changed

.bumpversion.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[bumpversion]
22
commit = True
3-
current_version = 2.4.0
3+
current_version = 3.0.0
44
tag = True
55
tag_name = {new_version}
66

.editorconfig

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,16 @@ indent_style = space
1010
insert_final_newline = true
1111
trim_trailing_whitespace = true
1212

13-
[*.md]
14-
indent_size = 2
15-
indent_style = space
16-
1713
[LICENSE.txt]
1814
insert_final_newline = false
1915

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

23-
[*.{json,yaml,yml}]
19+
[*.{json,md,yaml,yml}]
20+
indent_size = 2
21+
indent_style = space
22+
23+
[.{prettierrc,yamllint}]
2424
indent_size = 2
2525
indent_style = space

.github/release.yaml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
changelog:
3+
exclude:
4+
labels:
5+
- duplicate
6+
- ignore-for-release
7+
- invalid
8+
- maintenance
9+
- question
10+
- wontfix
11+
categories:
12+
- title: Breaking Changes 🛠
13+
labels:
14+
- backwards-incompatible
15+
- breaking
16+
- title: Fixed Bugs 🐛
17+
labels:
18+
- bug
19+
- fix
20+
- title: Exciting New Features 🎉
21+
labels:
22+
- enhancement
23+
- feature
24+
- title: 👒 Dependencies
25+
labels:
26+
- dependencies

.github/workflows/checks.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,4 @@ jobs:
2222
uses: broadinstitute/shared-workflows/.github/workflows/python-unit-test.yaml@v6.0.0
2323
with:
2424
python_package_name: cert_manager
25+
python_versions: '{ "versions": [ "3.9", "3.10", "3.11", "3.12", "3.13", "3.14" ] }'

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ repos:
1717
- --allow-missing-credentials
1818
- id: detect-private-key
1919
- id: end-of-file-fixer
20-
exclude: '.bumpversion.cfg'
20+
exclude: ".bumpversion.cfg"
2121
- id: mixed-line-ending
2222
- id: name-tests-test
2323
args:

cert_manager/__init__.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from .acme import ACMEAccount
55
from .admin import Admin
66
from .client import Client
7+
from .dcv import DomainControlValidation
78
from .domain import Domain
89
from .organization import Organization
910
from .person import Person
@@ -12,5 +13,15 @@
1213
from .ssl import SSL
1314

1415
__all__ = [
15-
"ACMEAccount", "Admin", "Client", "Domain", "Organization", "PendingError", "Person", "Report", "SMIME", "SSL"
16+
"ACMEAccount",
17+
"Admin",
18+
"Client",
19+
"Domain",
20+
"DomainControlValidation",
21+
"Organization",
22+
"PendingError",
23+
"Person",
24+
"Report",
25+
"SMIME",
26+
"SSL",
1627
]

cert_manager/__version__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@
33
__title__ = "cert_manager"
44
__description__ = "Python interface to the Sectigo Certificate Manager REST API"
55
__url__ = "https://github.com/broadinstitute/python-cert_manager"
6-
__version__ = "2.4.0"
6+
__version__ = "3.0.0"

cert_manager/_certificates.py

Lines changed: 64 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -32,59 +32,66 @@ class Certificates(Endpoint):
3232
def __init__(self, client, endpoint, api_version="v1"):
3333
"""Initialize the class.
3434
35-
:param object client: An instantiated cert_manager.Client object
36-
:param string endpoint: The URL of the API endpoint (ex. "/ssl")
37-
:param string api_version: The API version to use; the default is "v1"
35+
Args:
36+
client: An instantiated cert_manager.Client object
37+
endpoint: The URL of the API endpoint (ex. "/ssl")
38+
api_version: The API version to use; the default is "v1"
3839
"""
3940
super().__init__(client=client, endpoint=endpoint, api_version=api_version)
4041

4142
# Set to None initially. Will be filled in by methods later.
42-
self.__cert_types = None
43-
self.__custom_fields = None
44-
self.__reason_maxlen = 512
43+
self._cert_types = None
44+
self._custom_fields = None
45+
self._reason_maxlen = 512
4546

4647
@property
4748
def types(self):
4849
"""Retrieve all certificate types that are currently available.
4950
50-
:return list: A list of dictionaries of certificate types
51+
Returns:
52+
A list of dictionaries of certificate types
5153
"""
5254
# Only go to the API if we haven't done the API call yet, or if someone
5355
# specifically wants to refresh the internal cache
54-
if not self.__cert_types:
56+
if not self._cert_types:
5557
url = self._url("/types")
5658
result = self._client.get(url)
5759

5860
# Build a dictionary instead of a flat list of dictionaries
59-
self.__cert_types = {}
61+
self._cert_types = {}
6062
for res in result.json():
6163
name = res["name"]
62-
self.__cert_types[name] = {}
63-
self.__cert_types[name]["id"] = res["id"]
64-
self.__cert_types[name]["terms"] = res["terms"]
64+
self._cert_types[name] = {}
65+
self._cert_types[name]["id"] = res["id"]
66+
self._cert_types[name]["terms"] = res["terms"]
6567

66-
return self.__cert_types
68+
return self._cert_types
6769

6870
@property
6971
def custom_fields(self):
7072
"""Retrieve all custom fields defined for SSL certificates.
7173
72-
:return list: A list of dictionaries of custom fields
74+
Returns:
75+
A list of dictionaries of custom fields
7376
"""
7477
# Only go to the API if we haven't done the API call yet, or if someone
7578
# specifically wants to refresh the internal cache
76-
if not self.__custom_fields:
79+
if not self._custom_fields:
7780
url = self._url("/customFields")
7881
result = self._client.get(url)
7982

80-
self.__custom_fields = result.json()
83+
self._custom_fields = result.json()
8184

82-
return self.__custom_fields
85+
return self._custom_fields
8386

8487
def _validate_custom_fields(self, custom_fields):
8588
"""Check the structure and contents of a list of custom fields dicts. Raise exceptions if validation fails.
8689
87-
:raises Exception: if any of the validation steps fail
90+
Args:
91+
custom_fields: A list of dictionaries representing custom fields
92+
93+
Raises:
94+
CustomFieldsError: if any of the validation steps fail
8895
"""
8996
# Make sure all custom fields are valid if present
9097
custom_field_names = [f['name'] for f in self.custom_fields]
@@ -114,9 +121,11 @@ def collect(self, cert_id, cert_format):
114121
115122
This method will raise a PendingError exception if the certificate is still in a pending state.
116123
117-
:param int cert_id: The certificate ID
118-
:param str cert_format: The format in which to retreive the certificate. Allowed values: *self.valid_formats*
119-
:return str: the string representing the certificate in the requested format
124+
Args:
125+
cert_id: The certificate ID
126+
cert_format: The format in which to retreive the certificate. Allowed values: *self.valid_formats*
127+
Returns:
128+
The string representing the certificate in the requested format
120129
"""
121130
if cert_format not in self.valid_formats:
122131
raise ValueError(f"Invalid cert format {cert_format} provided")
@@ -135,16 +144,20 @@ def collect(self, cert_id, cert_format):
135144
def enroll(self, **kwargs):
136145
"""Enroll a certificate request with Sectigo to generate a certificate.
137146
138-
:param string cert_type_name: The full cert type name
139-
Note: the name must match names returned from the get_types() method
140-
:param string csr: The Certificate Signing Request (CSR)
141-
:param int term: The length, in days, for the certificate to be issued
142-
:param int org_id: The ID of the organization in which to enroll the certificate
143-
:param list subject_alt_names: A list of Subject Alternative Names
144-
:param list external_requester: One or more e-mail addresses
145-
:param list custom_fields: zero or more objects representing custom fields and their values
146-
Note: each object must have a 'name' key and a 'value' key
147-
:return dict: The certificate_id and the normal status messages for errors
147+
Args:
148+
kwargs: A dictionary of arguments to pass to the API.
149+
Required fields are:
150+
cert_type_name: The full cert type name
151+
Note: the name must match names returned from the get_types() method
152+
csr: The Certificate Signing Request (CSR)
153+
term: The length, in days, for the certificate to be issued
154+
org_id: The ID of the organization in which to enroll the certificate
155+
subject_alt_names: A list of Subject Alternative Names
156+
external_requester: One or more e-mail addresses
157+
custom_fields: zero or more objects representing custom fields and their values
158+
Note: each object must have a 'name' key and a 'value' key
159+
Returns:
160+
The certificate_id and the normal status messages for errors
148161
"""
149162
# Retrieve all the arguments
150163
cert_type_name = kwargs.get("cert_type_name")
@@ -191,13 +204,17 @@ def enroll(self, **kwargs):
191204
def replace(self, **kwargs):
192205
"""Replace a pre-existing certificate.
193206
194-
:param int cert_id: The certificate ID
195-
:param string csr: The Certificate Signing Request (CSR)
196-
:param string common_name: Certificate common name.
197-
:param str reason: Reason for replacement (up to 512 characters), can be blank: "", but must exist.
198-
:param list subject_alt_names: A list of Subject Alternative Names.
199-
:return: The result of the operation, "Successful" on success
200-
:rtype: dict
207+
Args:
208+
kwargs: A dictionary of arguments to pass to the API.
209+
Required fields are:
210+
cert_id: The certificate ID
211+
csr: The Certificate Signing Request (CSR)
212+
common_name: Certificate common name.
213+
reason: Reason for replacement (up to 512 characters), can be blank: "", but must exist.
214+
subject_alt_names: A list of Subject Alternative Names.
215+
216+
Returns:
217+
An empty dictionary on success
201218
"""
202219
# Retrieve all the arguments
203220
cert_id = kwargs.get("cert_id")
@@ -222,16 +239,19 @@ def replace(self, **kwargs):
222239
def revoke(self, cert_id, reason=""):
223240
"""Revoke the certificate specified by the certificate ID.
224241
225-
:param int cert_id: The certificate ID
226-
:param str reason: The Reason for revocation.
227-
Reason can be up to 512 characters and cannot be blank (i.e. empty string)
228-
:return dict: The revocation result. "Successful" on success
242+
Args:
243+
cert_id: The certificate ID
244+
reason: The Reason for revocation.
245+
Reason can be up to 512 characters and cannot be blank (i.e. empty string)
246+
247+
Returns:
248+
An empty dictionary on success
229249
"""
230250
url = self._url(f"/revoke/{cert_id}")
231251

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

236256
data = {"reason": reason}
237257

cert_manager/_endpoint.py

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ class Endpoint:
1111
def __init__(self, client, endpoint, api_version="v1"):
1212
"""Initialize the class.
1313
14-
:param object client: An instantiated cert_manager.Client object
15-
:param string endpoint: The API endpoint you are accessing (for example: "/ssl")
16-
:param string api_version: The API version to use; the default is "v1"
14+
Args:
15+
client: An instantiated cert_manager.Client object
16+
endpoint: The API endpoint you are accessing (for example: "/ssl")
17+
api_version: The API version to use; the default is "v1"
1718
"""
1819
self._client = client
1920
self._api_version = api_version
@@ -35,14 +36,15 @@ def api_url(self):
3536
def create_api_url(base_url, service, version):
3637
"""Build the entire Certificate Manager API URL for the service and version.
3738
38-
:param str base_url: The base URL you have i.e. for https://hard.cert-manager.com/api/ssl/v1/ the base URL
39-
would be https://hard.cert-manager.com/api
40-
:param str service: The API service to use i.e. for https://hard.cert-manager.com/api/ssl/v1/ the service would
41-
be /ssl
42-
:param str version: The API version to use i.e. for https://hard.cert-manager.com/api/ssl/v1/ the version would
43-
be /v1
44-
:return: The full URL
45-
:rtype: str
39+
Args:
40+
base_url: The base URL you have
41+
i.e. for https://hard.cert-manager.com/api/ssl/v1/ the base URL would be https://hard.cert-manager.com/api
42+
service: The API service to use
43+
i.e. for https://hard.cert-manager.com/api/ssl/v1/ the service would be /ssl
44+
version: The API version to use
45+
i.e. for https://hard.cert-manager.com/api/ssl/v1/ the version would be /v1
46+
Returns:
47+
The full URL
4648
"""
4749
url = base_url.rstrip("/")
4850
url += "/" + service.strip("/")
@@ -54,9 +56,12 @@ def create_api_url(base_url, service, version):
5456
def _url(self, *args):
5557
"""Build the endpoint URL based on the API URL inside this object.
5658
57-
:param str suffix: The suffix of the URL you wish to create i.e. for
58-
https://hard.cert-manager.com/api/ssl/v1/types the suffix would be /types
59-
:return str: The full URL
59+
Args:
60+
args: A list of suffixes of the URL you wish to create
61+
i.e. for https://hard.cert-manager.com/api/ssl/v1/types the suffix would be /types
62+
63+
Returns:
64+
The full URL
6065
"""
6166
url = self._api_url.rstrip("/")
6267
for suffix in args:

cert_manager/_helpers.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ def traffic_log(traffic_logger=None):
1616
1717
Note: The "DEBUG" level should *never* be used in production.
1818
19-
:param obj traffic_logger: a logging.Logger to use for logging messages.
19+
Args:
20+
traffic_logger (logging.Logger): a logging.Logger to use for logging messages.
2021
"""
2122
def decorator(func):
2223
"""Wrap the actual decorator so a reference to the function can be returned."""
@@ -81,7 +82,9 @@ def version_hack(service, version="v1"):
8182
temporarily change the version to something other than what the object was initialized with so that the internal
8283
*self.api_url* will be correct.
8384
84-
:param version: API version string to use. If None, 'v1'
85+
Args:
86+
service: The API service to use.
87+
version: API version string to use. If None, 'v1'
8588
"""
8689
def decorator(func):
8790
"""Wrap the actual decorator so a reference to the function can be returned."""
@@ -125,10 +128,12 @@ def decorator(*args, **kwargs):
125128
The `size` and `position` parameters passed through `kwargs` to this function will be used
126129
by the pagination wrapper to page through results.
127130
128-
:param list args: Positional parameters to pass to the wrapped function
129-
:param dict kwargs: A dictionary with any parameters to add to the request URL
131+
Args:
132+
args: Positional parameters to pass to the wrapped function
133+
kwargs: A dictionary with any parameters to add to the request URL
130134
131-
:return obj: Yield results from the wrapped function's response for each request
135+
Returns:
136+
Yield (generator) results from the wrapped function's response for each request
132137
"""
133138
size = kwargs.pop("size", 200) # max seems to be 200 by default
134139
position = kwargs.pop("position", 0) # 0-..

0 commit comments

Comments
 (0)