diff --git a/lemur/authorities/schemas.py b/lemur/authorities/schemas.py index ef6263a8..f80d1581 100644 --- a/lemur/authorities/schemas.py +++ b/lemur/authorities/schemas.py @@ -43,13 +43,13 @@ class AuthorityInputSchema(LemurInputSchema): organization = fields.String( missing=lambda: current_app.config.get("LEMUR_DEFAULT_ORGANIZATION") ) - location = fields.String( - missing=lambda: current_app.config.get("LEMUR_DEFAULT_LOCATION") - ) + location = fields.String() country = fields.String( missing=lambda: current_app.config.get("LEMUR_DEFAULT_COUNTRY") ) state = fields.String(missing=lambda: current_app.config.get("LEMUR_DEFAULT_STATE")) + # Creating a String field instead of Email to allow empty value + email = fields.String() plugin = fields.Nested(PluginInputSchema) diff --git a/lemur/certificates/schemas.py b/lemur/certificates/schemas.py index f393aa49..688d6ba4 100644 --- a/lemur/certificates/schemas.py +++ b/lemur/certificates/schemas.py @@ -23,6 +23,7 @@ from lemur.domains.schemas import DomainNestedOutputSchema from lemur.notifications import service as notification_service from lemur.notifications.schemas import NotificationNestedOutputSchema from lemur.policies.schemas import RotationPolicyNestedOutputSchema +from lemur.roles import service as roles_service from lemur.roles.schemas import RoleNestedOutputSchema from lemur.schemas import ( AssociatedAuthoritySchema, @@ -107,9 +108,7 @@ class CertificateInputSchema(CertificateCreationSchema): organization = fields.String( missing=lambda: current_app.config.get("LEMUR_DEFAULT_ORGANIZATION") ) - location = fields.String( - missing=lambda: current_app.config.get("LEMUR_DEFAULT_LOCATION") - ) + location = fields.String() country = fields.String( missing=lambda: current_app.config.get("LEMUR_DEFAULT_COUNTRY") ) @@ -186,25 +185,52 @@ class CertificateEditInputSchema(CertificateSchema): data["replaces"] = data[ "replacements" ] # TODO remove when field is deprecated + + if data.get("owner"): + # Check if role already exists. This avoids adding duplicate role. + if data.get("roles") and any(r.get("name") == data["owner"] for r in data["roles"]): + return data + + # Add required role + owner_role = roles_service.get_or_create( + data["owner"], + description=f"Auto generated role based on owner: {data['owner']}" + ) + + # Put role info in correct format using RoleNestedOutputSchema + owner_role_dict = RoleNestedOutputSchema().dump(owner_role).data + if data.get("roles"): + data["roles"].append(owner_role_dict) + else: + data["roles"] = [owner_role_dict] + return data @post_load def enforce_notifications(self, data): """ - Ensures that when an owner changes, default notifications are added for the new owner. - Old owner notifications are retained unless explicitly removed. + Add default notification for current owner if none exist. + This ensures that the default notifications are added in the event of owner change. + Old owner notifications are retained unless explicitly removed later in the code path. :param data: :return: """ - if data["owner"]: + if data.get("owner"): notification_name = "DEFAULT_{0}".format( data["owner"].split("@")[0].upper() ) + + # Even if one default role exists, return + # This allows a User to remove unwanted default notification for current owner + if any(n.label.startswith(notification_name) for n in data["notifications"]): + return data + data[ "notifications" ] += notification_service.create_default_expiration_notifications( notification_name, [data["owner"]] ) + return data diff --git a/lemur/certificates/service.py b/lemur/certificates/service.py index df73487d..6d1bd2ac 100644 --- a/lemur/certificates/service.py +++ b/lemur/certificates/service.py @@ -256,17 +256,29 @@ def update(cert_id, **kwargs): return database.update(cert) -def create_certificate_roles(**kwargs): - # create an role for the owner and assign it - owner_role = role_service.get_by_name(kwargs["owner"]) +def cleanup_owner_roles_notification(owner_name, kwargs): + kwargs["roles"] = [r for r in kwargs["roles"] if r.name != owner_name] + notification_prefix = f"DEFAULT_{owner_name.split('@')[0].upper()}" + kwargs["notifications"] = [n for n in kwargs["notifications"] if not n.label.startswith(notification_prefix)] - if not owner_role: - owner_role = role_service.create( - kwargs["owner"], - description="Auto generated role based on owner: {0}".format( - kwargs["owner"] - ), - ) + +def update_notify(cert, notify_flag): + """ + Toggle notification value which is a boolean + :param notify_flag: new notify value + :param cert: Certificate object to be updated + :return: + """ + cert.notify = notify_flag + return database.update(cert) + + +def create_certificate_roles(**kwargs): + # create a role for the owner and assign it + owner_role = role_service.get_or_create( + kwargs["owner"], + description=f"Auto generated role based on owner: {kwargs['owner']}" + ) # ensure that the authority's owner is also associated with the certificate if kwargs.get("authority"): diff --git a/lemur/certificates/views.py b/lemur/certificates/views.py index 51f7f615..18746636 100644 --- a/lemur/certificates/views.py +++ b/lemur/certificates/views.py @@ -884,10 +884,118 @@ class Certificates(AuthenticatedResource): 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) + cert = service.update(certificate_id, **data) log_service.create(g.current_user, "update_cert", certificate=cert) return cert + @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 + + { + "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 @@ -1354,6 +1462,9 @@ api.add_resource( api.add_resource( Certificates, "/certificates/", endpoint="certificate" ) +api.add_resource( + Certificates, "/certificates//update/notify", endpoint="certificateUpdateNotify" +) api.add_resource(CertificatesStats, "/certificates/stats", endpoint="certificateStats") api.add_resource( CertificatesUpload, "/certificates/upload", endpoint="certificateUpload" diff --git a/lemur/common/utils.py b/lemur/common/utils.py index 283d1eec..19b256e8 100644 --- a/lemur/common/utils.py +++ b/lemur/common/utils.py @@ -9,6 +9,7 @@ import random import re import string +import pem import sqlalchemy from cryptography import x509 @@ -16,7 +17,7 @@ from cryptography.exceptions import InvalidSignature, UnsupportedAlgorithm from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import rsa, ec, padding -from cryptography.hazmat.primitives.serialization import load_pem_private_key +from cryptography.hazmat.primitives.serialization import load_pem_private_key, Encoding, pkcs7 from flask_restful.reqparse import RequestParser from sqlalchemy import and_, func @@ -357,3 +358,19 @@ def find_matching_certificates_by_hash(cert, matching_certs): ): matching.append(c) return matching + + +def convert_pkcs7_bytes_to_pem(certs_pkcs7): + """ + Given a list of certificates in pkcs7 encoding (bytes), covert them into a list of PEM encoded files + :raises ValueError or ValidationError + :param certs_pkcs7: + :return: list of certs in PEM format + """ + + certificates = pkcs7.load_pem_pkcs7_certificates(certs_pkcs7) + certificates_pem = [] + for cert in certificates: + certificates_pem.append(pem.parse(cert.public_bytes(encoding=Encoding.PEM))[0]) + + return certificates_pem diff --git a/lemur/plugins/lemur_digicert/plugin.py b/lemur/plugins/lemur_digicert/plugin.py index 3948acbb..cf01c9d1 100644 --- a/lemur/plugins/lemur_digicert/plugin.py +++ b/lemur/plugins/lemur_digicert/plugin.py @@ -21,7 +21,7 @@ import requests import sys from cryptography import x509 from flask import current_app, g -from lemur.common.utils import validate_conf +from lemur.common.utils import validate_conf, convert_pkcs7_bytes_to_pem from lemur.extensions import metrics from lemur.plugins import lemur_digicert as digicert from lemur.plugins.bases import IssuerPlugin, SourcePlugin @@ -235,15 +235,18 @@ def get_certificate_id(session, base_url, order_id): @retry(stop_max_attempt_number=10, wait_fixed=10000) def get_cis_certificate(session, base_url, order_id): - """Retrieve certificate order id from Digicert API.""" - certificate_url = "{0}/platform/cis/certificate/{1}".format(base_url, order_id) - session.headers.update({"Accept": "application/x-pem-file"}) + """Retrieve certificate order id from Digicert API, including the chain""" + certificate_url = "{0}/platform/cis/certificate/{1}/download".format(base_url, order_id) + session.headers.update({"Accept": "application/x-pkcs7-certificates"}) response = session.get(certificate_url) if response.status_code == 404: raise Exception("Order not in issued state.") - return response.content + cert_chain_pem = convert_pkcs7_bytes_to_pem(response.content) + if len(cert_chain_pem) < 3: + raise Exception("Missing the certificate chain") + return cert_chain_pem class DigiCertSourcePlugin(SourcePlugin): @@ -447,7 +450,6 @@ class DigiCertCISSourcePlugin(SourcePlugin): "DIGICERT_CIS_API_KEY", "DIGICERT_CIS_URL", "DIGICERT_CIS_ROOTS", - "DIGICERT_CIS_INTERMEDIATES", "DIGICERT_CIS_PROFILE_NAMES", ] validate_conf(current_app, required_vars) @@ -522,7 +524,6 @@ class DigiCertCISIssuerPlugin(IssuerPlugin): "DIGICERT_CIS_API_KEY", "DIGICERT_CIS_URL", "DIGICERT_CIS_ROOTS", - "DIGICERT_CIS_INTERMEDIATES", "DIGICERT_CIS_PROFILE_NAMES", ] @@ -552,22 +553,15 @@ class DigiCertCISIssuerPlugin(IssuerPlugin): data = handle_cis_response(response) # retrieve certificate - certificate_pem = get_cis_certificate(self.session, base_url, data["id"]) + certificate_chain_pem = get_cis_certificate(self.session, base_url, data["id"]) self.session.headers.pop("Accept") - end_entity = pem.parse(certificate_pem)[0] + end_entity = certificate_chain_pem[0] + intermediate = certificate_chain_pem[1] - if "ECC" in issuer_options["key_type"]: - return ( - "\n".join(str(end_entity).splitlines()), - current_app.config.get("DIGICERT_ECC_CIS_INTERMEDIATES", {}).get(issuer_options['authority'].name), - data["id"], - ) - - # By default return RSA return ( "\n".join(str(end_entity).splitlines()), - current_app.config.get("DIGICERT_CIS_INTERMEDIATES", {}).get(issuer_options['authority'].name), + "\n".join(str(intermediate).splitlines()), data["id"], ) diff --git a/lemur/plugins/lemur_entrust/plugin.py b/lemur/plugins/lemur_entrust/plugin.py index 9b7848ed..515e2400 100644 --- a/lemur/plugins/lemur_entrust/plugin.py +++ b/lemur/plugins/lemur_entrust/plugin.py @@ -34,8 +34,7 @@ def determine_end_date(end_date): if not end_date: end_date = max_validity_end - - if end_date > max_validity_end: + elif end_date > max_validity_end: end_date = max_validity_end return end_date.format('YYYY-MM-DD') diff --git a/lemur/plugins/lemur_entrust/tests/test_entrust.py b/lemur/plugins/lemur_entrust/tests/test_entrust.py index b1cd4c83..354e204e 100644 --- a/lemur/plugins/lemur_entrust/tests/test_entrust.py +++ b/lemur/plugins/lemur_entrust/tests/test_entrust.py @@ -3,6 +3,7 @@ from unittest.mock import patch, Mock import arrow from cryptography import x509 from lemur.plugins.lemur_entrust import plugin +from freezegun import freeze_time def config_mock(*args): @@ -21,11 +22,18 @@ def config_mock(*args): return values[args[0]] +@patch("lemur.plugins.lemur_digicert.plugin.current_app") +def test_determine_end_date(mock_current_app): + with freeze_time(time_to_freeze=arrow.get(2016, 11, 3).datetime): + assert arrow.get(2017, 12, 3).format('YYYY-MM-DD') == plugin.determine_end_date(0) # 1 year + 1 month + assert arrow.get(2017, 3, 5).format('YYYY-MM-DD') == plugin.determine_end_date(arrow.get(2017, 3, 5)) + assert arrow.get(2017, 12, 3).format('YYYY-MM-DD') == plugin.determine_end_date(arrow.get(2020, 5, 7)) + + @patch("lemur.plugins.lemur_entrust.plugin.current_app") def test_process_options(mock_current_app, authority): mock_current_app.config.get = Mock(side_effect=config_mock) - plugin.determine_end_date = Mock(return_value=arrow.get(2020, 10, 7).format('YYYY-MM-DD')) - + plugin.determine_end_date = Mock(return_value=arrow.get(2017, 11, 5).format('YYYY-MM-DD')) authority.name = "Entrust" names = [u"one.example.com", u"two.example.com", u"three.example.com"] options = { @@ -35,7 +43,7 @@ def test_process_options(mock_current_app, authority): "extensions": {"sub_alt_names": {"names": [x509.DNSName(x) for x in names]}}, "organization": "Example, Inc.", "organizational_unit": "Example Org", - "validity_end": arrow.get(2020, 10, 7), + "validity_end": arrow.utcnow().shift(years=1, months=+1), "authority": authority, } @@ -43,7 +51,7 @@ def test_process_options(mock_current_app, authority): "signingAlg": "SHA-2", "eku": "SERVER_AND_CLIENT_AUTH", "certType": "ADVANTAGE_SSL", - "certExpiryDate": arrow.get(2020, 10, 7).format('YYYY-MM-DD'), + "certExpiryDate": arrow.get(2017, 11, 5).format('YYYY-MM-DD'), "tracking": { "requesterName": mock_current_app.config.get("ENTRUST_NAME"), "requesterEmail": mock_current_app.config.get("ENTRUST_EMAIL"), diff --git a/lemur/roles/service.py b/lemur/roles/service.py index 51597d6e..fa4c9c97 100644 --- a/lemur/roles/service.py +++ b/lemur/roles/service.py @@ -128,3 +128,11 @@ def render(args): query = database.filter(query, Role, terms) return database.sort_and_page(query, Role, args) + + +def get_or_create(role_name, description): + role = get_by_name(role_name) + if not role: + role = create(name=role_name, description=description) + + return role diff --git a/lemur/static/app/angular/authorities/authority/authority.js b/lemur/static/app/angular/authorities/authority/authority.js index 9863bf4d..a449cff5 100644 --- a/lemur/static/app/angular/authorities/authority/authority.js +++ b/lemur/static/app/angular/authorities/authority/authority.js @@ -124,4 +124,8 @@ angular.module('lemur') opened: false }; + $scope.populateSubjectEmail = function () { + $scope.authority.email = $scope.authority.owner; + }; + }); diff --git a/lemur/static/app/angular/authorities/authority/distinguishedName.tpl.html b/lemur/static/app/angular/authorities/authority/distinguishedName.tpl.html index c6a7d312..e94f856e 100644 --- a/lemur/static/app/angular/authorities/authority/distinguishedName.tpl.html +++ b/lemur/static/app/angular/authorities/authority/distinguishedName.tpl.html @@ -26,8 +26,7 @@ Location
- -

You must enter a location

+
+
+ +
+ +
+
diff --git a/lemur/static/app/angular/authorities/authority/tracking.tpl.html b/lemur/static/app/angular/authorities/authority/tracking.tpl.html index 72d7e3d5..a561745f 100644 --- a/lemur/static/app/angular/authorities/authority/tracking.tpl.html +++ b/lemur/static/app/angular/authorities/authority/tracking.tpl.html @@ -21,7 +21,7 @@
+ class="form-control" ng-change="populateSubjectEmail()" required/>

You must enter an Certificate Authority owner

diff --git a/lemur/static/app/angular/certificates/certificate/distinguishedName.tpl.html b/lemur/static/app/angular/certificates/certificate/distinguishedName.tpl.html index 72f168a0..bc08c786 100644 --- a/lemur/static/app/angular/certificates/certificate/distinguishedName.tpl.html +++ b/lemur/static/app/angular/certificates/certificate/distinguishedName.tpl.html @@ -38,9 +38,7 @@ Location
- -

You must enter a - location

+