Merge pull request #2757 from jplana/add-pending-certificate-upload
Allow uploading a signed cert for a pending certificate.
This commit is contained in:
commit
ccd0bde0bc
|
@ -1,5 +1,7 @@
|
|||
from marshmallow import fields, post_load
|
||||
from marshmallow import fields, validates_schema, post_load
|
||||
from marshmallow.exceptions import ValidationError
|
||||
|
||||
from lemur.common import utils, validators
|
||||
from lemur.authorities.schemas import AuthorityNestedOutputSchema
|
||||
from lemur.certificates.schemas import CertificateNestedOutputSchema
|
||||
from lemur.common.schema import LemurInputSchema, LemurOutputSchema
|
||||
|
@ -98,6 +100,31 @@ class PendingCertificateCancelSchema(LemurInputSchema):
|
|||
note = fields.String()
|
||||
|
||||
|
||||
class PendingCertificateUploadInputSchema(LemurInputSchema):
|
||||
external_id = fields.String(missing=None, allow_none=True)
|
||||
body = fields.String(required=True)
|
||||
chain = fields.String(missing=None, allow_none=True)
|
||||
|
||||
@validates_schema
|
||||
def validate_cert_chain(self, data):
|
||||
cert = None
|
||||
if data.get('body'):
|
||||
try:
|
||||
cert = utils.parse_certificate(data['body'])
|
||||
except ValueError:
|
||||
raise ValidationError("Public certificate presented is not valid.", field_names=['body'])
|
||||
|
||||
if data.get('chain'):
|
||||
try:
|
||||
chain = utils.parse_cert_chain(data['chain'])
|
||||
except ValueError:
|
||||
raise ValidationError("Invalid certificate in certificate chain.", field_names=['chain'])
|
||||
|
||||
# Throws ValidationError
|
||||
validators.verify_cert_chain([cert] + chain)
|
||||
|
||||
|
||||
pending_certificate_output_schema = PendingCertificateOutputSchema()
|
||||
pending_certificate_edit_input_schema = PendingCertificateEditInputSchema()
|
||||
pending_certificate_cancel_schema = PendingCertificateCancelSchema()
|
||||
pending_certificate_upload_input_schema = PendingCertificateUploadInputSchema()
|
||||
|
|
|
@ -8,9 +8,11 @@ from sqlalchemy import or_, cast, Integer
|
|||
|
||||
from lemur import database
|
||||
from lemur.authorities.models import Authority
|
||||
from lemur.authorities import service as authorities_service
|
||||
from lemur.certificates import service as certificate_service
|
||||
from lemur.certificates.schemas import CertificateUploadInputSchema
|
||||
from lemur.common.utils import truthiness
|
||||
from lemur.common.utils import truthiness, parse_cert_chain, parse_certificate
|
||||
from lemur.common import validators
|
||||
from lemur.destinations.models import Destination
|
||||
from lemur.domains.models import Domain
|
||||
from lemur.notifications.models import Notification
|
||||
|
@ -230,3 +232,40 @@ def render(args):
|
|||
# Only show unresolved certificates in the UI
|
||||
query = query.filter(PendingCertificate.resolved.is_(False))
|
||||
return database.sort_and_page(query, PendingCertificate, args)
|
||||
|
||||
|
||||
def upload(pending_certificate_id, **kwargs):
|
||||
"""
|
||||
Uploads a (signed) pending certificate. The allowed fields are validated by
|
||||
PendingCertificateUploadInputSchema. The certificate is also validated to be
|
||||
signed by the correct authoritity.
|
||||
"""
|
||||
pending_cert = get(pending_certificate_id)
|
||||
partial_cert = kwargs
|
||||
uploaded_chain = partial_cert['chain']
|
||||
|
||||
authority = authorities_service.get(pending_cert.authority.id)
|
||||
|
||||
# Construct the chain for cert validation
|
||||
if uploaded_chain:
|
||||
chain = uploaded_chain + '\n' + authority.authority_certificate.body
|
||||
else:
|
||||
chain = authority.authority_certificate.body
|
||||
|
||||
parsed_chain = parse_cert_chain(chain)
|
||||
|
||||
# Check that the certificate is actually signed by the CA to avoid incorrect cert pasting
|
||||
validators.verify_cert_chain([parse_certificate(partial_cert['body'])] + parsed_chain)
|
||||
|
||||
final_cert = create_certificate(pending_cert, partial_cert, pending_cert.user)
|
||||
|
||||
update(
|
||||
pending_cert.id,
|
||||
resolved=True
|
||||
)
|
||||
pending_cert_final_result = update(
|
||||
pending_cert.id,
|
||||
resolved_cert_id=final_cert.id
|
||||
)
|
||||
|
||||
return pending_cert_final_result
|
||||
|
|
|
@ -20,6 +20,7 @@ from lemur.pending_certificates.schemas import (
|
|||
pending_certificate_output_schema,
|
||||
pending_certificate_edit_input_schema,
|
||||
pending_certificate_cancel_schema,
|
||||
pending_certificate_upload_input_schema,
|
||||
)
|
||||
|
||||
mod = Blueprint('pending_certificates', __name__)
|
||||
|
@ -419,6 +420,101 @@ class PendingCertificatePrivateKey(AuthenticatedResource):
|
|||
return response
|
||||
|
||||
|
||||
class PendingCertificatesUpload(AuthenticatedResource):
|
||||
""" Defines the 'pending_certificates' upload endpoint """
|
||||
|
||||
def __init__(self):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(PendingCertificatesUpload, self).__init__()
|
||||
|
||||
@validate_schema(pending_certificate_upload_input_schema, pending_certificate_output_schema)
|
||||
def post(self, pending_certificate_id, data=None):
|
||||
"""
|
||||
.. http:post:: /pending_certificates/1/upload
|
||||
|
||||
Upload the body for a (signed) pending_certificate
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /certificates/1/upload HTTP/1.1
|
||||
Host: example.com
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
{
|
||||
"body": "-----BEGIN CERTIFICATE-----...",
|
||||
"chain": "-----BEGIN CERTIFICATE-----...",
|
||||
}
|
||||
|
||||
**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,
|
||||
"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
|
||||
}
|
||||
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 403: unauthenticated
|
||||
:statuscode 200: no error
|
||||
|
||||
"""
|
||||
return service.upload(pending_certificate_id, **data)
|
||||
|
||||
|
||||
api.add_resource(PendingCertificatesList, '/pending_certificates', endpoint='pending_certificates')
|
||||
api.add_resource(PendingCertificates, '/pending_certificates/<int:pending_certificate_id>', endpoint='pending_certificate')
|
||||
api.add_resource(PendingCertificatesUpload, '/pending_certificates/<int:pending_certificate_id>/upload', endpoint='pendingCertificateUpload')
|
||||
api.add_resource(PendingCertificatePrivateKey, '/pending_certificates/<int:pending_certificate_id>/key', endpoint='privateKeyPendingCertificates')
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
'use strict';
|
||||
|
||||
angular.module('lemur')
|
||||
.controller('PendingCertificateUploadController', function ($scope, $uibModalInstance, PendingCertificateApi, PendingCertificateService, toaster, uploadId) {
|
||||
PendingCertificateApi.get(uploadId).then(function (pendingCertificate) {
|
||||
$scope.pendingCertificate = pendingCertificate;
|
||||
});
|
||||
|
||||
$scope.upload = PendingCertificateService.upload;
|
||||
$scope.save = function (pendingCertificate) {
|
||||
PendingCertificateService.upload(pendingCertificate).then(
|
||||
function () {
|
||||
toaster.pop({
|
||||
type: 'success',
|
||||
title: pendingCertificate.name,
|
||||
body: 'Successfully uploaded!'
|
||||
});
|
||||
$uibModalInstance.close();
|
||||
},
|
||||
function (response) {
|
||||
toaster.pop({
|
||||
type: 'error',
|
||||
title: pendingCertificate.name,
|
||||
body: 'Failed to upload ' + response.data.message,
|
||||
timeout: 100000
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.cancel = function () {
|
||||
$uibModalInstance.dismiss('cancel');
|
||||
};
|
||||
|
||||
});
|
|
@ -0,0 +1,41 @@
|
|||
<div class="modal-header">
|
||||
<button type="button" class="close" ng-click="cancel()" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h3 class="modal-title">Import certificate <span class="text-muted"><small>{{ pendingCertificate.name }}</small></span></h3>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<form name="uploadForm" class="form-horizontal" role="form" novalidate>
|
||||
<div class="form-group"
|
||||
ng-class="{'has-error': uploadForm.publicCert.$invalid, 'has-success': !uploadForm.publicCert.$invalid&&uploadForm.publicCert.$dirty}">
|
||||
<label class="control-label col-sm-2">
|
||||
Public Certificate
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea name="publicCert" ng-model="pendingCertificate.body" placeholder="PEM encoded string..."
|
||||
class="form-control" ng-pattern="/^-----BEGIN CERTIFICATE-----/" required></textarea>
|
||||
<p ng-show="uploadForm.publicCert.$invalid && !uploadForm.publicCert.$pristine" class="help-block">Enter
|
||||
a valid certificate.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group"
|
||||
ng-class="{'has-error': uploadForm.owner.$invalid&&uploadform.intermediateCert.$dirty, 'has-success': !uploadForm.intermediateCert.$invalid&&uploadForm.intermediateCert.$dirty}">
|
||||
<label class="control-label col-sm-2">
|
||||
Intermediate Certificate
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea name="intermediateCert" ng-model="pendingCertificate.chain"
|
||||
placeholder="PEM encoded string..." class="form-control"
|
||||
ng-pattern="/^-----BEGIN CERTIFICATE-----/"></textarea>
|
||||
<p ng-show="uploadForm.intermediateCert.$invalid && !uploadForm.intemediateCert.$pristine"
|
||||
class="help-block">Enter a valid certificate.</p>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" ng-click="save(pendingCertificate)" ng-disabled="uploadForm.$invalid" class="btn btn-success">
|
||||
Import
|
||||
</button>
|
||||
<button ng-click="cancel()" class="btn btn-danger">Cancel</button>
|
||||
</div>
|
||||
</div>
|
|
@ -245,5 +245,9 @@ angular.module('lemur')
|
|||
return pending_certificate.customOperation('remove', null, {}, {'Content-Type': 'application/json'}, options);
|
||||
};
|
||||
|
||||
PendingCertificateService.upload = function (pending_certificate) {
|
||||
return pending_certificate.customPOST({'body': pending_certificate.body, 'chain': pending_certificate.chain}, 'upload');
|
||||
};
|
||||
|
||||
return PendingCertificateService;
|
||||
});
|
||||
|
|
|
@ -99,4 +99,23 @@ angular.module('lemur')
|
|||
$scope.pendingCertificateTable.reload();
|
||||
});
|
||||
};
|
||||
|
||||
$scope.upload = function (pendingCertificateId) {
|
||||
var uibModalInstance = $uibModal.open({
|
||||
animation: true,
|
||||
controller: 'PendingCertificateUploadController',
|
||||
templateUrl: '/angular/pending_certificates/pending_certificate/upload.tpl.html',
|
||||
size: 'lg',
|
||||
backdrop: 'static',
|
||||
resolve: {
|
||||
uploadId: function () {
|
||||
return pendingCertificateId;
|
||||
}
|
||||
}
|
||||
});
|
||||
uibModalInstance.result.then(function () {
|
||||
$scope.pendingCertificateTable.reload();
|
||||
});
|
||||
};
|
||||
|
||||
});
|
||||
|
|
|
@ -51,6 +51,7 @@
|
|||
<ul class="dropdown-menu">
|
||||
<li><a href ng-click="edit(pendingCertificate.id)">Edit</a></li>
|
||||
<li><a href ng-click="cancel(pendingCertificate.id)">Cancel</a></li>
|
||||
<li><a href ng-click="upload(pendingCertificate.id)">Upload</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -13,12 +13,12 @@ from lemur import create_app
|
|||
from lemur.common.utils import parse_private_key
|
||||
from lemur.database import db as _db
|
||||
from lemur.auth.service import create_token
|
||||
from lemur.tests.vectors import SAN_CERT_KEY, INTERMEDIATE_KEY
|
||||
from lemur.tests.vectors import SAN_CERT_KEY, INTERMEDIATE_KEY, ROOTCA_CERT_STR, ROOTCA_KEY
|
||||
|
||||
from .factories import ApiKeyFactory, AuthorityFactory, NotificationFactory, DestinationFactory, \
|
||||
CertificateFactory, UserFactory, RoleFactory, SourceFactory, EndpointFactory, \
|
||||
RotationPolicyFactory, PendingCertificateFactory, AsyncAuthorityFactory, InvalidCertificateFactory, \
|
||||
CryptoAuthorityFactory
|
||||
CryptoAuthorityFactory, CACertificateFactory
|
||||
|
||||
|
||||
def pytest_runtest_setup(item):
|
||||
|
@ -172,6 +172,25 @@ def pending_certificate(session):
|
|||
return p
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pending_certificate_from_full_chain_ca(session):
|
||||
u = UserFactory()
|
||||
a = AuthorityFactory()
|
||||
p = PendingCertificateFactory(user=u, authority=a)
|
||||
session.commit()
|
||||
return p
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pending_certificate_from_partial_chain_ca(session):
|
||||
u = UserFactory()
|
||||
c = CACertificateFactory(body=ROOTCA_CERT_STR, private_key=ROOTCA_KEY, chain=None)
|
||||
a = AuthorityFactory(authority_certificate=c)
|
||||
p = PendingCertificateFactory(user=u, authority=a)
|
||||
session.commit()
|
||||
return p
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def invalid_certificate(session):
|
||||
u = UserFactory()
|
||||
|
|
|
@ -2,6 +2,7 @@ import json
|
|||
|
||||
import pytest
|
||||
|
||||
from marshmallow import ValidationError
|
||||
from lemur.pending_certificates.views import * # noqa
|
||||
from .vectors import CSR_STR, INTERMEDIATE_CERT_STR, VALID_ADMIN_API_TOKEN, VALID_ADMIN_HEADER_TOKEN, \
|
||||
VALID_USER_HEADER_TOKEN, WILDCARD_CERT_STR
|
||||
|
@ -50,3 +51,44 @@ def test_pending_cancel(client, pending_certificate, token, status):
|
|||
assert client.delete(api.url_for(PendingCertificates, pending_certificate_id=pending_certificate.id),
|
||||
data=json.dumps({'note': "unit test", 'send_email': False}),
|
||||
headers=token).status_code == status
|
||||
|
||||
|
||||
def test_pending_upload(pending_certificate_from_full_chain_ca):
|
||||
from lemur.pending_certificates.service import upload
|
||||
from lemur.certificates.service import get
|
||||
|
||||
cert = {'body': WILDCARD_CERT_STR,
|
||||
'chain': None,
|
||||
'external_id': None
|
||||
}
|
||||
|
||||
pending_cert = upload(pending_certificate_from_full_chain_ca.id, **cert)
|
||||
assert pending_cert.resolved
|
||||
assert get(pending_cert.resolved_cert_id)
|
||||
|
||||
|
||||
def test_pending_upload_with_chain(pending_certificate_from_partial_chain_ca):
|
||||
from lemur.pending_certificates.service import upload
|
||||
from lemur.certificates.service import get
|
||||
|
||||
cert = {'body': WILDCARD_CERT_STR,
|
||||
'chain': INTERMEDIATE_CERT_STR,
|
||||
'external_id': None
|
||||
}
|
||||
|
||||
pending_cert = upload(pending_certificate_from_partial_chain_ca.id, **cert)
|
||||
assert pending_cert.resolved
|
||||
assert get(pending_cert.resolved_cert_id)
|
||||
|
||||
|
||||
def test_invalid_pending_upload_with_chain(pending_certificate_from_partial_chain_ca):
|
||||
from lemur.pending_certificates.service import upload
|
||||
|
||||
cert = {'body': WILDCARD_CERT_STR,
|
||||
'chain': None,
|
||||
'external_id': None
|
||||
}
|
||||
with pytest.raises(ValidationError) as err:
|
||||
upload(pending_certificate_from_partial_chain_ca.id, **cert)
|
||||
assert str(err.value).startswith(
|
||||
'Incorrect chain certificate(s) provided: \'*.wild.example.org\' is not signed by \'LemurTrust Unittests Root CA 2018')
|
||||
|
|
Loading…
Reference in New Issue