lemur/lemur/certificates/views.py

1540 lines
53 KiB
Python
Raw Normal View History

2015-06-22 22:47:27 +02:00
"""
.. module: lemur.certificates.views
:platform: Unix
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
2015-06-22 22:47:27 +02:00
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
2015-11-30 18:47:36 +01:00
import base64
2015-08-04 06:07:28 +02:00
from builtins import str
from flask import Blueprint, make_response, jsonify, g, current_app
from flask_restful import reqparse, Api, inputs
from lemur.common.schema import validate_schema
from lemur.common.utils import paginated_parser
from lemur.auth.service import AuthenticatedResource
2016-06-23 22:29:59 +02:00
from lemur.auth.permissions import AuthorityPermission, CertificatePermission
2015-06-22 22:47:27 +02:00
from lemur.certificates import service
Async Certificate Issuing using Pending Certificates (#1037) * Add PendingCertificate model This change creates a DB table called pending_certificates and associated mapping relationship tables from pending certificate to roles, rotation policy, destination, sources, etc. The table is generated on initialization of Lemur. A pending certificate holds most of the information of a Certificate, while it has not be issued so that it can later backfill the information when the CA has issued the certificate. Change-Id: I277c16b776a71fe5edaf0fa0e76bbedc88924db0 Tickets: PBL-36499 * Create a PendingCertificate if cert is empty IssuePlugins should return empty cert bodies if the request failed to complete immediately (such as Digicert). This way, we can immediately return the certificate, or if not just place into PendingCertificates for later processing. + Fix relation from Certificate to Pending Certificate, as view only. There is no real need for anything more than that since Pending cert only needs to know the cert to replace when it is issued later. + Made PendingCertificate private key be empty: UI does not allow private key on 'Create' but only on 'Import'. For Instart, we require the private key but upstream does not necessarily need it. Thus, if someone at Instart wants to create a CSR / key combo, they should manually issue the cert themselves and import later. Otherwise you should let Lemur generate that. This keeps the workflow transparent for upstream Lemur users. Change-Id: Ib74722a5ed5792d4b10ca702659422739c95ae26 Tickets: PBL-36343 * Fix empty private_key when create Pending Cert On creation of a certificate with a CSR, there is no option for private key. In this case, we actually have a dictionary with private_key as key, but the value is None. This fixes the strip() called on NoneType. Change-Id: I7b265564d8095bfc83d9d4cd14ae13fea3c03199 Tickets: PBL-36499 * Source sync finds and uses pending certificate When a source syncs certificates, it will check for a pending certificate. If that is found via external_id (given by digicert as order_id) then it will use the found Pending Certificate's fields to create a new certificate. Then the pending certificate is deleted. Tickets: PBL-36343 Change-Id: I4f7959da29275ebc47a3996741f7e98d3e2d29d9 * Add Lemur static files and views for pending certs This adds the basic static files to view pending certificates in a table. Tickets: PBL-36343 Change-Id: Ia4362e6664ec730d05d280c5ef5c815a6feda0d9 * Add CLI and plugin based pending fetch This change uses the adds a new function to issuer plugins to fetch certificates like source, but for one order. This way, we can control which pending certificates to try and populate instead of getting all certificates from source. Tickets: PBL-36343 Change-Id: Ifc1747ccdc2cba09a81f298b31ddddebfee1b1d6 * Revert source using Pending Certificate Tickets: PBL-36343 Change-Id: I05121bc951e0530d804070afdb9c9e09baa0bc51 * Fix PendingCertificate init getting authority id Should get authority id from authority.id instead of the authority_id key in kwargs. Change-Id: Ie56df1a5fb0ab2729e91050f3ad1a831853e0623 Tickets: n/a * Add fixtures and basic test for PendingCertificate Change-Id: I4cca34105544d40dac1cc50a87bba93d8af9ab34 Tickets: PBL-36343 * Add User to create_certificate parameters create_certificate now takes a User, which will be used to populate the 'creator' field in certificates.service.upload(). This allows the UI populate with the current user if the owner does not exist in Lemur. + Fix chain being replaced with version from pending certificate, which may be empty (depends on plugin implementation). Change-Id: I516027b36bc643c4978b9c4890060569e03f3049 Tickets: n/a * Fix permalink and filters to pending certs Fixes the permalink button to get a single pending certificate Add argument filter parsing for the pending certificate API Fix comment on API usage Added get_by_name for pending_certificate (currently unused, but useful for CLI, instead of using IDs) Change-Id: Iaa48909c45606bec65dfb193c13d6bd0e816f6db Tickets: PBL-36910 * Update displayed fields for Pending Certificates There are a number of unused / unpopulated fields from Certificate UI that does apply to Pending Certificates. Those ones were removed, and added other useful fields: Owner, number of attempts to fetch and date created Change-Id: I3010a715f0357ba149cf539a19fdb5974c5ce08b Tickets: PBL-36910 * Add common name (cn) to Pending Certificate model Fixes the UI missing the CN for Pending Certificate, as it was originally being parsed from the generated certificate. In the case of pending certificate, the CN from the user generates the request, which means a pending cert can trust the original user putting in the CN instead of having to parse the not-yet-generated certificate. There is no real possibility to return a certificate from a pending certificate where the CN has changed since it was initially ordered. Change-Id: I88a4fa28116d5d8d293e58970d9777ce73fbb2ab Tickets: PBL-36910 * Fix missing imports for service filter + Removed duplicate get_by_name function from old merge Change-Id: I04ae6852533aa42988433338de74390e2868d69b Tickets: PBL-36910 * Add private key viewing to Pending Certificates Add private key API for Pending Certificates, with the same authorization as Certificates (only owner, creator or owner-roles can view private key). Change-Id: Ie5175154a10fe0007cc0e9f35b80c0a01ed48d5b Tickets: PBL-36910 * Add edit capability to pending certificates Like editing certificates, we should be able to modify some parts of a pending certificate so the resulting certificate has the right references, owner, etc. + Added API to update pending certificate + Fix UI to use pending certificate scope instead of reusing Certificate + Change pending_certificate.replaces to non-passive association, so that updates do affect it (similar to roles/notifications/etc) Tickets: PBL-36910 Change-Id: Ibbcb166a33f0337e1b14f426472261222f790ce6 * Add common_name parsing instead using kwargs To fix tests where common name may not be passed in, use the CSR generated to find the official common name. Change-Id: I09f9258fa92c2762d095798676ce210c5d7a3da4 Tickets: PBL-36343 * Add Cancel to pending certificates and plugins This allows pending certificates to be cancelled, which will be handled by the issuer plugin. Change-Id: Ibd6b5627c3977e33aca7860690cfb7f677236ca9 Tickets: PBL-36910 * Add API for Cancelling Pending Certificate Added the DELETE handler for pending_certificates, which will cancel and delete the pending certificate from the pending certs table on successful cancellation via Issuer Plugin. + Add UT for testing cancel API Change-Id: I11b1d87872e4284f6e4f9c366a15da4ddba38bc4 Tickets: PBL-36910 * Remove Export from Pending Certificates Pending Certificates doesn't need an export since it should just be fetched by Lemur via plugins, and the CSR is viewable via the UI. Change-Id: I9a3e65ea11ac5a85316f6428e7f526c3c09178ae Tickets: PBL-36910 * Add cancel button functionality to UI This adds the Cancel option to the dropdown of pending certificates. + Adds modal window for Note (may not be required for all issuers, just Digicert) + Add schema for cancel input + Fix Digitcert plugin for non-existant orders When an order is actually issued, then attempting to cancel will return a 403 from Digicert. This is a case where it should only be done once we know the pending cert has been sitting for too long. Change-Id: I256c81ecd142dd51dcf8e38802d2c202829887b0 Tickets: PBL-36910 * Fix test_pending_cancel UT This change creates and injects a pending cert, which will then be used for the ID so it can be canceled by the unit test. Change-Id: I686e7e0fafd68cdaeb26438fb8504d79de77c346 Tickets: PBL-36343 * Fix test_digicert on non-existent order cancelling a non-existent order is fine since we're cancelling it Change-Id: I70c0e82ba2f4b8723a7f65b113c19e6eeff7e68c Tickets: PBL-36343 * Add migrations for PendingCertificates Added revision for Pending Certificates table and foreign key mapping tables. Change-Id: Ife8202cef1e6b99db377851264639ba540b749db Tickets: n/a * Fix relationship copy from Pending to Certificate When a Pending Certificate is changed to a full Certificate, the relationship fields are not copied via vars() function, as it's not a column but mapped via association table. This adds an explicit copy for these relations. Which will properly copy them to the new Certificate, and thus also update destinations. Change-Id: I322032ce4a9e3e67773f7cf39ee4971054c92685 Tickets: PBL-36343 * Fix renaming of certificates and unit tests The rename flag was not used to rename certificates on creation as expected. Fixed unit test, instead of expunging the session, just copy the pending_certificate so we don't have a weird reference to the object that can't be copied via vars() function. Change-Id: I962943272ed92386ab6eab2af4ed6d074d4cffa0 Tickets: PBL-36343 * Updated developer docs for async certs Added blurb for implementing new issuer functions. Change-Id: I1caed6e914bcd73214eae2d241e4784e1b8a0c4c Tickets: n/a
2018-02-22 17:13:16 +01:00
from lemur.certificates.models import Certificate
from lemur.extensions import sentry
from lemur.certificates.schemas import (
certificate_input_schema,
certificate_output_schema,
certificate_upload_input_schema,
certificates_output_schema,
certificate_export_input_schema,
2019-05-16 16:57:02 +02:00
certificate_edit_input_schema,
certificates_list_output_schema_factory,
2020-12-01 05:06:37 +01:00
certificate_revoke_schema,
)
2015-06-22 22:47:27 +02:00
from lemur.roles import service as role_service
from lemur.logs import service as log_service
2015-08-29 22:07:30 +02:00
2019-05-16 16:57:02 +02:00
mod = Blueprint("certificates", __name__)
2015-06-22 22:47:27 +02:00
api = Api(mod)
class CertificatesListValid(AuthenticatedResource):
""" Defines the 'certificates/valid' endpoint """
def __init__(self):
self.reqparse = reqparse.RequestParser()
super(CertificatesListValid, self).__init__()
@validate_schema(None, certificates_output_schema)
def get(self):
"""
.. http:get:: /certificates/valid/<query>
2021-01-06 20:28:18 +01:00
The current list of not-expired certificates for a given common name, and owner. The API offers
optional pagination. One can send page number(>=1) and desired count per page. The returned data
contains total number of certificates which can help in determining the last page. Pagination
will not be offered if page or count info is not sent or if it is zero.
**Example request**:
.. sourcecode:: http
2021-02-17 23:07:06 +01:00
2021-02-19 02:23:02 +01:00
GET /certificates/valid?filter=cn;*.test.example.net&owner=joe@example.com&page=1&count=20 HTTP/1.1
Host: example.com
Accept: application/json, text/javascript
2021-01-06 20:28:18 +01:00
**Example response (with single cert to be concise)**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: text/javascript
{
"items": [{
"status": null,
"cn": "*.test.example.net",
"chain": "",
"csr": "-----BEGIN CERTIFICATE REQUEST-----"
"authority": {
"active": true,
"owner": "secure@example.com",
"id": 1,
"description": "verisign test authority",
"name": "verisign"
},
"owner": "joe@example.com",
"serial": "82311058732025924142789179368889309156",
"id": 2288,
"issuer": "SymantecCorporation",
"dateCreated": "2016-06-03T06:09:42.133769+00:00",
"notBefore": "2016-06-03T00:00:00+00:00",
"notAfter": "2018-01-12T23:59:59+00:00",
"destinations": [],
"bits": 2048,
"body": "-----BEGIN CERTIFICATE-----...",
"description": null,
"deleted": null,
"notifications": [{
"id": 1
}],
"signingAlgorithm": "sha256",
"user": {
"username": "jane",
"active": true,
"email": "jane@example.com",
"id": 2
},
"active": true,
"domains": [{
"sensitive": false,
"id": 1090,
"name": "*.test.example.net"
}],
"replaces": [],
"replaced": [],
"name": "WILDCARD.test.example.net-SymantecCorporation-20160603-20180112",
"roles": [{
"id": 464,
"description": "This is a google group based role created by Lemur",
"name": "joe@example.com"
}],
"san": null
}],
"total": 1
}
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
:statuscode 403: unauthenticated
"""
2021-01-06 20:28:18 +01:00
# using non-paginated parser to ensure backward compatibility
self.reqparse.add_argument("filter", type=str, location="args")
self.reqparse.add_argument("owner", type=str, location="args")
self.reqparse.add_argument("count", type=int, location="args")
self.reqparse.add_argument("page", type=int, location="args")
args = self.reqparse.parse_args()
2019-05-16 16:57:02 +02:00
args["user"] = g.user
2021-01-06 20:28:18 +01:00
common_name = args.pop("filter").split(";")[1]
return service.query_common_name(common_name, args)
class CertificatesNameQuery(AuthenticatedResource):
""" Defines the 'certificates/name' endpoint """
def __init__(self):
self.reqparse = reqparse.RequestParser()
super(CertificatesNameQuery, self).__init__()
@validate_schema(None, certificates_output_schema)
def get(self, certificate_name):
"""
.. http:get:: /certificates/name/<query>
The current list of certificates
**Example request**:
.. sourcecode:: http
GET /certificates/name/WILDCARD.test.example.net-SymantecCorporation-20160603-20180112 HTTP/1.1
Host: example.com
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: text/javascript
{
"items": [{
"status": null,
"cn": "*.test.example.net",
"chain": "",
"csr": "-----BEGIN CERTIFICATE REQUEST-----"
"authority": {
"active": true,
"owner": "secure@example.com",
"id": 1,
"description": "verisign test authority",
"name": "verisign"
},
"owner": "joe@example.com",
"serial": "82311058732025924142789179368889309156",
"id": 2288,
"issuer": "SymantecCorporation",
"dateCreated": "2016-06-03T06:09:42.133769+00:00",
"notBefore": "2016-06-03T00:00:00+00:00",
"notAfter": "2018-01-12T23:59:59+00:00",
"destinations": [],
"bits": 2048,
"body": "-----BEGIN CERTIFICATE-----...",
"description": null,
"deleted": null,
"notifications": [{
"id": 1
}],
"signingAlgorithm": "sha256",
"user": {
"username": "jane",
"active": true,
"email": "jane@example.com",
"id": 2
},
"active": true,
"domains": [{
"sensitive": false,
"id": 1090,
"name": "*.test.example.net"
}],
"replaces": [],
"replaced": [],
"name": "WILDCARD.test.example.net-SymantecCorporation-20160603-20180112",
"roles": [{
"id": 464,
"description": "This is a google group based role created by Lemur",
"name": "joe@example.com"
}],
"san": null
}],
"total": 1
}
:query sortBy: field to sort on
:query sortDir: asc or desc
:query page: int. default is 1
:query filter: key value pair format is k;v
:query count: count number. default is 10
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
:statuscode 403: unauthenticated
"""
parser = paginated_parser.copy()
2019-05-16 16:57:02 +02:00
parser.add_argument("timeRange", type=int, dest="time_range", location="args")
parser.add_argument("owner", type=inputs.boolean, location="args")
parser.add_argument("id", type=str, location="args")
parser.add_argument("active", type=inputs.boolean, location="args")
parser.add_argument(
"destinationId", type=int, dest="destination_id", location="args"
)
parser.add_argument("creator", type=str, location="args")
parser.add_argument("show", type=str, location="args")
args = parser.parse_args()
2019-05-16 16:57:02 +02:00
args["user"] = g.user
return service.query_name(certificate_name, args)
2015-06-22 22:47:27 +02:00
class CertificatesList(AuthenticatedResource):
""" Defines the 'certificates' endpoint """
2015-11-30 22:51:27 +01:00
2015-06-22 22:47:27 +02:00
def __init__(self):
self.reqparse = reqparse.RequestParser()
super(CertificatesList, self).__init__()
@validate_schema(None, certificates_list_output_schema_factory)
2015-06-22 22:47:27 +02:00
def get(self):
"""
.. http:get:: /certificates
The current list of certificates
**Example request**:
.. sourcecode:: http
GET /certificates HTTP/1.1
Host: example.com
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: text/javascript
{
"items": [{
"status": null,
"cn": "*.test.example.net",
"chain": "",
"csr": "-----BEGIN CERTIFICATE REQUEST-----"
"authority": {
"active": true,
"owner": "secure@example.com",
"id": 1,
"description": "verisign test authority",
"name": "verisign"
},
"owner": "joe@example.com",
"serial": "82311058732025924142789179368889309156",
"id": 2288,
"issuer": "SymantecCorporation",
2017-08-18 14:13:27 +02:00
"dateCreated": "2016-06-03T06:09:42.133769+00:00",
"notBefore": "2016-06-03T00:00:00+00:00",
"notAfter": "2018-01-12T23:59:59+00:00",
"destinations": [],
"bits": 2048,
"body": "-----BEGIN CERTIFICATE-----...",
"description": null,
"deleted": null,
"notifications": [{
"id": 1
}],
"signingAlgorithm": "sha256",
"user": {
"username": "jane",
"active": true,
"email": "jane@example.com",
"id": 2
},
"active": true,
"domains": [{
"sensitive": false,
"id": 1090,
"name": "*.test.example.net"
}],
"replaces": [],
2016-12-20 23:26:39 +01:00
"replaced": [],
"name": "WILDCARD.test.example.net-SymantecCorporation-20160603-20180112",
"roles": [{
"id": 464,
"description": "This is a google group based role created by Lemur",
"name": "joe@example.com"
}],
"san": null
}],
2015-06-22 22:47:27 +02:00
"total": 1
}
:query sortBy: field to sort on
:query sortDir: asc or desc
2015-06-22 22:47:27 +02:00
:query page: int. default is 1
2016-01-29 20:47:16 +01:00
:query filter: key value pair format is k;v
:query count: count number. default is 10
2015-06-22 22:47:27 +02:00
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
:statuscode 403: unauthenticated
2015-06-22 22:47:27 +02:00
"""
parser = paginated_parser.copy()
2019-05-16 16:57:02 +02:00
parser.add_argument("timeRange", type=int, dest="time_range", location="args")
parser.add_argument("owner", type=inputs.boolean, location="args")
parser.add_argument("id", type=str, location="args")
parser.add_argument("active", type=inputs.boolean, location="args")
parser.add_argument(
"destinationId", type=int, dest="destination_id", location="args"
)
parser.add_argument("creator", type=str, location="args")
parser.add_argument("show", type=str, location="args")
parser.add_argument("showExpired", type=int, location="args")
2015-06-22 22:47:27 +02:00
args = parser.parse_args()
2019-05-16 16:57:02 +02:00
args["user"] = g.user
2015-06-22 22:47:27 +02:00
return service.render(args)
@validate_schema(certificate_input_schema, certificate_output_schema)
def post(self, data=None):
2015-06-22 22:47:27 +02:00
"""
.. http:post:: /certificates
Creates a new certificate
**Example request**:
.. sourcecode:: http
POST /certificates HTTP/1.1
Host: example.com
Accept: application/json, text/javascript
Content-Type: application/json;charset=UTF-8
2015-06-22 22:47:27 +02:00
{
"owner": "secure@example.net",
"commonName": "test.example.net",
"country": "US",
"extensions": {
"subAltNames": {
"names": [
{
"nameType": "DNSName",
"value": "*.test.example.net"
},
{
"nameType": "DNSName",
"value": "www.test.example.net"
}
]
}
},
2016-11-11 22:35:24 +01:00
"replacements": [{
"id": 1
}],
"notify": true,
"validityEnd": "2026-01-01T08:00:00.000Z",
"authority": {
"name": "verisign"
},
"organization": "Netflix, Inc.",
"location": "Los Gatos",
"state": "California",
"validityStart": "2016-11-11T04:19:48.000Z",
"organizationalUnit": "Operations"
}
2015-06-22 22:47:27 +02:00
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: text/javascript
{
"status": null,
"cn": "*.test.example.net",
"chain": "",
"authority": {
"active": true,
"owner": "secure@example.com",
"id": 1,
"description": "verisign test authority",
"name": "verisign"
},
"owner": "joe@example.com",
"serial": "82311058732025924142789179368889309156",
"id": 2288,
"issuer": "SymantecCorporation",
2017-08-18 14:13:27 +02:00
"dateCreated": "2016-06-03T06:09:42.133769+00:00",
"notBefore": "2016-06-03T00:00:00+00:00",
"notAfter": "2018-01-12T23:59:59+00:00",
"destinations": [],
2015-06-22 22:47:27 +02:00
"bits": 2048,
"body": "-----BEGIN CERTIFICATE-----...",
"description": null,
"deleted": null,
"notifications": [{
"id": 1
}],
"signingAlgorithm": "sha256",
"user": {
"username": "jane",
"active": true,
"email": "jane@example.com",
"id": 2
},
"active": true,
"domains": [{
"sensitive": false,
"id": 1090,
"name": "*.test.example.net"
}],
"replaces": [{
"id": 1
}],
"rotation": true,
"rotationPolicy": {"name": "default"},
"name": "WILDCARD.test.example.net-SymantecCorporation-20160603-20180112",
"roles": [{
"id": 464,
"description": "This is a google group based role created by Lemur",
"name": "joe@example.com"
}],
"san": null
2015-06-22 22:47:27 +02:00
}
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
:statuscode 403: unauthenticated
2015-06-22 22:47:27 +02:00
"""
2019-05-16 16:57:02 +02:00
role = role_service.get_by_name(data["authority"].owner)
2015-06-22 22:47:27 +02:00
# all the authority role members should be allowed
2019-05-16 16:57:02 +02:00
roles = [x.name for x in data["authority"].roles]
2015-06-22 22:47:27 +02:00
# allow "owner" roles by team DL
roles.append(role)
2019-05-16 16:57:02 +02:00
authority_permission = AuthorityPermission(data["authority"].id, roles)
2015-06-22 22:47:27 +02:00
if authority_permission.can():
2019-05-16 16:57:02 +02:00
data["creator"] = g.user
cert = service.create(**data)
Async Certificate Issuing using Pending Certificates (#1037) * Add PendingCertificate model This change creates a DB table called pending_certificates and associated mapping relationship tables from pending certificate to roles, rotation policy, destination, sources, etc. The table is generated on initialization of Lemur. A pending certificate holds most of the information of a Certificate, while it has not be issued so that it can later backfill the information when the CA has issued the certificate. Change-Id: I277c16b776a71fe5edaf0fa0e76bbedc88924db0 Tickets: PBL-36499 * Create a PendingCertificate if cert is empty IssuePlugins should return empty cert bodies if the request failed to complete immediately (such as Digicert). This way, we can immediately return the certificate, or if not just place into PendingCertificates for later processing. + Fix relation from Certificate to Pending Certificate, as view only. There is no real need for anything more than that since Pending cert only needs to know the cert to replace when it is issued later. + Made PendingCertificate private key be empty: UI does not allow private key on 'Create' but only on 'Import'. For Instart, we require the private key but upstream does not necessarily need it. Thus, if someone at Instart wants to create a CSR / key combo, they should manually issue the cert themselves and import later. Otherwise you should let Lemur generate that. This keeps the workflow transparent for upstream Lemur users. Change-Id: Ib74722a5ed5792d4b10ca702659422739c95ae26 Tickets: PBL-36343 * Fix empty private_key when create Pending Cert On creation of a certificate with a CSR, there is no option for private key. In this case, we actually have a dictionary with private_key as key, but the value is None. This fixes the strip() called on NoneType. Change-Id: I7b265564d8095bfc83d9d4cd14ae13fea3c03199 Tickets: PBL-36499 * Source sync finds and uses pending certificate When a source syncs certificates, it will check for a pending certificate. If that is found via external_id (given by digicert as order_id) then it will use the found Pending Certificate's fields to create a new certificate. Then the pending certificate is deleted. Tickets: PBL-36343 Change-Id: I4f7959da29275ebc47a3996741f7e98d3e2d29d9 * Add Lemur static files and views for pending certs This adds the basic static files to view pending certificates in a table. Tickets: PBL-36343 Change-Id: Ia4362e6664ec730d05d280c5ef5c815a6feda0d9 * Add CLI and plugin based pending fetch This change uses the adds a new function to issuer plugins to fetch certificates like source, but for one order. This way, we can control which pending certificates to try and populate instead of getting all certificates from source. Tickets: PBL-36343 Change-Id: Ifc1747ccdc2cba09a81f298b31ddddebfee1b1d6 * Revert source using Pending Certificate Tickets: PBL-36343 Change-Id: I05121bc951e0530d804070afdb9c9e09baa0bc51 * Fix PendingCertificate init getting authority id Should get authority id from authority.id instead of the authority_id key in kwargs. Change-Id: Ie56df1a5fb0ab2729e91050f3ad1a831853e0623 Tickets: n/a * Add fixtures and basic test for PendingCertificate Change-Id: I4cca34105544d40dac1cc50a87bba93d8af9ab34 Tickets: PBL-36343 * Add User to create_certificate parameters create_certificate now takes a User, which will be used to populate the 'creator' field in certificates.service.upload(). This allows the UI populate with the current user if the owner does not exist in Lemur. + Fix chain being replaced with version from pending certificate, which may be empty (depends on plugin implementation). Change-Id: I516027b36bc643c4978b9c4890060569e03f3049 Tickets: n/a * Fix permalink and filters to pending certs Fixes the permalink button to get a single pending certificate Add argument filter parsing for the pending certificate API Fix comment on API usage Added get_by_name for pending_certificate (currently unused, but useful for CLI, instead of using IDs) Change-Id: Iaa48909c45606bec65dfb193c13d6bd0e816f6db Tickets: PBL-36910 * Update displayed fields for Pending Certificates There are a number of unused / unpopulated fields from Certificate UI that does apply to Pending Certificates. Those ones were removed, and added other useful fields: Owner, number of attempts to fetch and date created Change-Id: I3010a715f0357ba149cf539a19fdb5974c5ce08b Tickets: PBL-36910 * Add common name (cn) to Pending Certificate model Fixes the UI missing the CN for Pending Certificate, as it was originally being parsed from the generated certificate. In the case of pending certificate, the CN from the user generates the request, which means a pending cert can trust the original user putting in the CN instead of having to parse the not-yet-generated certificate. There is no real possibility to return a certificate from a pending certificate where the CN has changed since it was initially ordered. Change-Id: I88a4fa28116d5d8d293e58970d9777ce73fbb2ab Tickets: PBL-36910 * Fix missing imports for service filter + Removed duplicate get_by_name function from old merge Change-Id: I04ae6852533aa42988433338de74390e2868d69b Tickets: PBL-36910 * Add private key viewing to Pending Certificates Add private key API for Pending Certificates, with the same authorization as Certificates (only owner, creator or owner-roles can view private key). Change-Id: Ie5175154a10fe0007cc0e9f35b80c0a01ed48d5b Tickets: PBL-36910 * Add edit capability to pending certificates Like editing certificates, we should be able to modify some parts of a pending certificate so the resulting certificate has the right references, owner, etc. + Added API to update pending certificate + Fix UI to use pending certificate scope instead of reusing Certificate + Change pending_certificate.replaces to non-passive association, so that updates do affect it (similar to roles/notifications/etc) Tickets: PBL-36910 Change-Id: Ibbcb166a33f0337e1b14f426472261222f790ce6 * Add common_name parsing instead using kwargs To fix tests where common name may not be passed in, use the CSR generated to find the official common name. Change-Id: I09f9258fa92c2762d095798676ce210c5d7a3da4 Tickets: PBL-36343 * Add Cancel to pending certificates and plugins This allows pending certificates to be cancelled, which will be handled by the issuer plugin. Change-Id: Ibd6b5627c3977e33aca7860690cfb7f677236ca9 Tickets: PBL-36910 * Add API for Cancelling Pending Certificate Added the DELETE handler for pending_certificates, which will cancel and delete the pending certificate from the pending certs table on successful cancellation via Issuer Plugin. + Add UT for testing cancel API Change-Id: I11b1d87872e4284f6e4f9c366a15da4ddba38bc4 Tickets: PBL-36910 * Remove Export from Pending Certificates Pending Certificates doesn't need an export since it should just be fetched by Lemur via plugins, and the CSR is viewable via the UI. Change-Id: I9a3e65ea11ac5a85316f6428e7f526c3c09178ae Tickets: PBL-36910 * Add cancel button functionality to UI This adds the Cancel option to the dropdown of pending certificates. + Adds modal window for Note (may not be required for all issuers, just Digicert) + Add schema for cancel input + Fix Digitcert plugin for non-existant orders When an order is actually issued, then attempting to cancel will return a 403 from Digicert. This is a case where it should only be done once we know the pending cert has been sitting for too long. Change-Id: I256c81ecd142dd51dcf8e38802d2c202829887b0 Tickets: PBL-36910 * Fix test_pending_cancel UT This change creates and injects a pending cert, which will then be used for the ID so it can be canceled by the unit test. Change-Id: I686e7e0fafd68cdaeb26438fb8504d79de77c346 Tickets: PBL-36343 * Fix test_digicert on non-existent order cancelling a non-existent order is fine since we're cancelling it Change-Id: I70c0e82ba2f4b8723a7f65b113c19e6eeff7e68c Tickets: PBL-36343 * Add migrations for PendingCertificates Added revision for Pending Certificates table and foreign key mapping tables. Change-Id: Ife8202cef1e6b99db377851264639ba540b749db Tickets: n/a * Fix relationship copy from Pending to Certificate When a Pending Certificate is changed to a full Certificate, the relationship fields are not copied via vars() function, as it's not a column but mapped via association table. This adds an explicit copy for these relations. Which will properly copy them to the new Certificate, and thus also update destinations. Change-Id: I322032ce4a9e3e67773f7cf39ee4971054c92685 Tickets: PBL-36343 * Fix renaming of certificates and unit tests The rename flag was not used to rename certificates on creation as expected. Fixed unit test, instead of expunging the session, just copy the pending_certificate so we don't have a weird reference to the object that can't be copied via vars() function. Change-Id: I962943272ed92386ab6eab2af4ed6d074d4cffa0 Tickets: PBL-36343 * Updated developer docs for async certs Added blurb for implementing new issuer functions. Change-Id: I1caed6e914bcd73214eae2d241e4784e1b8a0c4c Tickets: n/a
2018-02-22 17:13:16 +01:00
if isinstance(cert, Certificate):
# only log if created, not pending
2019-05-16 16:57:02 +02:00
log_service.create(g.user, "create_cert", certificate=cert)
return cert
2015-06-22 22:47:27 +02:00
2019-05-16 16:57:02 +02:00
return (
dict(
message="You are not authorized to use the authority: {0}".format(
data["authority"].name
)
),
403,
)
2015-06-22 22:47:27 +02:00
class CertificatesUpload(AuthenticatedResource):
""" Defines the 'certificates' upload endpoint """
2015-11-30 22:51:27 +01:00
2015-06-22 22:47:27 +02:00
def __init__(self):
self.reqparse = reqparse.RequestParser()
super(CertificatesUpload, self).__init__()
@validate_schema(certificate_upload_input_schema, certificate_output_schema)
def post(self, data=None):
2015-06-22 22:47:27 +02:00
"""
.. http:post:: /certificates/upload
Upload a certificate
**Example request**:
.. sourcecode:: http
POST /certificates/upload HTTP/1.1
Host: example.com
Accept: application/json, text/javascript
Content-Type: application/json;charset=UTF-8
2015-06-22 22:47:27 +02:00
{
"owner": "joe@example.com",
2018-05-08 19:58:48 +02:00
"body": "-----BEGIN CERTIFICATE-----...",
"chain": "-----BEGIN CERTIFICATE-----...",
"privateKey": "-----BEGIN RSA PRIVATE KEY-----..."
"csr": "-----BEGIN CERTIFICATE REQUEST-----..."
"destinations": [],
2015-08-29 20:48:39 +02:00
"notifications": [],
2015-11-24 23:53:22 +01:00
"replacements": [],
2018-05-08 19:58:48 +02:00
"roles": [],
"notify": true,
2015-08-29 20:48:39 +02:00
"name": "cert1"
2015-06-22 22:47:27 +02:00
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: text/javascript
{
"status": null,
"cn": "*.test.example.net",
"chain": "",
"authority": {
"active": true,
"owner": "secure@example.com",
"id": 1,
"description": "verisign test authority",
"name": "verisign"
},
"owner": "joe@example.com",
"serial": "82311058732025924142789179368889309156",
"id": 2288,
"issuer": "SymantecCorporation",
2017-08-18 14:13:27 +02:00
"dateCreated": "2016-06-03T06:09:42.133769+00:00",
"notBefore": "2016-06-03T00:00:00+00:00",
"notAfter": "2018-01-12T23:59:59+00:00",
"destinations": [],
"bits": 2048,
"body": "-----BEGIN CERTIFICATE-----...",
"description": null,
"deleted": null,
"notifications": [{
"id": 1
}],
"signingAlgorithm": "sha256",
"user": {
"username": "jane",
"active": true,
"email": "jane@example.com",
"id": 2
},
"active": true,
"domains": [{
"sensitive": false,
"id": 1090,
"name": "*.test.example.net"
}],
"replaces": [],
"rotation": true,
"rotationPolicy": {"name": "default"},
"name": "WILDCARD.test.example.net-SymantecCorporation-20160603-20180112",
"roles": [{
"id": 464,
"description": "This is a google group based role created by Lemur",
"name": "joe@example.com"
}],
"san": null
2015-06-22 22:47:27 +02:00
}
:reqheader Authorization: OAuth token to authenticate
:statuscode 403: unauthenticated
:statuscode 200: no error
2015-06-22 22:47:27 +02:00
"""
2019-05-16 16:57:02 +02:00
data["creator"] = g.user
if data.get("destinations"):
if data.get("private_key"):
return service.upload(**data)
2015-06-22 22:47:27 +02:00
else:
2019-05-16 16:57:02 +02:00
raise Exception(
"Private key must be provided in order to upload certificate to AWS"
)
return service.upload(**data)
2015-06-22 22:47:27 +02:00
class CertificatesStats(AuthenticatedResource):
""" Defines the 'certificates' stats endpoint """
2015-11-30 22:51:27 +01:00
2015-06-22 22:47:27 +02:00
def __init__(self):
self.reqparse = reqparse.RequestParser()
super(CertificatesStats, self).__init__()
def get(self):
2019-05-16 16:57:02 +02:00
self.reqparse.add_argument("metric", type=str, location="args")
self.reqparse.add_argument("range", default=32, type=int, location="args")
self.reqparse.add_argument(
"destinationId", dest="destination_id", location="args"
)
self.reqparse.add_argument("active", type=str, default="true", location="args")
2015-06-22 22:47:27 +02:00
args = self.reqparse.parse_args()
items = service.stats(**args)
return dict(items=items, total=len(items))
class CertificatePrivateKey(AuthenticatedResource):
def __init__(self):
super(CertificatePrivateKey, self).__init__()
def get(self, certificate_id):
"""
.. http:get:: /certificates/1/key
Retrieves the private key for a given certificate
**Example request**:
.. sourcecode:: http
GET /certificates/1/key HTTP/1.1
Host: example.com
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: text/javascript
{
"key": "-----BEGIN ..."
2015-06-22 22:47:27 +02:00
}
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
:statuscode 403: unauthenticated
"""
cert = service.get(certificate_id)
if not cert:
return dict(message="Cannot find specified certificate"), 404
# allow creators
if g.current_user != cert.user:
owner_role = role_service.get_by_name(cert.owner)
permission = CertificatePermission(owner_role, [x.name for x in cert.roles])
if not permission.can():
2019-05-16 16:57:02 +02:00
return dict(message="You are not authorized to view this key"), 403
2015-06-22 22:47:27 +02:00
2019-05-16 16:57:02 +02:00
log_service.create(g.current_user, "key_view", certificate=cert)
response = make_response(jsonify(key=cert.private_key), 200)
2019-05-16 16:57:02 +02:00
response.headers["cache-control"] = "private, max-age=0, no-cache, no-store"
response.headers["pragma"] = "no-cache"
return response
2015-06-22 22:47:27 +02:00
class Certificates(AuthenticatedResource):
def __init__(self):
self.reqparse = reqparse.RequestParser()
super(Certificates, self).__init__()
@validate_schema(None, certificate_output_schema)
2015-06-22 22:47:27 +02:00
def get(self, certificate_id):
"""
.. http:get:: /certificates/1
One certificate
**Example request**:
.. sourcecode:: http
GET /certificates/1 HTTP/1.1
Host: example.com
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: text/javascript
{
"status": null,
"cn": "*.test.example.net",
"chain": "",
"csr": "-----BEGIN CERTIFICATE REQUEST-----"
"authority": {
"active": true,
"owner": "secure@example.com",
"id": 1,
"description": "verisign test authority",
"name": "verisign"
},
"owner": "joe@example.com",
"serial": "82311058732025924142789179368889309156",
"id": 2288,
"issuer": "SymantecCorporation",
2017-08-18 14:13:27 +02:00
"dateCreated": "2016-06-03T06:09:42.133769+00:00",
"notBefore": "2016-06-03T00:00:00+00:00",
"notAfter": "2018-01-12T23:59:59+00:00",
"destinations": [],
2015-06-22 22:47:27 +02:00
"bits": 2048,
"body": "-----BEGIN CERTIFICATE-----...",
"description": null,
"deleted": null,
"notifications": [{
"id": 1
}],
"signingAlgorithm": "sha256",
"user": {
"username": "jane",
"active": true,
"email": "jane@example.com",
"id": 2
},
2015-06-22 22:47:27 +02:00
"active": true,
"domains": [{
"sensitive": false,
"id": 1090,
"name": "*.test.example.net"
}],
"rotation": true,
"rotationPolicy": {"name": "default"},
"replaces": [],
2016-12-20 23:26:39 +01:00
"replaced": [],
"name": "WILDCARD.test.example.net-SymantecCorporation-20160603-20180112",
"roles": [{
"id": 464,
"description": "This is a google group based role created by Lemur",
"name": "joe@example.com"
}],
"san": null
2015-06-22 22:47:27 +02:00
}
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
:statuscode 403: unauthenticated
2015-06-22 22:47:27 +02:00
"""
return service.get(certificate_id)
@validate_schema(certificate_edit_input_schema, certificate_output_schema)
def put(self, certificate_id, data=None):
2015-06-22 22:47:27 +02:00
"""
.. http:put:: /certificates/1
Update a certificate
**Example request**:
.. sourcecode:: http
PUT /certificates/1 HTTP/1.1
Host: example.com
Accept: application/json, text/javascript
Content-Type: application/json;charset=UTF-8
2015-06-22 22:47:27 +02:00
{
"owner": "jimbob@example.com",
"active": false
"notifications": [],
2015-11-24 23:53:22 +01:00
"destinations": [],
"replacements": []
2015-06-22 22:47:27 +02:00
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: text/javascript
{
"status": null,
"cn": "*.test.example.net",
"chain": "",
"authority": {
"active": true,
"owner": "secure@example.com",
"id": 1,
"description": "verisign test authority",
"name": "verisign"
},
"owner": "joe@example.com",
"serial": "82311058732025924142789179368889309156",
"id": 2288,
"issuer": "SymantecCorporation",
2017-08-18 14:13:27 +02:00
"dateCreated": "2016-06-03T06:09:42.133769+00:00",
"notBefore": "2016-06-03T00:00:00+00:00",
"notAfter": "2018-01-12T23:59:59+00:00",
"destinations": [],
2015-06-22 22:47:27 +02:00
"bits": 2048,
"body": "-----BEGIN CERTIFICATE-----...",
"description": null,
"deleted": null,
"notifications": [{
"id": 1
}]
"signingAlgorithm": "sha256",
"user": {
"username": "jane",
"active": true,
"email": "jane@example.com",
"id": 2
},
"active": true,
"domains": [{
"sensitive": false,
"id": 1090,
"name": "*.test.example.net"
}],
"replaces": [],
"name": "WILDCARD.test.example.net-SymantecCorporation-20160603-20180112",
"roles": [{
"id": 464,
"description": "This is a google group based role created by Lemur",
"name": "joe@example.com"
}],
"rotation": true,
"rotationPolicy": {"name": "default"},
"san": null
2015-06-22 22:47:27 +02:00
}
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
:statuscode 403: unauthenticated
2015-06-22 22:47:27 +02:00
"""
cert = service.get(certificate_id)
2015-08-29 20:53:46 +02:00
if not cert:
return dict(message="Cannot find specified certificate"), 404
2015-06-22 22:47:27 +02:00
# allow creators
if g.current_user != cert.user:
owner_role = role_service.get_by_name(cert.owner)
permission = CertificatePermission(owner_role, [x.name for x in cert.roles])
if not permission.can():
2019-05-16 16:57:02 +02:00
return (
dict(message="You are not authorized to update this certificate"),
403,
)
2019-05-16 16:57:02 +02:00
for destination in data["destinations"]:
if destination.plugin.requires_key:
if not cert.private_key:
2019-05-16 16:57:02 +02:00
return (
dict(
message="Unable to add destination: {0}. Certificate does not have required private key.".format(
destination.label
)
),
400,
)
# if owner is changed, remove all notifications and roles associated with old owner
if cert.owner != data["owner"]:
service.cleanup_owner_roles_notification(cert.owner, data)
error_message = ""
# if destination is removed, cleanup the certificate from AWS
for destination in cert.destinations:
if destination not in data["destinations"]:
try:
service.remove_from_destination(cert, destination)
except Exception as e:
sentry.captureException()
# Add the removed destination back
data["destinations"].append(destination)
error_message = error_message + f"Failed to remove destination: {destination.label}. {str(e)}. "
# go ahead with DB update
cert = service.update(certificate_id, **data)
2019-05-16 16:57:02 +02:00
log_service.create(g.current_user, "update_cert", certificate=cert)
if error_message:
return dict(message=f"Edit Successful except -\n\n {error_message}"), 400
return cert
2015-06-22 22:47:27 +02:00
2020-10-08 20:38:39 +02:00
@validate_schema(certificate_edit_input_schema, certificate_output_schema)
def post(self, certificate_id, data=None):
"""
.. http:post:: /certificates/1/update/notify
Update certificate notification
**Example request**:
.. sourcecode:: http
POST /certificates/1/update/notify HTTP/1.1
Host: example.com
Accept: application/json, text/javascript
Content-Type: application/json;charset=UTF-8
2020-10-08 20:38:39 +02:00
{
"notify": false
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: text/javascript
{
"status": null,
"cn": "*.test.example.net",
"chain": "",
"authority": {
"active": true,
"owner": "secure@example.com",
"id": 1,
"description": "verisign test authority",
"name": "verisign"
},
"owner": "joe@example.com",
"serial": "82311058732025924142789179368889309156",
"id": 2288,
"issuer": "SymantecCorporation",
"dateCreated": "2016-06-03T06:09:42.133769+00:00",
"notBefore": "2016-06-03T00:00:00+00:00",
"notAfter": "2018-01-12T23:59:59+00:00",
"destinations": [],
"bits": 2048,
"body": "-----BEGIN CERTIFICATE-----...",
"description": null,
"deleted": null,
"notify": false,
"notifications": [{
"id": 1
}]
"signingAlgorithm": "sha256",
"user": {
"username": "jane",
"active": true,
"email": "jane@example.com",
"id": 2
},
"active": true,
"domains": [{
"sensitive": false,
"id": 1090,
"name": "*.test.example.net"
}],
"replaces": [],
"name": "WILDCARD.test.example.net-SymantecCorporation-20160603-20180112",
"roles": [{
"id": 464,
"description": "This is a google group based role created by Lemur",
"name": "joe@example.com"
}],
"rotation": true,
"rotationPolicy": {"name": "default"},
"san": null
}
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
:statuscode 403: unauthenticated
"""
cert = service.get(certificate_id)
if not cert:
return dict(message="Cannot find specified certificate"), 404
# allow creators
if g.current_user != cert.user:
owner_role = role_service.get_by_name(cert.owner)
permission = CertificatePermission(owner_role, [x.name for x in cert.roles])
if not permission.can():
return (
dict(message="You are not authorized to update this certificate"),
403,
)
cert = service.update_notify(cert, data.get("notify"))
log_service.create(g.current_user, "update_cert", certificate=cert)
return cert
def delete(self, certificate_id, data=None):
"""
.. http:delete:: /certificates/1
Delete a certificate
**Example request**:
.. sourcecode:: http
DELETE /certificates/1 HTTP/1.1
Host: example.com
**Example response**:
.. sourcecode:: http
HTTP/1.1 204 OK
:reqheader Authorization: OAuth token to authenticate
:statuscode 204: no error
:statuscode 403: unauthenticated
2019-02-14 11:58:29 +01:00
:statuscode 404: certificate not found
:statuscode 405: certificate deletion is disabled
"""
2019-05-16 16:57:02 +02:00
if not current_app.config.get("ALLOW_CERT_DELETION", False):
return dict(message="Certificate deletion is disabled"), 405
cert = service.get(certificate_id)
if not cert:
return dict(message="Cannot find specified certificate"), 404
if cert.deleted:
return dict(message="Certificate is already deleted"), 412
# allow creators
if g.current_user != cert.user:
owner_role = role_service.get_by_name(cert.owner)
permission = CertificatePermission(owner_role, [x.name for x in cert.roles])
if not permission.can():
2019-05-16 16:57:02 +02:00
return (
dict(message="You are not authorized to delete this certificate"),
403,
)
service.update(certificate_id, deleted=True)
2019-05-16 16:57:02 +02:00
log_service.create(g.current_user, "delete_cert", certificate=cert)
return "Certificate deleted", 204
2015-06-22 22:47:27 +02:00
class NotificationCertificatesList(AuthenticatedResource):
""" Defines the 'certificates' endpoint """
2015-11-30 22:51:27 +01:00
def __init__(self):
self.reqparse = reqparse.RequestParser()
super(NotificationCertificatesList, self).__init__()
@validate_schema(None, certificates_output_schema)
def get(self, notification_id):
"""
.. http:get:: /notifications/1/certificates
The current list of certificates for a given notification
**Example request**:
.. sourcecode:: http
GET /notifications/1/certificates HTTP/1.1
Host: example.com
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: text/javascript
{
"items": [{
"status": null,
"cn": "*.test.example.net",
"chain": "",
"csr": "-----BEGIN CERTIFICATE REQUEST-----"
"authority": {
"active": true,
"owner": "secure@example.com",
"id": 1,
"description": "verisign test authority",
"name": "verisign"
},
"owner": "joe@example.com",
"serial": "82311058732025924142789179368889309156",
"id": 2288,
"issuer": "SymantecCorporation",
2017-08-18 14:13:27 +02:00
"dateCreated": "2016-06-03T06:09:42.133769+00:00",
"notBefore": "2016-06-03T00:00:00+00:00",
"notAfter": "2018-01-12T23:59:59+00:00",
"destinations": [],
"bits": 2048,
"body": "-----BEGIN CERTIFICATE-----...",
"description": null,
"deleted": null,
"notifications": [{
"id": 1
}],
"signingAlgorithm": "sha256",
"user": {
"username": "jane",
"active": true,
"email": "jane@example.com",
"id": 2
},
"active": true,
"domains": [{
"sensitive": false,
"id": 1090,
"name": "*.test.example.net"
}],
"replaces": [],
2016-12-20 23:26:39 +01:00
"replaced": [],
"rotation": true,
"rotationPolicy": {"name": "default"},
"name": "WILDCARD.test.example.net-SymantecCorporation-20160603-20180112",
"roles": [{
"id": 464,
"description": "This is a google group based role created by Lemur",
"name": "joe@example.com"
}],
"san": null
}],
"total": 1
}
:query sortBy: field to sort on
:query sortDir: asc or desc
2016-01-29 20:47:16 +01:00
:query page: int default is 1
:query filter: key value pair format is k;v
:query count: count number default is 10
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
:statuscode 403: unauthenticated
"""
parser = paginated_parser.copy()
2019-05-16 16:57:02 +02:00
parser.add_argument("timeRange", type=int, dest="time_range", location="args")
parser.add_argument("owner", type=inputs.boolean, location="args")
parser.add_argument("id", type=str, location="args")
parser.add_argument("active", type=inputs.boolean, location="args")
parser.add_argument(
"destinationId", type=int, dest="destination_id", location="args"
)
parser.add_argument("creator", type=str, location="args")
parser.add_argument("show", type=str, location="args")
parser.add_argument("showExpired", type=int, location="args")
args = parser.parse_args()
2019-05-16 16:57:02 +02:00
args["notification_id"] = notification_id
args["user"] = g.current_user
return service.render(args)
2015-11-24 23:53:22 +01:00
class CertificatesReplacementsList(AuthenticatedResource):
def __init__(self):
self.reqparse = reqparse.RequestParser()
super(CertificatesReplacementsList, self).__init__()
@validate_schema(None, certificates_output_schema)
2015-11-24 23:53:22 +01:00
def get(self, certificate_id):
"""
.. http:get:: /certificates/1/replacements
One certificate
**Example request**:
.. sourcecode:: http
GET /certificates/1/replacements HTTP/1.1
Host: example.com
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: text/javascript
{
"items": [{
"status": null,
"cn": "*.test.example.net",
"chain": "",
"csr": "-----BEGIN CERTIFICATE REQUEST-----",
"authority": {
"active": true,
"owner": "secure@example.com",
"id": 1,
"description": "verisign test authority",
"name": "verisign"
},
"owner": "joe@example.com",
"serial": "82311058732025924142789179368889309156",
"id": 2288,
"issuer": "SymantecCorporation",
2017-08-18 14:13:27 +02:00
"dateCreated": "2016-06-03T06:09:42.133769+00:00",
"notBefore": "2016-06-03T00:00:00+00:00",
"notAfter": "2018-01-12T23:59:59+00:00",
"destinations": [],
"bits": 2048,
"body": "-----BEGIN CERTIFICATE-----...",
"description": null,
"deleted": null,
"notifications": [{
"id": 1
}]
"signingAlgorithm": "sha256",
"user": {
"username": "jane",
"active": true,
"email": "jane@example.com",
"id": 2
},
"active": true,
"domains": [{
"sensitive": false,
"id": 1090,
"name": "*.test.example.net"
}],
"replaces": [],
2016-12-20 23:26:39 +01:00
"replaced": [],
"rotation": true,
"rotationPolicy": {"name": "default"},
"name": "WILDCARD.test.example.net-SymantecCorporation-20160603-20180112",
"roles": [{
"id": 464,
"description": "This is a google group based role created by Lemur",
"name": "joe@example.com"
}],
"san": null
}],
"total": 1
}
2015-11-24 23:53:22 +01:00
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
:statuscode 403: unauthenticated
2015-11-24 23:53:22 +01:00
"""
return service.get(certificate_id).replaces
2015-11-27 22:27:14 +01:00
class CertificateExport(AuthenticatedResource):
2015-11-25 23:54:08 +01:00
def __init__(self):
self.reqparse = reqparse.RequestParser()
2015-11-27 22:27:14 +01:00
super(CertificateExport, self).__init__()
2015-11-25 23:54:08 +01:00
@validate_schema(certificate_export_input_schema, None)
def post(self, certificate_id, data=None):
2015-11-25 23:54:08 +01:00
"""
.. http:post:: /certificates/1/export
Export a certificate
**Example request**:
.. sourcecode:: http
PUT /certificates/1/export HTTP/1.1
Host: example.com
Accept: application/json, text/javascript
Content-Type: application/json;charset=UTF-8
2015-11-25 23:54:08 +01:00
2015-11-30 22:51:27 +01:00
{
"export": {
"plugin": {
"pluginOptions": [{
"available": ["Java Key Store (JKS)"],
"required": true,
"type": "select",
"name": "type",
"helpMessage": "Choose the format you wish to export",
"value": "Java Key Store (JKS)"
}, {
"required": false,
"type": "str",
"name": "passphrase",
"validation": "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[$@$!%*#?&])[A-Za-z\\d$@$!%*#?&]{8,}$",
"helpMessage": "If no passphrase is given one will be generated for you, we highly recommend this. Minimum length is 8."
}, {
"required": false,
"type": "str",
"name": "alias",
"helpMessage": "Enter the alias you wish to use for the keystore."
}],
"version": "unknown",
"description": "Attempts to generate a JKS keystore or truststore",
"title": "Java",
"author": "Kevin Glisson",
"type": "export",
"slug": "java-export"
}
}
}
2015-11-25 23:54:08 +01:00
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: text/javascript
2015-11-30 22:51:27 +01:00
{
"data": "base64encodedstring",
"passphrase": "UAWOHW#&@_%!tnwmxh832025",
"extension": "jks"
}
2015-11-25 23:54:08 +01:00
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
:statuscode 403: unauthenticated
2015-11-25 23:54:08 +01:00
"""
cert = service.get(certificate_id)
if not cert:
return dict(message="Cannot find specified certificate"), 404
2015-11-25 23:54:08 +01:00
2019-05-16 16:57:02 +02:00
plugin = data["plugin"]["plugin_object"]
if plugin.requires_key:
if not cert.private_key:
2019-05-16 16:57:02 +02:00
return (
dict(
message="Unable to export certificate, plugin: {0} requires a private key but no key was found.".format(
plugin.slug
)
),
400,
)
2015-11-25 23:54:08 +01:00
else:
# allow creators
if g.current_user != cert.user:
owner_role = role_service.get_by_name(cert.owner)
2019-05-16 16:57:02 +02:00
permission = CertificatePermission(
owner_role, [x.name for x in cert.roles]
)
2015-11-25 23:54:08 +01:00
if not permission.can():
2019-05-16 16:57:02 +02:00
return (
dict(
message="You are not authorized to export this certificate."
),
403,
)
2015-11-25 23:54:08 +01:00
2019-05-16 16:57:02 +02:00
options = data["plugin"]["plugin_options"]
2016-10-31 19:00:15 +01:00
2019-05-16 16:57:02 +02:00
log_service.create(g.current_user, "key_view", certificate=cert)
extension, passphrase, data = plugin.export(
cert.body, cert.chain, cert.private_key, options
)
2016-10-31 19:00:15 +01:00
# we take a hit in message size when b64 encoding
2019-05-16 16:57:02 +02:00
return dict(
extension=extension,
passphrase=passphrase,
data=base64.b64encode(data).decode("utf-8"),
)
2016-10-31 19:00:15 +01:00
class CertificateRevoke(AuthenticatedResource):
def __init__(self):
self.reqparse = reqparse.RequestParser()
super(CertificateRevoke, self).__init__()
2020-12-01 05:06:37 +01:00
@validate_schema(certificate_revoke_schema, None)
def put(self, certificate_id, data=None):
"""
.. http:put:: /certificates/1/revoke
Revoke a certificate
**Example request**:
.. sourcecode:: http
POST /certificates/1/revoke HTTP/1.1
Host: example.com
Accept: application/json, text/javascript
Content-Type: application/json;charset=UTF-8
{
"crlReason": "affiliationChanged",
"comments": "Additional details if any"
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: text/javascript
{
"id": 1
}
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
:statuscode 403: unauthenticated or cert attached to LB
:statuscode 400: encountered error, more details in error message
"""
cert = service.get(certificate_id)
if not cert:
return dict(message="Cannot find specified certificate"), 404
# allow creators
if g.current_user != cert.user:
owner_role = role_service.get_by_name(cert.owner)
permission = CertificatePermission(owner_role, [x.name for x in cert.roles])
if not permission.can():
2019-05-16 16:57:02 +02:00
return (
dict(message="You are not authorized to revoke this certificate."),
403,
)
2020-11-24 02:24:01 +01:00
if not cert.external_id:
return dict(message="Cannot revoke certificate. No external id found."), 400
if cert.endpoints:
for endpoint in cert.endpoints:
if service.is_attached_to_endpoint(cert.name, endpoint.name):
return (
dict(
message="Cannot revoke certificate. Endpoints are deployed with the given certificate."
),
403,
)
try:
error_message = service.revoke(cert, data)
log_service.create(g.current_user, "revoke_cert", certificate=cert)
if error_message:
return dict(message=f"Certificate (id:{cert.id}) is revoked - {error_message}"), 400
return dict(id=cert.id)
except NotImplementedError as ne:
2020-12-03 03:12:19 +01:00
return dict(message="Revoke is not implemented for issuer of this certificate"), 400
except Exception as e:
sentry.captureException()
return dict(message=f"Failed to revoke: {str(e)}"), 400
2020-12-03 03:12:19 +01:00
2019-05-16 16:57:02 +02:00
api.add_resource(
CertificateRevoke,
"/certificates/<int:certificate_id>/revoke",
endpoint="revokeCertificate",
)
api.add_resource(
CertificatesNameQuery,
"/certificates/name/<string:certificate_name>",
endpoint="certificatesNameQuery",
)
api.add_resource(CertificatesList, "/certificates", endpoint="certificates")
api.add_resource(
CertificatesListValid, "/certificates/valid", endpoint="certificatesListValid"
)
api.add_resource(
Certificates, "/certificates/<int:certificate_id>", endpoint="certificate"
)
2020-10-08 20:38:39 +02:00
api.add_resource(
Certificates, "/certificates/<int:certificate_id>/update/notify", endpoint="certificateUpdateNotify"
)
2019-05-16 16:57:02 +02:00
api.add_resource(CertificatesStats, "/certificates/stats", endpoint="certificateStats")
api.add_resource(
CertificatesUpload, "/certificates/upload", endpoint="certificateUpload"
)
api.add_resource(
CertificatePrivateKey,
"/certificates/<int:certificate_id>/key",
endpoint="privateKeyCertificates",
)
api.add_resource(
CertificateExport,
"/certificates/<int:certificate_id>/export",
endpoint="exportCertificate",
)
api.add_resource(
NotificationCertificatesList,
"/notifications/<int:notification_id>/certificates",
endpoint="notificationCertificates",
)
api.add_resource(
CertificatesReplacementsList,
"/certificates/<int:certificate_id>/replacements",
endpoint="replacements",
)