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
This commit is contained in:
parent
f44fe81573
commit
2578970f7d
@ -100,10 +100,16 @@ If you have a third party or internal service that creates authorities (EJBCA, e
|
|||||||
it can treat any issuer plugin as both a source of creating new certificates as well as new authorities.
|
it can treat any issuer plugin as both a source of creating new certificates as well as new authorities.
|
||||||
|
|
||||||
|
|
||||||
The `IssuerPlugin` exposes two functions::
|
The `IssuerPlugin` exposes four functions functions::
|
||||||
|
|
||||||
def create_certificate(self, csr, issuer_options):
|
def create_certificate(self, csr, issuer_options):
|
||||||
# requests.get('a third party')
|
# requests.get('a third party')
|
||||||
|
def revoke_certificate(self, certificate, comments):
|
||||||
|
# requests.put('a third party')
|
||||||
|
def get_ordered_certificate(self, order_id):
|
||||||
|
# requests.get('already existing certificate')
|
||||||
|
def canceled_ordered_certificate(self, pending_cert, **kwargs):
|
||||||
|
# requests.put('cancel an order that has yet to be issued')
|
||||||
|
|
||||||
Lemur will pass a dictionary of all possible options for certificate creation. Including a valid CSR, and the raw options associated with the request.
|
Lemur will pass a dictionary of all possible options for certificate creation. Including a valid CSR, and the raw options associated with the request.
|
||||||
|
|
||||||
@ -139,6 +145,19 @@ The `IssuerPlugin` doesn't have any options like Destination, Source, and Notifi
|
|||||||
any fields you might need to submit a request to a third party. If there are additional options you need
|
any fields you might need to submit a request to a third party. If there are additional options you need
|
||||||
in your plugin feel free to open an issue, or look into adding additional options to issuers yourself.
|
in your plugin feel free to open an issue, or look into adding additional options to issuers yourself.
|
||||||
|
|
||||||
|
Asynchronous Certificates
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
An issuer may take some time to actually issue a certificate for an order. In this case, a `PendingCertificate` is returned, which holds information to recreate a `Certificate` object at a later time. Then, `get_ordered_certificate()` should be run periodically via `python manage.py pending_certs fetch -i all` to attempt to retrieve an ordered certificate::
|
||||||
|
|
||||||
|
def get_ordered_ceriticate(self, order_id):
|
||||||
|
# order_id is the external id of the order, not the external_id of the certificate
|
||||||
|
# retrieve an order, and check if there is an issued certificate attached to it
|
||||||
|
|
||||||
|
`cancel_ordered_certificate()` should be implemented to allow an ordered certificate to be canceled before it is issued::
|
||||||
|
def cancel_ordered_certificate(self, pending_cert, **kwargs):
|
||||||
|
# pending_cert should contain the necessary information to match an order
|
||||||
|
# kwargs can be given to provide information to the issuer for canceling
|
||||||
|
|
||||||
Destination
|
Destination
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
|
@ -27,6 +27,7 @@ from lemur.sources.views import mod as sources_bp
|
|||||||
from lemur.endpoints.views import mod as endpoints_bp
|
from lemur.endpoints.views import mod as endpoints_bp
|
||||||
from lemur.logs.views import mod as logs_bp
|
from lemur.logs.views import mod as logs_bp
|
||||||
from lemur.api_keys.views import mod as api_key_bp
|
from lemur.api_keys.views import mod as api_key_bp
|
||||||
|
from lemur.pending_certificates.views import mod as pending_certificates_bp
|
||||||
|
|
||||||
from lemur.__about__ import (
|
from lemur.__about__ import (
|
||||||
__author__, __copyright__, __email__, __license__, __summary__, __title__,
|
__author__, __copyright__, __email__, __license__, __summary__, __title__,
|
||||||
@ -53,7 +54,8 @@ LEMUR_BLUEPRINTS = (
|
|||||||
sources_bp,
|
sources_bp,
|
||||||
endpoints_bp,
|
endpoints_bp,
|
||||||
logs_bp,
|
logs_bp,
|
||||||
api_key_bp
|
api_key_bp,
|
||||||
|
pending_certificates_bp,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -32,6 +32,9 @@ class Authority(db.Model):
|
|||||||
authority_certificate = relationship("Certificate", backref='root_authority', uselist=False, foreign_keys='Certificate.root_authority_id')
|
authority_certificate = relationship("Certificate", backref='root_authority', uselist=False, foreign_keys='Certificate.root_authority_id')
|
||||||
certificates = relationship("Certificate", backref='authority', foreign_keys='Certificate.authority_id')
|
certificates = relationship("Certificate", backref='authority', foreign_keys='Certificate.authority_id')
|
||||||
|
|
||||||
|
authority_pending_certificate = relationship("PendingCertificate", backref='root_authority', uselist=False, foreign_keys='PendingCertificate.root_authority_id')
|
||||||
|
pending_certificates = relationship('PendingCertificate', backref='authority', foreign_keys='PendingCertificate.authority_id')
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
self.owner = kwargs['owner']
|
self.owner = kwargs['owner']
|
||||||
self.roles = kwargs.get('roles', [])
|
self.roles = kwargs.get('roles', [])
|
||||||
|
@ -37,7 +37,7 @@ from lemur.constants import SUCCESS_METRIC_STATUS, FAILURE_METRIC_STATUS
|
|||||||
|
|
||||||
from lemur.models import certificate_associations, certificate_source_associations, \
|
from lemur.models import certificate_associations, certificate_source_associations, \
|
||||||
certificate_destination_associations, certificate_notification_associations, \
|
certificate_destination_associations, certificate_notification_associations, \
|
||||||
certificate_replacement_associations, roles_certificates
|
certificate_replacement_associations, roles_certificates, pending_cert_replacement_associations
|
||||||
|
|
||||||
from lemur.domains.models import Domain
|
from lemur.domains.models import Domain
|
||||||
from lemur.policies.models import RotationPolicy
|
from lemur.policies.models import RotationPolicy
|
||||||
@ -129,6 +129,11 @@ class Certificate(db.Model):
|
|||||||
secondaryjoin=id == certificate_replacement_associations.c.replaced_certificate_id, # noqa
|
secondaryjoin=id == certificate_replacement_associations.c.replaced_certificate_id, # noqa
|
||||||
backref='replaced')
|
backref='replaced')
|
||||||
|
|
||||||
|
replaced_by_pending = relationship('PendingCertificate',
|
||||||
|
secondary=pending_cert_replacement_associations,
|
||||||
|
backref='pending_replace',
|
||||||
|
viewonly=True)
|
||||||
|
|
||||||
logs = relationship('Log', backref='certificate')
|
logs = relationship('Log', backref='certificate')
|
||||||
endpoints = relationship('Endpoint', backref='certificate')
|
endpoints = relationship('Endpoint', backref='certificate')
|
||||||
rotation_policy = relationship("RotationPolicy")
|
rotation_policy = relationship("RotationPolicy")
|
||||||
|
@ -25,6 +25,7 @@ from lemur.authorities.models import Authority
|
|||||||
from lemur.destinations.models import Destination
|
from lemur.destinations.models import Destination
|
||||||
from lemur.certificates.models import Certificate
|
from lemur.certificates.models import Certificate
|
||||||
from lemur.notifications.models import Notification
|
from lemur.notifications.models import Notification
|
||||||
|
from lemur.pending_certificates.models import PendingCertificate
|
||||||
|
|
||||||
from lemur.certificates.schemas import CertificateOutputSchema, CertificateInputSchema
|
from lemur.certificates.schemas import CertificateOutputSchema, CertificateInputSchema
|
||||||
|
|
||||||
@ -63,6 +64,9 @@ def get_by_serial(serial):
|
|||||||
:param serial:
|
:param serial:
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
|
if isinstance(serial, int):
|
||||||
|
# although serial is a number, the DB column is String(128)
|
||||||
|
serial = str(serial)
|
||||||
return Certificate.query.filter(Certificate.serial == serial).all()
|
return Certificate.query.filter(Certificate.serial == serial).all()
|
||||||
|
|
||||||
|
|
||||||
@ -190,7 +194,7 @@ def mint(**kwargs):
|
|||||||
csr_imported.send(authority=authority, csr=csr)
|
csr_imported.send(authority=authority, csr=csr)
|
||||||
|
|
||||||
cert_body, cert_chain, external_id = issuer.create_certificate(csr, kwargs)
|
cert_body, cert_chain, external_id = issuer.create_certificate(csr, kwargs)
|
||||||
return cert_body, private_key, cert_chain, external_id
|
return cert_body, private_key, cert_chain, external_id, csr
|
||||||
|
|
||||||
|
|
||||||
def import_certificate(**kwargs):
|
def import_certificate(**kwargs):
|
||||||
@ -243,11 +247,12 @@ def create(**kwargs):
|
|||||||
"""
|
"""
|
||||||
Creates a new certificate.
|
Creates a new certificate.
|
||||||
"""
|
"""
|
||||||
cert_body, private_key, cert_chain, external_id = mint(**kwargs)
|
cert_body, private_key, cert_chain, external_id, csr = mint(**kwargs)
|
||||||
kwargs['body'] = cert_body
|
kwargs['body'] = cert_body
|
||||||
kwargs['private_key'] = private_key
|
kwargs['private_key'] = private_key
|
||||||
kwargs['chain'] = cert_chain
|
kwargs['chain'] = cert_chain
|
||||||
kwargs['external_id'] = external_id
|
kwargs['external_id'] = external_id
|
||||||
|
kwargs['csr'] = csr
|
||||||
|
|
||||||
roles = create_certificate_roles(**kwargs)
|
roles = create_certificate_roles(**kwargs)
|
||||||
|
|
||||||
@ -256,14 +261,19 @@ def create(**kwargs):
|
|||||||
else:
|
else:
|
||||||
kwargs['roles'] = roles
|
kwargs['roles'] = roles
|
||||||
|
|
||||||
|
if cert_body:
|
||||||
cert = Certificate(**kwargs)
|
cert = Certificate(**kwargs)
|
||||||
|
|
||||||
kwargs['creator'].certificates.append(cert)
|
kwargs['creator'].certificates.append(cert)
|
||||||
|
else:
|
||||||
|
cert = PendingCertificate(**kwargs)
|
||||||
|
kwargs['creator'].pending_certificates.append(cert)
|
||||||
|
|
||||||
cert.authority = kwargs['authority']
|
cert.authority = kwargs['authority']
|
||||||
certificate_issued.send(certificate=cert, authority=cert.authority)
|
|
||||||
|
|
||||||
database.commit()
|
database.commit()
|
||||||
|
|
||||||
|
if isinstance(cert, Certificate):
|
||||||
|
certificate_issued.send(certificate=cert, authority=cert.authority)
|
||||||
metrics.send('certificate_issued', 'counter', 1, metric_tags=dict(owner=cert.owner, issuer=cert.issuer))
|
metrics.send('certificate_issued', 'counter', 1, metric_tags=dict(owner=cert.owner, issuer=cert.issuer))
|
||||||
return cert
|
return cert
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ from lemur.auth.service import AuthenticatedResource
|
|||||||
from lemur.auth.permissions import AuthorityPermission, CertificatePermission
|
from lemur.auth.permissions import AuthorityPermission, CertificatePermission
|
||||||
|
|
||||||
from lemur.certificates import service
|
from lemur.certificates import service
|
||||||
|
from lemur.certificates.models import Certificate
|
||||||
from lemur.plugins.base import plugins
|
from lemur.plugins.base import plugins
|
||||||
from lemur.certificates.schemas import (
|
from lemur.certificates.schemas import (
|
||||||
certificate_input_schema,
|
certificate_input_schema,
|
||||||
@ -267,7 +268,9 @@ class CertificatesList(AuthenticatedResource):
|
|||||||
if authority_permission.can():
|
if authority_permission.can():
|
||||||
data['creator'] = g.user
|
data['creator'] = g.user
|
||||||
cert = service.create(**data)
|
cert = service.create(**data)
|
||||||
log_service.create(g.user, 'create_cert', certificate=cert)
|
if isinstance(cert, Certificate):
|
||||||
|
# only log if created, not pending
|
||||||
|
log_service.create(g.user, 'create_cert', 'OK', certificate=cert)
|
||||||
return cert
|
return cert
|
||||||
|
|
||||||
return dict(message="You are not authorized to use the authority: {0}".format(data['authority'].name)), 403
|
return dict(message="You are not authorized to use the authority: {0}".format(data['authority'].name)), 403
|
||||||
|
@ -53,6 +53,19 @@ def parse_certificate(body):
|
|||||||
return x509.load_pem_x509_certificate(body, default_backend())
|
return x509.load_pem_x509_certificate(body, default_backend())
|
||||||
|
|
||||||
|
|
||||||
|
def parse_csr(csr):
|
||||||
|
"""
|
||||||
|
Helper function that parses a CSR.
|
||||||
|
|
||||||
|
:param csr:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if isinstance(csr, str):
|
||||||
|
csr = csr.encode('utf-8')
|
||||||
|
|
||||||
|
return x509.load_pem_x509_csr(csr, default_backend())
|
||||||
|
|
||||||
|
|
||||||
def get_authority_key(body):
|
def get_authority_key(body):
|
||||||
"""Returns the authority key for a given certificate in hex format"""
|
"""Returns the authority key for a given certificate in hex format"""
|
||||||
parsed_cert = parse_certificate(body)
|
parsed_cert = parse_certificate(body)
|
||||||
|
@ -21,6 +21,7 @@ from lemur.reporting.cli import manager as report_manager
|
|||||||
from lemur.endpoints.cli import manager as endpoint_manager
|
from lemur.endpoints.cli import manager as endpoint_manager
|
||||||
from lemur.certificates.cli import manager as certificate_manager
|
from lemur.certificates.cli import manager as certificate_manager
|
||||||
from lemur.notifications.cli import manager as notification_manager
|
from lemur.notifications.cli import manager as notification_manager
|
||||||
|
from lemur.pending_certificates.cli import manager as pending_certificate_manager
|
||||||
|
|
||||||
from lemur import database
|
from lemur import database
|
||||||
from lemur.users import service as user_service
|
from lemur.users import service as user_service
|
||||||
@ -44,6 +45,7 @@ from lemur.sources.models import Source # noqa
|
|||||||
from lemur.logs.models import Log # noqa
|
from lemur.logs.models import Log # noqa
|
||||||
from lemur.endpoints.models import Endpoint # noqa
|
from lemur.endpoints.models import Endpoint # noqa
|
||||||
from lemur.policies.models import RotationPolicy # noqa
|
from lemur.policies.models import RotationPolicy # noqa
|
||||||
|
from lemur.pending_certificates.models import PendingCertificate # noqa
|
||||||
|
|
||||||
|
|
||||||
manager = Manager(create_app)
|
manager = Manager(create_app)
|
||||||
@ -542,6 +544,7 @@ def main():
|
|||||||
manager.add_command("endpoint", endpoint_manager)
|
manager.add_command("endpoint", endpoint_manager)
|
||||||
manager.add_command("report", report_manager)
|
manager.add_command("report", report_manager)
|
||||||
manager.add_command("policy", policy_manager)
|
manager.add_command("policy", policy_manager)
|
||||||
|
manager.add_command("pending_certs", pending_certificate_manager)
|
||||||
manager.run()
|
manager.run()
|
||||||
|
|
||||||
|
|
||||||
|
103
lemur/migrations/versions/556ceb3e3c3e_.py
Normal file
103
lemur/migrations/versions/556ceb3e3c3e_.py
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
"""Add Pending Certificates models and relations
|
||||||
|
|
||||||
|
Revision ID: 556ceb3e3c3e
|
||||||
|
Revises: 47baffaae1a7
|
||||||
|
Create Date: 2018-01-05 01:18:45.571595
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '556ceb3e3c3e'
|
||||||
|
down_revision = '47baffaae1a7'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('pending_certs',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('external_id', sa.String(length=128), nullable=True),
|
||||||
|
sa.Column('owner', sa.String(length=128), nullable=False),
|
||||||
|
sa.Column('name', sa.String(length=256), nullable=True),
|
||||||
|
sa.Column('description', sa.String(length=1024), nullable=True),
|
||||||
|
sa.Column('notify', sa.Boolean(), nullable=True),
|
||||||
|
sa.Column('number_attempts', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('rename', sa.Boolean(), nullable=True),
|
||||||
|
sa.Column('cn', sa.String(length=128), nullable=True),
|
||||||
|
sa.Column('csr', sa.Text(), nullable=False),
|
||||||
|
sa.Column('chain', sa.Text(), nullable=True),
|
||||||
|
sa.Column('private_key', lemur.utils.Vault(), nullable=True),
|
||||||
|
sa.Column('date_created', sqlalchemy_utils.types.arrow.ArrowType(), server_default=sa.text('now()'), nullable=False),
|
||||||
|
sa.Column('status', sa.String(length=128), nullable=True),
|
||||||
|
sa.Column('rotation', sa.Boolean(), nullable=True),
|
||||||
|
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('authority_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('root_authority_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('rotation_policy_id', sa.Integer(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['authority_id'], ['authorities.id'], ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['root_authority_id'], ['authorities.id'], ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['rotation_policy_id'], ['rotation_policies.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('name')
|
||||||
|
)
|
||||||
|
op.create_table('pending_cert_destination_associations',
|
||||||
|
sa.Column('destination_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('pending_cert_id', sa.Integer(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['destination_id'], ['destinations.id'], ondelete='cascade'),
|
||||||
|
sa.ForeignKeyConstraint(['pending_cert_id'], ['pending_certs.id'], ondelete='cascade')
|
||||||
|
)
|
||||||
|
op.create_index('pending_cert_destination_associations_ix', 'pending_cert_destination_associations', ['destination_id', 'pending_cert_id'], unique=False)
|
||||||
|
op.create_table('pending_cert_notification_associations',
|
||||||
|
sa.Column('notification_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('pending_cert_id', sa.Integer(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['notification_id'], ['notifications.id'], ondelete='cascade'),
|
||||||
|
sa.ForeignKeyConstraint(['pending_cert_id'], ['pending_certs.id'], ondelete='cascade')
|
||||||
|
)
|
||||||
|
op.create_index('pending_cert_notification_associations_ix', 'pending_cert_notification_associations', ['notification_id', 'pending_cert_id'], unique=False)
|
||||||
|
op.create_table('pending_cert_replacement_associations',
|
||||||
|
sa.Column('replaced_certificate_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('pending_cert_id', sa.Integer(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['pending_cert_id'], ['pending_certs.id'], ondelete='cascade'),
|
||||||
|
sa.ForeignKeyConstraint(['replaced_certificate_id'], ['certificates.id'], ondelete='cascade')
|
||||||
|
)
|
||||||
|
op.create_index('pending_cert_replacement_associations_ix', 'pending_cert_replacement_associations', ['replaced_certificate_id', 'pending_cert_id'], unique=False)
|
||||||
|
op.create_table('pending_cert_role_associations',
|
||||||
|
sa.Column('pending_cert_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('role_id', sa.Integer(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['pending_cert_id'], ['pending_certs.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['role_id'], ['roles.id'], )
|
||||||
|
)
|
||||||
|
op.create_index('pending_cert_role_associations_ix', 'pending_cert_role_associations', ['pending_cert_id', 'role_id'], unique=False)
|
||||||
|
op.create_table('pending_cert_source_associations',
|
||||||
|
sa.Column('source_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('pending_cert_id', sa.Integer(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['pending_cert_id'], ['pending_certs.id'], ondelete='cascade'),
|
||||||
|
sa.ForeignKeyConstraint(['source_id'], ['sources.id'], ondelete='cascade')
|
||||||
|
)
|
||||||
|
op.create_index('pending_cert_source_associations_ix', 'pending_cert_source_associations', ['source_id', 'pending_cert_id'], unique=False)
|
||||||
|
op.create_table('roles_authorities',
|
||||||
|
sa.Column('authority_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('role_id', sa.Integer(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['authority_id'], ['authorities.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['role_id'], ['roles.id'], )
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_index('pending_cert_source_associations_ix', table_name='pending_cert_source_associations')
|
||||||
|
op.drop_table('pending_cert_source_associations')
|
||||||
|
op.drop_index('pending_cert_role_associations_ix', table_name='pending_cert_role_associations')
|
||||||
|
op.drop_table('pending_cert_role_associations')
|
||||||
|
op.drop_index('pending_cert_replacement_associations_ix', table_name='pending_cert_replacement_associations')
|
||||||
|
op.drop_table('pending_cert_replacement_associations')
|
||||||
|
op.drop_index('pending_cert_notification_associations_ix', table_name='pending_cert_notification_associations')
|
||||||
|
op.drop_table('pending_cert_notification_associations')
|
||||||
|
op.drop_index('pending_cert_destination_associations_ix', table_name='pending_cert_destination_associations')
|
||||||
|
op.drop_table('pending_cert_destination_associations')
|
||||||
|
op.drop_table('pending_certs')
|
||||||
|
# ### end Alembic commands ###
|
@ -83,3 +83,48 @@ policies_ciphers = db.Table('policies_ciphers',
|
|||||||
Column('policy_id', Integer, ForeignKey('policy.id')))
|
Column('policy_id', Integer, ForeignKey('policy.id')))
|
||||||
|
|
||||||
Index('policies_ciphers_ix', policies_ciphers.c.cipher_id, policies_ciphers.c.policy_id)
|
Index('policies_ciphers_ix', policies_ciphers.c.cipher_id, policies_ciphers.c.policy_id)
|
||||||
|
|
||||||
|
|
||||||
|
pending_cert_destination_associations = db.Table('pending_cert_destination_associations',
|
||||||
|
Column('destination_id', Integer,
|
||||||
|
ForeignKey('destinations.id', ondelete='cascade')),
|
||||||
|
Column('pending_cert_id', Integer,
|
||||||
|
ForeignKey('pending_certs.id', ondelete='cascade'))
|
||||||
|
)
|
||||||
|
|
||||||
|
Index('pending_cert_destination_associations_ix', pending_cert_destination_associations.c.destination_id, pending_cert_destination_associations.c.pending_cert_id)
|
||||||
|
|
||||||
|
|
||||||
|
pending_cert_notification_associations = db.Table('pending_cert_notification_associations',
|
||||||
|
Column('notification_id', Integer,
|
||||||
|
ForeignKey('notifications.id', ondelete='cascade')),
|
||||||
|
Column('pending_cert_id', Integer,
|
||||||
|
ForeignKey('pending_certs.id', ondelete='cascade'))
|
||||||
|
)
|
||||||
|
|
||||||
|
Index('pending_cert_notification_associations_ix', pending_cert_notification_associations.c.notification_id, pending_cert_notification_associations.c.pending_cert_id)
|
||||||
|
|
||||||
|
pending_cert_source_associations = db.Table('pending_cert_source_associations',
|
||||||
|
Column('source_id', Integer,
|
||||||
|
ForeignKey('sources.id', ondelete='cascade')),
|
||||||
|
Column('pending_cert_id', Integer,
|
||||||
|
ForeignKey('pending_certs.id', ondelete='cascade'))
|
||||||
|
)
|
||||||
|
|
||||||
|
Index('pending_cert_source_associations_ix', pending_cert_source_associations.c.source_id, pending_cert_source_associations.c.pending_cert_id)
|
||||||
|
|
||||||
|
pending_cert_replacement_associations = db.Table('pending_cert_replacement_associations',
|
||||||
|
Column('replaced_certificate_id', Integer,
|
||||||
|
ForeignKey('certificates.id', ondelete='cascade')),
|
||||||
|
Column('pending_cert_id', Integer,
|
||||||
|
ForeignKey('pending_certs.id', ondelete='cascade'))
|
||||||
|
)
|
||||||
|
|
||||||
|
Index('pending_cert_replacement_associations_ix', pending_cert_replacement_associations.c.replaced_certificate_id, pending_cert_replacement_associations.c.pending_cert_id)
|
||||||
|
|
||||||
|
pending_cert_role_associations = db.Table('pending_cert_role_associations',
|
||||||
|
Column('pending_cert_id', Integer, ForeignKey('pending_certs.id')),
|
||||||
|
Column('role_id', Integer, ForeignKey('roles.id'))
|
||||||
|
)
|
||||||
|
|
||||||
|
Index('pending_cert_role_associations_ix', pending_cert_role_associations.c.pending_cert_id, pending_cert_role_associations.c.role_id)
|
||||||
|
@ -11,7 +11,8 @@ from sqlalchemy_utils import JSONType
|
|||||||
|
|
||||||
from lemur.database import db
|
from lemur.database import db
|
||||||
from lemur.plugins.base import plugins
|
from lemur.plugins.base import plugins
|
||||||
from lemur.models import certificate_notification_associations
|
from lemur.models import certificate_notification_associations, \
|
||||||
|
pending_cert_notification_associations
|
||||||
|
|
||||||
|
|
||||||
class Notification(db.Model):
|
class Notification(db.Model):
|
||||||
@ -29,6 +30,13 @@ class Notification(db.Model):
|
|||||||
backref="notification",
|
backref="notification",
|
||||||
cascade='all,delete'
|
cascade='all,delete'
|
||||||
)
|
)
|
||||||
|
pending_certificates = relationship(
|
||||||
|
"PendingCertificate",
|
||||||
|
secondary=pending_cert_notification_associations,
|
||||||
|
passive_deletes=True,
|
||||||
|
backref="notification",
|
||||||
|
cascade='all,delete'
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def plugin(self):
|
def plugin(self):
|
||||||
|
0
lemur/pending_certificates/__init__.py
Normal file
0
lemur/pending_certificates/__init__.py
Normal file
48
lemur/pending_certificates/cli.py
Normal file
48
lemur/pending_certificates/cli.py
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
"""
|
||||||
|
.. module: lemur.pending_certificates.cli
|
||||||
|
|
||||||
|
.. moduleauthor:: James Chuong <jchuong@instartlogic.com>
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flask_script import Manager
|
||||||
|
|
||||||
|
from lemur.pending_certificates import service as pending_certificate_service
|
||||||
|
from lemur.plugins.base import plugins
|
||||||
|
from lemur.users import service as user_service
|
||||||
|
|
||||||
|
manager = Manager(usage="Handles pending certificate related tasks.")
|
||||||
|
|
||||||
|
|
||||||
|
@manager.option('-i', dest='ids', action='append', help='IDs of pending certificates to fetch')
|
||||||
|
def fetch(ids):
|
||||||
|
"""
|
||||||
|
Attempt to get full certificates for each pending certificate listed.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ids: a list of ids of PendingCertificates (passed in by manager options when run as CLI)
|
||||||
|
`python manager.py pending_certs fetch -i 123 321 all`
|
||||||
|
"""
|
||||||
|
new = 0
|
||||||
|
failed = 0
|
||||||
|
pending_certs = pending_certificate_service.get_pending_certs(ids)
|
||||||
|
user = user_service.get_by_username('lemur')
|
||||||
|
|
||||||
|
for cert in pending_certs:
|
||||||
|
authority = plugins.get(cert.authority.plugin_name)
|
||||||
|
real_cert = authority.get_ordered_certificate(cert.external_id)
|
||||||
|
if real_cert:
|
||||||
|
# If a real certificate was returned from issuer, then create it in Lemur and delete
|
||||||
|
# the pending certificate
|
||||||
|
pending_certificate_service.create_certificate(cert, real_cert, user)
|
||||||
|
pending_certificate_service.delete(cert)
|
||||||
|
# add metrics to metrics extension
|
||||||
|
new += 1
|
||||||
|
else:
|
||||||
|
pending_certificate_service.increment_attempt(cert)
|
||||||
|
failed += 1
|
||||||
|
print(
|
||||||
|
"[+] Certificates: New: {new} Failed: {failed}".format(
|
||||||
|
new=new,
|
||||||
|
failed=failed
|
||||||
|
)
|
||||||
|
)
|
95
lemur/pending_certificates/models.py
Normal file
95
lemur/pending_certificates/models.py
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
"""
|
||||||
|
.. module: lemur.pending_certificates.models
|
||||||
|
Copyright (c) 2017 and onwards Instart Logic, Inc. All rights reserved.
|
||||||
|
.. moduleauthor:: James Chuong <jchuong@instartlogic.com>
|
||||||
|
"""
|
||||||
|
from datetime import datetime as dt
|
||||||
|
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
from sqlalchemy import Integer, ForeignKey, String, PassiveDefault, func, Column, Text, Boolean
|
||||||
|
from sqlalchemy_utils.types.arrow import ArrowType
|
||||||
|
|
||||||
|
import lemur.common.utils
|
||||||
|
from lemur.certificates.models import get_or_increase_name
|
||||||
|
from lemur.common import defaults
|
||||||
|
from lemur.database import db
|
||||||
|
from lemur.utils import Vault
|
||||||
|
|
||||||
|
from lemur.models import pending_cert_source_associations, \
|
||||||
|
pending_cert_destination_associations, pending_cert_notification_associations, \
|
||||||
|
pending_cert_replacement_associations, pending_cert_role_associations
|
||||||
|
|
||||||
|
|
||||||
|
class PendingCertificate(db.Model):
|
||||||
|
__tablename__ = 'pending_certs'
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
external_id = Column(String(128))
|
||||||
|
owner = Column(String(128), nullable=False)
|
||||||
|
name = Column(String(256), unique=True)
|
||||||
|
description = Column(String(1024))
|
||||||
|
notify = Column(Boolean, default=True)
|
||||||
|
number_attempts = Column(Integer)
|
||||||
|
rename = Column(Boolean, default=True)
|
||||||
|
|
||||||
|
cn = Column(String(128))
|
||||||
|
csr = Column(Text(), nullable=False)
|
||||||
|
chain = Column(Text())
|
||||||
|
private_key = Column(Vault, nullable=True)
|
||||||
|
|
||||||
|
date_created = Column(ArrowType, PassiveDefault(func.now()), nullable=False)
|
||||||
|
|
||||||
|
status = Column(String(128))
|
||||||
|
|
||||||
|
rotation = Column(Boolean, default=False)
|
||||||
|
user_id = Column(Integer, ForeignKey('users.id'))
|
||||||
|
authority_id = Column(Integer, ForeignKey('authorities.id', ondelete="CASCADE"))
|
||||||
|
root_authority_id = Column(Integer, ForeignKey('authorities.id', ondelete="CASCADE"))
|
||||||
|
rotation_policy_id = Column(Integer, ForeignKey('rotation_policies.id'))
|
||||||
|
|
||||||
|
notifications = relationship('Notification', secondary=pending_cert_notification_associations, backref='pending_cert', passive_deletes=True)
|
||||||
|
destinations = relationship('Destination', secondary=pending_cert_destination_associations, backref='pending_cert', passive_deletes=True)
|
||||||
|
sources = relationship('Source', secondary=pending_cert_source_associations, backref='pending_cert', passive_deletes=True)
|
||||||
|
roles = relationship('Role', secondary=pending_cert_role_associations, backref='pending_cert', passive_deletes=True)
|
||||||
|
replaces = relationship('Certificate',
|
||||||
|
secondary=pending_cert_replacement_associations,
|
||||||
|
backref='pending_cert',
|
||||||
|
passive_deletes=True)
|
||||||
|
|
||||||
|
rotation_policy = relationship("RotationPolicy")
|
||||||
|
|
||||||
|
sensitive_fields = ('private_key',)
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self.csr = kwargs.get('csr')
|
||||||
|
self.private_key = kwargs.get('private_key', "")
|
||||||
|
if self.private_key:
|
||||||
|
# If the request does not send private key, the key exists but the value is None
|
||||||
|
self.private_key = self.private_key.strip()
|
||||||
|
self.external_id = kwargs.get('external_id')
|
||||||
|
|
||||||
|
# when destinations are appended they require a valid name.
|
||||||
|
if kwargs.get('name'):
|
||||||
|
self.name = get_or_increase_name(defaults.text_to_slug(kwargs['name']), 0)
|
||||||
|
self.rename = False
|
||||||
|
else:
|
||||||
|
# TODO: Fix auto-generated name, it should be renamed on creation
|
||||||
|
self.name = get_or_increase_name(
|
||||||
|
defaults.certificate_name(kwargs['common_name'], kwargs['authority'].name,
|
||||||
|
dt.now(), dt.now(), False), self.external_id)
|
||||||
|
self.rename = True
|
||||||
|
|
||||||
|
self.cn = defaults.common_name(lemur.common.utils.parse_csr(self.csr))
|
||||||
|
self.owner = kwargs['owner']
|
||||||
|
self.number_attempts = 0
|
||||||
|
|
||||||
|
if kwargs.get('chain'):
|
||||||
|
self.chain = kwargs['chain'].strip()
|
||||||
|
|
||||||
|
self.notify = kwargs.get('notify', True)
|
||||||
|
self.destinations = kwargs.get('destinations', [])
|
||||||
|
self.notifications = kwargs.get('notifications', [])
|
||||||
|
self.description = kwargs.get('description')
|
||||||
|
self.roles = list(set(kwargs.get('roles', [])))
|
||||||
|
self.replaces = kwargs.get('replaces', [])
|
||||||
|
self.rotation = kwargs.get('rotation')
|
||||||
|
self.rotation_policy = kwargs.get('rotation_policy')
|
101
lemur/pending_certificates/schemas.py
Normal file
101
lemur/pending_certificates/schemas.py
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
from marshmallow import fields, post_load
|
||||||
|
|
||||||
|
from lemur.schemas import (
|
||||||
|
AssociatedCertificateSchema,
|
||||||
|
AssociatedDestinationSchema,
|
||||||
|
AssociatedNotificationSchema,
|
||||||
|
AssociatedRoleSchema,
|
||||||
|
EndpointNestedOutputSchema,
|
||||||
|
ExtensionSchema
|
||||||
|
)
|
||||||
|
|
||||||
|
from lemur.common.schema import LemurInputSchema, LemurOutputSchema
|
||||||
|
from lemur.users.schemas import UserNestedOutputSchema
|
||||||
|
from lemur.authorities.schemas import AuthorityNestedOutputSchema
|
||||||
|
from lemur.certificates.schemas import CertificateNestedOutputSchema
|
||||||
|
from lemur.destinations.schemas import DestinationNestedOutputSchema
|
||||||
|
from lemur.domains.schemas import DomainNestedOutputSchema
|
||||||
|
from lemur.notifications.schemas import NotificationNestedOutputSchema
|
||||||
|
from lemur.roles.schemas import RoleNestedOutputSchema
|
||||||
|
from lemur.policies.schemas import RotationPolicyNestedOutputSchema
|
||||||
|
|
||||||
|
from lemur.notifications import service as notification_service
|
||||||
|
|
||||||
|
|
||||||
|
class PendingCertificateSchema(LemurInputSchema):
|
||||||
|
owner = fields.Email(required=True)
|
||||||
|
description = fields.String(missing='', allow_none=True)
|
||||||
|
|
||||||
|
|
||||||
|
class PendingCertificateOutputSchema(LemurOutputSchema):
|
||||||
|
id = fields.Integer()
|
||||||
|
external_id = fields.String()
|
||||||
|
csr = fields.String()
|
||||||
|
chain = fields.String()
|
||||||
|
deleted = fields.Boolean(default=False)
|
||||||
|
description = fields.String()
|
||||||
|
issuer = fields.String()
|
||||||
|
name = fields.String()
|
||||||
|
number_attempts = fields.Integer()
|
||||||
|
date_created = fields.Date()
|
||||||
|
|
||||||
|
rotation = fields.Boolean()
|
||||||
|
|
||||||
|
# Note aliasing is the first step in deprecating these fields.
|
||||||
|
notify = fields.Boolean()
|
||||||
|
active = fields.Boolean(attribute='notify')
|
||||||
|
|
||||||
|
cn = fields.String()
|
||||||
|
common_name = fields.String(attribute='cn')
|
||||||
|
|
||||||
|
owner = fields.Email()
|
||||||
|
|
||||||
|
status = fields.String()
|
||||||
|
user = fields.Nested(UserNestedOutputSchema)
|
||||||
|
|
||||||
|
extensions = fields.Nested(ExtensionSchema)
|
||||||
|
|
||||||
|
# associated objects
|
||||||
|
domains = fields.Nested(DomainNestedOutputSchema, many=True)
|
||||||
|
destinations = fields.Nested(DestinationNestedOutputSchema, many=True)
|
||||||
|
notifications = fields.Nested(NotificationNestedOutputSchema, many=True)
|
||||||
|
replaces = fields.Nested(CertificateNestedOutputSchema, many=True)
|
||||||
|
authority = fields.Nested(AuthorityNestedOutputSchema)
|
||||||
|
roles = fields.Nested(RoleNestedOutputSchema, many=True)
|
||||||
|
endpoints = fields.Nested(EndpointNestedOutputSchema, many=True, missing=[])
|
||||||
|
replaced_by = fields.Nested(CertificateNestedOutputSchema, many=True, attribute='replaced')
|
||||||
|
rotation_policy = fields.Nested(RotationPolicyNestedOutputSchema)
|
||||||
|
|
||||||
|
|
||||||
|
class PendingCertificateEditInputSchema(PendingCertificateSchema):
|
||||||
|
owner = fields.String()
|
||||||
|
|
||||||
|
notify = fields.Boolean()
|
||||||
|
rotation = fields.Boolean()
|
||||||
|
|
||||||
|
destinations = fields.Nested(AssociatedDestinationSchema, missing=[], many=True)
|
||||||
|
notifications = fields.Nested(AssociatedNotificationSchema, missing=[], many=True)
|
||||||
|
replaces = fields.Nested(AssociatedCertificateSchema, missing=[], many=True)
|
||||||
|
roles = fields.Nested(AssociatedRoleSchema, missing=[], many=True)
|
||||||
|
|
||||||
|
@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.
|
||||||
|
:param data:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if data['owner']:
|
||||||
|
notification_name = "DEFAULT_{0}".format(data['owner'].split('@')[0].upper())
|
||||||
|
data['notifications'] += notification_service.create_default_expiration_notifications(notification_name, [data['owner']])
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class PendingCertificateCancelSchema(LemurInputSchema):
|
||||||
|
note = fields.String()
|
||||||
|
|
||||||
|
|
||||||
|
pending_certificate_output_schema = PendingCertificateOutputSchema()
|
||||||
|
pending_certificate_edit_input_schema = PendingCertificateEditInputSchema()
|
||||||
|
pending_certificate_cancel_schema = PendingCertificateCancelSchema()
|
219
lemur/pending_certificates/service.py
Normal file
219
lemur/pending_certificates/service.py
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
"""
|
||||||
|
.. module: lemur.pending_certificates.service
|
||||||
|
Copyright (c) 2017 and onwards Instart Logic, Inc. All rights reserved.
|
||||||
|
.. moduleauthor:: James Chuong <jchuong@instartlogic.com>
|
||||||
|
"""
|
||||||
|
import arrow
|
||||||
|
|
||||||
|
from sqlalchemy import or_, cast, Boolean, Integer
|
||||||
|
|
||||||
|
from lemur import database
|
||||||
|
from lemur.plugins.base import plugins
|
||||||
|
|
||||||
|
from lemur.roles.models import Role
|
||||||
|
from lemur.domains.models import Domain
|
||||||
|
from lemur.authorities.models import Authority
|
||||||
|
from lemur.destinations.models import Destination
|
||||||
|
from lemur.notifications.models import Notification
|
||||||
|
from lemur.pending_certificates.models import PendingCertificate
|
||||||
|
|
||||||
|
from lemur.certificates import service as certificate_service
|
||||||
|
from lemur.users import service as user_service
|
||||||
|
|
||||||
|
from lemur.certificates.schemas import CertificateUploadInputSchema
|
||||||
|
|
||||||
|
|
||||||
|
def get(pending_cert_id):
|
||||||
|
"""
|
||||||
|
Retrieve pending certificate by ID
|
||||||
|
"""
|
||||||
|
return database.get(PendingCertificate, pending_cert_id)
|
||||||
|
|
||||||
|
|
||||||
|
def get_by_external_id(issuer, external_id):
|
||||||
|
"""
|
||||||
|
Retrieves a pending certificate by its issuer and external_id
|
||||||
|
Since external_id is not necessarily unique between CAs
|
||||||
|
|
||||||
|
:param issuer:
|
||||||
|
:param external_id:
|
||||||
|
:return: PendingCertificate or None
|
||||||
|
"""
|
||||||
|
if isinstance(external_id, int):
|
||||||
|
external_id = str(external_id)
|
||||||
|
return PendingCertificate.query \
|
||||||
|
.filter(PendingCertificate.authority_id == issuer.id) \
|
||||||
|
.filter(PendingCertificate.external_id == external_id) \
|
||||||
|
.one_or_none()
|
||||||
|
|
||||||
|
|
||||||
|
def get_by_name(pending_cert_name):
|
||||||
|
"""
|
||||||
|
Retrieve pending certificate by name
|
||||||
|
"""
|
||||||
|
return database.get(PendingCertificate, pending_cert_name, field='name')
|
||||||
|
|
||||||
|
|
||||||
|
def delete(pending_certificate):
|
||||||
|
database.delete(pending_certificate)
|
||||||
|
|
||||||
|
|
||||||
|
def get_pending_certs(pending_ids):
|
||||||
|
"""
|
||||||
|
Retrieve a list of pending certs given a list of ids
|
||||||
|
Filters out non-existing pending certs
|
||||||
|
"""
|
||||||
|
pending_certs = []
|
||||||
|
if 'all' in pending_ids:
|
||||||
|
query = database.session_query(PendingCertificate)
|
||||||
|
return database.find_all(query, PendingCertificate, {}).all()
|
||||||
|
else:
|
||||||
|
for pending_id in pending_ids:
|
||||||
|
pending_cert = get(pending_id)
|
||||||
|
if pending_cert:
|
||||||
|
pending_certs.append(pending_cert)
|
||||||
|
return pending_certs
|
||||||
|
|
||||||
|
|
||||||
|
def create_certificate(pending_certificate, certificate, user):
|
||||||
|
"""
|
||||||
|
Create and store a certificate with pending certificate's info
|
||||||
|
Args:
|
||||||
|
pending_certificate: PendingCertificate which will populate the certificate
|
||||||
|
certificate: dict from Authority, which contains the body, chain and external id
|
||||||
|
user: User that called this function, used as 'creator' of the certificate if it does
|
||||||
|
not have an owner
|
||||||
|
"""
|
||||||
|
certificate['owner'] = pending_certificate.owner
|
||||||
|
data, errors = CertificateUploadInputSchema().load(certificate)
|
||||||
|
if errors:
|
||||||
|
raise Exception("Unable to create certificate: {reasons}".format(reasons=errors))
|
||||||
|
|
||||||
|
data.update(vars(pending_certificate))
|
||||||
|
# Copy relationships, vars doesn't copy this without explicit fields
|
||||||
|
data['notifications'] = list(pending_certificate.notifications)
|
||||||
|
data['destinations'] = list(pending_certificate.destinations)
|
||||||
|
data['sources'] = list(pending_certificate.sources)
|
||||||
|
data['roles'] = list(pending_certificate.roles)
|
||||||
|
data['replaces'] = list(pending_certificate.replaces)
|
||||||
|
data['rotation_policy'] = pending_certificate.rotation_policy
|
||||||
|
|
||||||
|
# Replace external id and chain with the one fetched from source
|
||||||
|
data['external_id'] = certificate['external_id']
|
||||||
|
data['chain'] = certificate['chain']
|
||||||
|
creator = user_service.get_by_email(pending_certificate.owner)
|
||||||
|
if not creator:
|
||||||
|
# Owner of the pending certificate is not the creator, so use the current user who called
|
||||||
|
# this as the creator (usually lemur)
|
||||||
|
creator = user
|
||||||
|
|
||||||
|
if pending_certificate.rename:
|
||||||
|
# If generating name from certificate, remove the one from pending certificate
|
||||||
|
del data['name']
|
||||||
|
data['creator'] = creator
|
||||||
|
cert = certificate_service.import_certificate(**data)
|
||||||
|
database.update(cert)
|
||||||
|
return cert
|
||||||
|
|
||||||
|
|
||||||
|
def increment_attempt(pending_certificate):
|
||||||
|
"""
|
||||||
|
Increments pending certificate attempt counter and updates it in the database.
|
||||||
|
"""
|
||||||
|
pending_certificate.number_attempts += 1
|
||||||
|
database.update(pending_certificate)
|
||||||
|
return pending_certificate.number_attempts
|
||||||
|
|
||||||
|
|
||||||
|
def update(pending_cert_id, **kwargs):
|
||||||
|
"""
|
||||||
|
Updates a pending certificate. The allowed fields are validated by
|
||||||
|
PendingCertificateEditInputSchema.
|
||||||
|
"""
|
||||||
|
pending_cert = get(pending_cert_id)
|
||||||
|
for key, value in kwargs.items():
|
||||||
|
setattr(pending_cert, key, value)
|
||||||
|
return database.update(pending_cert)
|
||||||
|
|
||||||
|
|
||||||
|
def cancel(pending_certificate, **kwargs):
|
||||||
|
"""
|
||||||
|
Cancel a pending certificate. A check should be done prior to this function to decide to
|
||||||
|
revoke the certificate or just abort cancelling.
|
||||||
|
Args:
|
||||||
|
pending_certificate: PendingCertificate to be cancelled
|
||||||
|
Returns: the pending certificate if successful, raises Exception if there was an issue
|
||||||
|
"""
|
||||||
|
plugin = plugins.get(pending_certificate.authority.plugin_name)
|
||||||
|
plugin.cancel_ordered_certificate(pending_certificate, **kwargs)
|
||||||
|
pending_certificate.status = 'Cancelled'
|
||||||
|
database.update(pending_certificate)
|
||||||
|
return pending_certificate
|
||||||
|
|
||||||
|
|
||||||
|
def render(args):
|
||||||
|
query = database.session_query(PendingCertificate)
|
||||||
|
time_range = args.pop('time_range')
|
||||||
|
destination_id = args.pop('destination_id')
|
||||||
|
notification_id = args.pop('notification_id', None)
|
||||||
|
show = args.pop('show')
|
||||||
|
# owner = args.pop('owner')
|
||||||
|
# creator = args.pop('creator') # TODO we should enabling filtering by owner
|
||||||
|
|
||||||
|
filt = args.pop('filter')
|
||||||
|
|
||||||
|
if filt:
|
||||||
|
terms = filt.split(';')
|
||||||
|
|
||||||
|
if 'issuer' in terms:
|
||||||
|
# we can't rely on issuer being correct in the cert directly so we combine queries
|
||||||
|
sub_query = database.session_query(Authority.id)\
|
||||||
|
.filter(Authority.name.ilike('%{0}%'.format(terms[1])))\
|
||||||
|
.subquery()
|
||||||
|
|
||||||
|
query = query.filter(
|
||||||
|
or_(
|
||||||
|
PendingCertificate.issuer.ilike('%{0}%'.format(terms[1])),
|
||||||
|
PendingCertificate.authority_id.in_(sub_query)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
elif 'destination' in terms:
|
||||||
|
query = query.filter(PendingCertificate.destinations.any(Destination.id == terms[1]))
|
||||||
|
elif 'notify' in filt:
|
||||||
|
query = query.filter(PendingCertificate.notify == cast(terms[1], Boolean))
|
||||||
|
elif 'active' in filt:
|
||||||
|
query = query.filter(PendingCertificate.active == terms[1])
|
||||||
|
elif 'cn' in terms:
|
||||||
|
query = query.filter(
|
||||||
|
or_(
|
||||||
|
PendingCertificate.cn.ilike('%{0}%'.format(terms[1])),
|
||||||
|
PendingCertificate.domains.any(Domain.name.ilike('%{0}%'.format(terms[1])))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif 'id' in terms:
|
||||||
|
query = query.filter(PendingCertificate.id == cast(terms[1], Integer))
|
||||||
|
else:
|
||||||
|
query = database.filter(query, PendingCertificate, terms)
|
||||||
|
|
||||||
|
if show:
|
||||||
|
sub_query = database.session_query(Role.name).filter(Role.user_id == args['user'].id).subquery()
|
||||||
|
query = query.filter(
|
||||||
|
or_(
|
||||||
|
PendingCertificate.user_id == args['user'].id,
|
||||||
|
PendingCertificate.owner.in_(sub_query)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if destination_id:
|
||||||
|
query = query.filter(PendingCertificate.destinations.any(Destination.id == destination_id))
|
||||||
|
|
||||||
|
if notification_id:
|
||||||
|
query = query.filter(PendingCertificate.notifications.any(Notification.id == notification_id))
|
||||||
|
|
||||||
|
if time_range:
|
||||||
|
to = arrow.now().replace(weeks=+time_range).format('YYYY-MM-DD')
|
||||||
|
now = arrow.now().format('YYYY-MM-DD')
|
||||||
|
query = query.filter(PendingCertificate.not_after <= to).filter(PendingCertificate.not_after >= now)
|
||||||
|
|
||||||
|
return database.sort_and_page(query, PendingCertificate, args)
|
424
lemur/pending_certificates/views.py
Normal file
424
lemur/pending_certificates/views.py
Normal file
@ -0,0 +1,424 @@
|
|||||||
|
"""
|
||||||
|
.. module: lemur.pending_certificates.views
|
||||||
|
:platform: Unix
|
||||||
|
:license: Apache, see LICENSE for more details.
|
||||||
|
.. moduleauthor:: James Chuong <jchuong@instartlogic.com>
|
||||||
|
"""
|
||||||
|
from flask import Blueprint, g, make_response, jsonify
|
||||||
|
from flask_restful import Api, reqparse
|
||||||
|
|
||||||
|
from lemur.auth.service import AuthenticatedResource
|
||||||
|
from lemur.auth.permissions import CertificatePermission
|
||||||
|
|
||||||
|
from lemur.common.schema import validate_schema
|
||||||
|
from lemur.common.utils import paginated_parser
|
||||||
|
|
||||||
|
from lemur.pending_certificates import service
|
||||||
|
from lemur.roles import service as role_service
|
||||||
|
|
||||||
|
from lemur.pending_certificates.schemas import (
|
||||||
|
pending_certificate_output_schema,
|
||||||
|
pending_certificate_edit_input_schema,
|
||||||
|
pending_certificate_cancel_schema,
|
||||||
|
)
|
||||||
|
|
||||||
|
mod = Blueprint('pending_certificates', __name__)
|
||||||
|
api = Api(mod)
|
||||||
|
|
||||||
|
|
||||||
|
class PendingCertificatesList(AuthenticatedResource):
|
||||||
|
def __init__(self):
|
||||||
|
self.reqparse = reqparse.RequestParser()
|
||||||
|
super(PendingCertificatesList, self).__init__()
|
||||||
|
|
||||||
|
@validate_schema(None, pending_certificate_output_schema)
|
||||||
|
def get(self):
|
||||||
|
"""
|
||||||
|
.. http:get:: /pending_certificates
|
||||||
|
|
||||||
|
List of pending certificates
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /pending_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
|
||||||
|
|
||||||
|
{
|
||||||
|
"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",
|
||||||
|
"notBefore": "2016-06-03T00:00:00+00:00",
|
||||||
|
"notAfter": "2018-01-12T23:59:59+00:00",
|
||||||
|
"destinations": [],
|
||||||
|
"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"
|
||||||
|
}],
|
||||||
|
"rotation": true,
|
||||||
|
"rotationPolicy": {"name": "default"},
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
|
||||||
|
:reqheader Authorization: OAuth token to authenticate
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 403: unauthenticated
|
||||||
|
|
||||||
|
"""
|
||||||
|
parser = paginated_parser.copy()
|
||||||
|
parser.add_argument('timeRange', type=int, dest='time_range', location='args')
|
||||||
|
parser.add_argument('owner', type=bool, location='args')
|
||||||
|
parser.add_argument('id', type=str, location='args')
|
||||||
|
parser.add_argument('active', type=bool, 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()
|
||||||
|
args['user'] = g.user
|
||||||
|
return service.render(args)
|
||||||
|
|
||||||
|
|
||||||
|
class PendingCertificates(AuthenticatedResource):
|
||||||
|
def __init__(self):
|
||||||
|
self.reqparse = reqparse.RequestParser()
|
||||||
|
super(PendingCertificates, self).__init__()
|
||||||
|
|
||||||
|
@validate_schema(None, pending_certificate_output_schema)
|
||||||
|
def get(self, pending_certificate_id):
|
||||||
|
"""
|
||||||
|
.. http:get:: /pending_certificates/1
|
||||||
|
|
||||||
|
One pending certificate
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /pending_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": "",
|
||||||
|
"authority": {
|
||||||
|
"active": true,
|
||||||
|
"owner": "secure@example.com",
|
||||||
|
"id": 1,
|
||||||
|
"description": "verisign test authority",
|
||||||
|
"name": "verisign"
|
||||||
|
},
|
||||||
|
"owner": "joe@example.com",
|
||||||
|
"serial": "82311058732025924142789179368889309156",
|
||||||
|
"id": 1,
|
||||||
|
"issuer": "SymantecCorporation",
|
||||||
|
"notBefore": "2016-06-03T00:00:00+00:00",
|
||||||
|
"notAfter": "2018-01-12T23:59:59+00:00",
|
||||||
|
"destinations": [],
|
||||||
|
"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"
|
||||||
|
}],
|
||||||
|
"rotation": true,
|
||||||
|
"rotationPolicy": {"name": "default"},
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
|
||||||
|
:reqheader Authorization: OAuth token to authenticate
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 403: unauthenticated
|
||||||
|
|
||||||
|
"""
|
||||||
|
return service.get(pending_certificate_id)
|
||||||
|
|
||||||
|
@validate_schema(pending_certificate_edit_input_schema, pending_certificate_output_schema)
|
||||||
|
def put(self, pending_certificate_id, data=None):
|
||||||
|
"""
|
||||||
|
.. http:put:: /pending_certificates/1
|
||||||
|
|
||||||
|
Update a pending certificate
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
PUT /pending certificates/1 HTTP/1.1
|
||||||
|
Host: example.com
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
{
|
||||||
|
"owner": "jimbob@example.com",
|
||||||
|
"active": false
|
||||||
|
"notifications": [],
|
||||||
|
"destinations": [],
|
||||||
|
"replacements": []
|
||||||
|
}
|
||||||
|
|
||||||
|
**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",
|
||||||
|
"destinations": [],
|
||||||
|
"description": null,
|
||||||
|
"deleted": null,
|
||||||
|
"notifications": [{
|
||||||
|
"id": 1
|
||||||
|
}]
|
||||||
|
"user": {
|
||||||
|
"username": "jane",
|
||||||
|
"active": true,
|
||||||
|
"email": "jane@example.com",
|
||||||
|
"id": 2
|
||||||
|
},
|
||||||
|
"active": true,
|
||||||
|
"number_attempts": 1,
|
||||||
|
"csr": "-----BEGIN CERTIFICATE REQUEST-----...",
|
||||||
|
"external_id": 12345,
|
||||||
|
"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"},
|
||||||
|
}
|
||||||
|
|
||||||
|
:reqheader Authorization: OAuth token to authenticate
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 403: unauthenticated
|
||||||
|
|
||||||
|
"""
|
||||||
|
pending_cert = service.get(pending_certificate_id)
|
||||||
|
|
||||||
|
if not pending_cert:
|
||||||
|
return dict(message="Cannot find specified pending certificate"), 404
|
||||||
|
|
||||||
|
# allow creators
|
||||||
|
if g.current_user != pending_cert.user:
|
||||||
|
owner_role = role_service.get_by_name(pending_cert.owner)
|
||||||
|
permission = CertificatePermission(owner_role, [x.name for x in pending_cert.roles])
|
||||||
|
|
||||||
|
if not permission.can():
|
||||||
|
return dict(message='You are not authorized to update this certificate'), 403
|
||||||
|
|
||||||
|
for destination in data['destinations']:
|
||||||
|
if destination.plugin.requires_key:
|
||||||
|
if not pending_cert.private_key:
|
||||||
|
return dict(
|
||||||
|
message='Unable to add destination: {0}. Certificate does not have required private key.'.format(
|
||||||
|
destination.label
|
||||||
|
)
|
||||||
|
), 400
|
||||||
|
|
||||||
|
pending_cert = service.update(pending_certificate_id, **data)
|
||||||
|
return pending_cert
|
||||||
|
|
||||||
|
@validate_schema(pending_certificate_cancel_schema, None)
|
||||||
|
def delete(self, pending_certificate_id, data=None):
|
||||||
|
"""
|
||||||
|
.. http:delete:: /pending_certificates/1
|
||||||
|
|
||||||
|
Cancel and delete a pending certificate
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
DELETE /pending certificates/1 HTTP/1.1
|
||||||
|
Host: example.com
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
{
|
||||||
|
"note": "Why I am cancelling this order"
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 204 No Content
|
||||||
|
|
||||||
|
:reqheader Authorization: OAuth token to authenticate
|
||||||
|
:statuscode 204: no error
|
||||||
|
:statuscode 401: unauthenticated
|
||||||
|
:statuscode 403: unauthorized
|
||||||
|
:statuscode 404: pending certificate id not found
|
||||||
|
:statuscode 500: internal error
|
||||||
|
"""
|
||||||
|
pending_cert = service.get(pending_certificate_id)
|
||||||
|
|
||||||
|
if not pending_cert:
|
||||||
|
return dict(message="Cannot find specified pending certificate"), 404
|
||||||
|
|
||||||
|
# allow creators
|
||||||
|
if g.current_user != pending_cert.user:
|
||||||
|
owner_role = role_service.get_by_name(pending_cert.owner)
|
||||||
|
permission = CertificatePermission(owner_role, [x.name for x in pending_cert.roles])
|
||||||
|
|
||||||
|
if not permission.can():
|
||||||
|
return dict(message='You are not authorized to update this certificate'), 403
|
||||||
|
|
||||||
|
if service.cancel(pending_cert, **data):
|
||||||
|
service.delete(pending_cert)
|
||||||
|
return('', 204)
|
||||||
|
else:
|
||||||
|
# service.cancel raises exception if there was an issue, but this will ensure something
|
||||||
|
# is relayed to user in case of something unexpected (unsuccessful update somehow).
|
||||||
|
return dict(message="Unexpected error occurred while trying to cancel this certificate"), 500
|
||||||
|
|
||||||
|
|
||||||
|
class PendingCertificatePrivateKey(AuthenticatedResource):
|
||||||
|
def __init__(self):
|
||||||
|
super(PendingCertificatePrivateKey, self).__init__()
|
||||||
|
|
||||||
|
def get(self, pending_certificate_id):
|
||||||
|
"""
|
||||||
|
.. http:get:: /pending_certificates/1/key
|
||||||
|
|
||||||
|
Retrieves the private key for a given pneding certificate
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /pending_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 ..."
|
||||||
|
}
|
||||||
|
|
||||||
|
:reqheader Authorization: OAuth token to authenticate
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 403: unauthenticated
|
||||||
|
"""
|
||||||
|
cert = service.get(pending_certificate_id)
|
||||||
|
if not cert:
|
||||||
|
return dict(message="Cannot find specified pending 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 view this key'), 403
|
||||||
|
|
||||||
|
response = make_response(jsonify(key=cert.private_key), 200)
|
||||||
|
response.headers['cache-control'] = 'private, max-age=0, no-cache, no-store'
|
||||||
|
response.headers['pragma'] = 'no-cache'
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
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(PendingCertificatePrivateKey, '/pending_certificates/<int:pending_certificate_id>/key', endpoint='privateKeyPendingCertificates')
|
@ -24,3 +24,9 @@ class IssuerPlugin(Plugin):
|
|||||||
|
|
||||||
def revoke_certificate(self, certificate, comments):
|
def revoke_certificate(self, certificate, comments):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def get_ordered_certificate(self, order_id):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def cancel_ordered_certificate(self, pending_cert, **kwargs):
|
||||||
|
raise NotImplementedError
|
||||||
|
@ -326,6 +326,39 @@ class DigiCertIssuerPlugin(IssuerPlugin):
|
|||||||
response = self.session.put(create_url, data=json.dumps({'comments': comments}))
|
response = self.session.put(create_url, data=json.dumps({'comments': comments}))
|
||||||
return handle_response(response)
|
return handle_response(response)
|
||||||
|
|
||||||
|
def get_ordered_certificate(self, order_id):
|
||||||
|
""" Retrieve a certificate via order id """
|
||||||
|
base_url = current_app.config.get('DIGICERT_URL')
|
||||||
|
try:
|
||||||
|
certificate_id = get_certificate_id(self.session, base_url, order_id)
|
||||||
|
except Exception as ex:
|
||||||
|
return None
|
||||||
|
certificate_url = "{0}/services/v2/certificate/{1}/download/format/pem_all".format(base_url, certificate_id)
|
||||||
|
end_entity, intermediate, root = pem.parse(self.session.get(certificate_url).content)
|
||||||
|
cert = {'body': "\n".join(str(end_entity).splitlines()),
|
||||||
|
'chain': "\n".join(str(intermediate).splitlines()),
|
||||||
|
'external_id': str(certificate_id)}
|
||||||
|
return cert
|
||||||
|
|
||||||
|
def cancel_ordered_certificate(self, pending_cert, **kwargs):
|
||||||
|
""" Set the certificate order to canceled """
|
||||||
|
base_url = current_app.config.get('DIGICERT_URL')
|
||||||
|
api_url = "{0}/services/v2/order/certificate/{1}/status".format(base_url, pending_cert.external_id)
|
||||||
|
payload = {
|
||||||
|
'status': 'CANCELED',
|
||||||
|
'note': kwargs.get('note')
|
||||||
|
}
|
||||||
|
response = self.session.put(api_url, data=json.dumps(payload))
|
||||||
|
if response.status_code == 404:
|
||||||
|
# not well documented by Digicert, but either the certificate does not exist or we
|
||||||
|
# don't own that order (someone else's order id!). Either way, we can just ignore it
|
||||||
|
# and have it removed from Lemur
|
||||||
|
current_app.logger.warning(
|
||||||
|
"Digicert Plugin tried to cancel pending certificate {0} but it does not exist!".format(pending_cert.name))
|
||||||
|
elif response.status_code != 204:
|
||||||
|
current_app.logger.debug("{0} code {1}".format(response.status_code, response.content))
|
||||||
|
raise Exception("Failed to cancel pending certificate {0}".format(pending_cert.name))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_authority(options):
|
def create_authority(options):
|
||||||
"""Create an authority.
|
"""Create an authority.
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import pytest
|
import pytest
|
||||||
import arrow
|
import arrow
|
||||||
import json
|
import json
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
from freezegun import freeze_time
|
from freezegun import freeze_time
|
||||||
|
|
||||||
from lemur.tests.vectors import CSR_STR
|
from lemur.tests.vectors import CSR_STR
|
||||||
@ -175,3 +177,22 @@ ghi
|
|||||||
|
|
||||||
assert cert == "-----BEGIN CERTIFICATE-----\nabc\n-----END CERTIFICATE-----"
|
assert cert == "-----BEGIN CERTIFICATE-----\nabc\n-----END CERTIFICATE-----"
|
||||||
assert intermediate == "-----BEGIN CERTIFICATE-----\ndef\n-----END CERTIFICATE-----"
|
assert intermediate == "-----BEGIN CERTIFICATE-----\ndef\n-----END CERTIFICATE-----"
|
||||||
|
|
||||||
|
|
||||||
|
@patch("lemur.pending_certificates.models.PendingCertificate")
|
||||||
|
def test_cancel_ordered_certificate(mock_pending_cert):
|
||||||
|
import requests_mock
|
||||||
|
from lemur.plugins.lemur_digicert.plugin import DigiCertIssuerPlugin
|
||||||
|
|
||||||
|
mock_pending_cert.external_id = 1234
|
||||||
|
subject = DigiCertIssuerPlugin()
|
||||||
|
adapter = requests_mock.Adapter()
|
||||||
|
adapter.register_uri('PUT', 'mock://www.digicert.com/services/v2/order/certificate/1234/status', status_code=204)
|
||||||
|
adapter.register_uri('PUT', 'mock://www.digicert.com/services/v2/order/certificate/111/status', status_code=404)
|
||||||
|
subject.session.mount('mock', adapter)
|
||||||
|
data = {'note': 'Test'}
|
||||||
|
subject.cancel_ordered_certificate(mock_pending_cert, **data)
|
||||||
|
|
||||||
|
# A non-existing order id, does not raise exception because if it doesn't exist, then it doesn't matter
|
||||||
|
mock_pending_cert.external_id = 111
|
||||||
|
subject.cancel_ordered_certificate(mock_pending_cert, **data)
|
||||||
|
@ -14,7 +14,8 @@ from sqlalchemy import Boolean, Column, Integer, String, Text, ForeignKey
|
|||||||
|
|
||||||
from lemur.database import db
|
from lemur.database import db
|
||||||
from lemur.utils import Vault
|
from lemur.utils import Vault
|
||||||
from lemur.models import roles_users, roles_authorities, roles_certificates
|
from lemur.models import roles_users, roles_authorities, roles_certificates, \
|
||||||
|
pending_cert_role_associations
|
||||||
|
|
||||||
|
|
||||||
class Role(db.Model):
|
class Role(db.Model):
|
||||||
@ -30,6 +31,7 @@ class Role(db.Model):
|
|||||||
third_party = Column(Boolean)
|
third_party = Column(Boolean)
|
||||||
users = relationship("User", secondary=roles_users, passive_deletes=True, backref="role")
|
users = relationship("User", secondary=roles_users, passive_deletes=True, backref="role")
|
||||||
certificates = relationship("Certificate", secondary=roles_certificates, backref="role")
|
certificates = relationship("Certificate", secondary=roles_certificates, backref="role")
|
||||||
|
pending_certificates = relationship("PendingCertificate", secondary=pending_cert_role_associations, backref="role")
|
||||||
|
|
||||||
sensitive_fields = ('password',)
|
sensitive_fields = ('password',)
|
||||||
|
|
||||||
|
@ -0,0 +1,25 @@
|
|||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" ng-click="exit()" aria-label="Exit"><span artia-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
<h3 class="modal-title">Cancel <span class="text-muted"><small>{{ pendingCertificate.name }}</small></span></h3>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form name="cancelForm" class="form-horizontal" role="form" novalidate>
|
||||||
|
<div class="form-group"
|
||||||
|
ng-class="{'has-error': cancelForm.note.$invalid, 'has-success': !cancelForm.note.$invalid&&cancelForm.note.$dirty}">
|
||||||
|
<label class="control-label col-sm-2">
|
||||||
|
Note for Cancelling
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<textarea name="note" ng-model="cancelOptions.note" placeholder="Reason for cancelling"
|
||||||
|
class="form-control" required></textarea>
|
||||||
|
<p ng-show="cancelForm.note.$invalid && !cancelForm.note.$pristine" class="help-block">You must give a
|
||||||
|
reason for cancelling a pending certificate order.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="submit" ng-click="cancel(pendingCertificate, cancelOptions)" ng-disabled="cancelForm.$invalid" class="btn btn-success">Cancel Certificate
|
||||||
|
</button>
|
||||||
|
<button ng-click="exit()" class="btn btn-danger">Exit</button>
|
||||||
|
</div>
|
@ -0,0 +1,29 @@
|
|||||||
|
<div class="form-group">
|
||||||
|
<label class="control-label col-sm-2">
|
||||||
|
Destinations
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" ng-model="pendingCertificate.selectedDestination" placeholder="AWS..."
|
||||||
|
uib-typeahead="destination.label for destination in destinationService.findDestinationsByName($viewValue)" typeahead-loading="loadingDestinations"
|
||||||
|
class="form-control input-md" typeahead-on-select="pendingCertificate.attachDestination($item)"
|
||||||
|
uib-tooltip="Lemur can upload certificates to any pre-defined destination"
|
||||||
|
uib-tooltip-trigger="focus" uib-tooltip-placement="top"
|
||||||
|
typeahead-wait-ms="500">
|
||||||
|
<span class="input-group-btn">
|
||||||
|
<button ng-model="destinations.show" class="btn btn-md btn-default" uib-btn-checkbox btn-checkbox-true="1" btn-checkbox-false="0">
|
||||||
|
<span class="badge">{{ pendingCertificate.destinations.length || 0 }}</span>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<table class="table">
|
||||||
|
<tr ng-repeat="destination in pendingCertificate.destinations track by $index">
|
||||||
|
<td><a class="btn btn-sm btn-info" href="#/destinations/{{ destination.id }}/pendingCertificates">{{ destination.label }}</a></td>
|
||||||
|
<td><span class="text-muted">{{ destination.description }}</span></td>
|
||||||
|
<td>
|
||||||
|
<button type="button" ng-click="pendingCertificate.removeDestination($index)" class="btn btn-danger btn-sm pull-right">Remove</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,50 @@
|
|||||||
|
<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">Edit <span class="text-muted"><small>{{ pendingCertificate.name }}</small></span></h3>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form name="editForm" class="form-horizontal" role="form" novalidate>
|
||||||
|
<div class="form-group"
|
||||||
|
ng-class="{'has-error': editForm.owner.$invalid, 'has-success': !editForm.owner.$invalid&&editForm.owner.$dirty}">
|
||||||
|
<label class="control-label col-sm-2">
|
||||||
|
Owner
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input type="email" name="owner" ng-model="pendingCertificate.owner" placeholder="owner@example.com"
|
||||||
|
class="form-control" required/>
|
||||||
|
|
||||||
|
<p ng-show="editForm.owner.$invalid && !editForm.owner.$pristine" class="help-block">Enter a valid
|
||||||
|
email.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group"
|
||||||
|
ng-class="{'has-error': editForm.description.$invalid, 'has-success': !editForm.$invalid&&editForm.description.$dirty}">
|
||||||
|
<label class="control-label col-sm-2">
|
||||||
|
Description
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<textarea name="description" ng-model="pendingCertificate.description" placeholder="Something elegant"
|
||||||
|
class="form-control" required></textarea>
|
||||||
|
<p ng-show="editForm.description.$invalid && !editForm.description.$pristine" class="help-block">You must give a
|
||||||
|
short description about this authority will be used for, this description should only include alphanumeric
|
||||||
|
characters</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="control-label col-sm-2">
|
||||||
|
Roles
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-10" ng-model="pendingCertificate" role-select></div>
|
||||||
|
</div>
|
||||||
|
<div ng-include="'angular/pending_certificates/pending_certificate/replaces.tpl.html'"></div>
|
||||||
|
<div ng-include="'angular/pending_certificates/pending_certificate/notifications.tpl.html'"></div>
|
||||||
|
|
||||||
|
<div ng-include="'angular/pending_certificates/pending_certificate/destinations.tpl.html'"></div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="submit" ng-click="save(pendingCertificate)" ng-disabled="editForm.$invalid" class="btn btn-success">Save
|
||||||
|
</button>
|
||||||
|
<button ng-click="cancel()" class="btn btn-danger">Cancel</button>
|
||||||
|
</div>
|
@ -0,0 +1,28 @@
|
|||||||
|
<div class="form-group">
|
||||||
|
<label class="control-label col-sm-2">
|
||||||
|
Notifications
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" ng-model="pendingCertificate.selectedNotification" placeholder="Email"
|
||||||
|
uib-typeahead="notification.label for notification in notificationService.findNotificationsByName($viewValue)" typeahead-loading="loadingDestinations"
|
||||||
|
class="form-control input-md" typeahead-on-select="pendingCertificate.attachNotification($item)"
|
||||||
|
uib-tooltip="By default Lemur will always notify you about this (pending) certificate through Email notifications."
|
||||||
|
uib-tooltip-trigger="focus" tooltip-placement="top" typeahead-wait-ms="500">
|
||||||
|
<span class="input-group-btn">
|
||||||
|
<button ng-model="notifications.show" class="btn btn-md btn-default" uib-btn-checkbox btn-checkbox-true="1" btn-checkbox-false="0">
|
||||||
|
<span class="badge">{{ pendingCertificate.notifications.length || 0 }}</span>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<table class="table">
|
||||||
|
<tr ng-repeat="notification in pendingCertificate.notifications track by $index">
|
||||||
|
<td><a class="btn btn-sm btn-info" href="#/notifications/{{ notification.id }}/pendingCertificates">{{ notification.label }}</a></td>
|
||||||
|
<td><span class="text-muted">{{ notification.description }}</span></td>
|
||||||
|
<td>
|
||||||
|
<button type="button" ng-click="pendingCertificate.removeNotification($index)" class="btn btn-danger btn-sm pull-right">Remove</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
72
lemur/static/app/angular/pending_certificates/pending_certificate/pending_certificate.js
vendored
Normal file
72
lemur/static/app/angular/pending_certificates/pending_certificate/pending_certificate.js
vendored
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
angular.module('lemur')
|
||||||
|
.controller('PendingCertificateEditController', function ($scope, $uibModalInstance, PendingCertificateApi, PendingCertificateService, CertificateService, DestinationService, NotificationService, toaster, editId) {
|
||||||
|
PendingCertificateApi.get(editId).then(function (pendingCertificate) {
|
||||||
|
$scope.pendingCertificate = pendingCertificate;
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.cancel = function () {
|
||||||
|
$uibModalInstance.dismiss('cancel');
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.save = function (pendingCertificate) {
|
||||||
|
PendingCertificateService.update(pendingCertificate).then(
|
||||||
|
function () {
|
||||||
|
toaster.pop({
|
||||||
|
type: 'success',
|
||||||
|
title: pendingCertificate.name,
|
||||||
|
body: 'Successfully updated!'
|
||||||
|
});
|
||||||
|
$uibModalInstance.close();
|
||||||
|
},
|
||||||
|
function (response) {
|
||||||
|
toaster.pop({
|
||||||
|
type: 'error',
|
||||||
|
title: pendingCertificate.name,
|
||||||
|
body: 'lemur-bad-request',
|
||||||
|
bodyOutputType: 'directive',
|
||||||
|
directiveData: response.data,
|
||||||
|
timeout: 100000
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.pendingCertificateService = PendingCertificateService;
|
||||||
|
$scope.certificateService = CertificateService;
|
||||||
|
$scope.destinationService = DestinationService;
|
||||||
|
$scope.notificationService = NotificationService;
|
||||||
|
})
|
||||||
|
.controller('PendingCertificateCancelController', function ($scope, $uibModalInstance, PendingCertificateApi, PendingCertificateService, toaster, cancelId) {
|
||||||
|
PendingCertificateApi.get(cancelId).then(function (pendingCertificate) {
|
||||||
|
$scope.pendingCertificate = pendingCertificate;
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.exit = function () {
|
||||||
|
$uibModalInstance.dismiss('cancel');
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.cancel = function (pendingCertificate, cancelOptions) {
|
||||||
|
PendingCertificateService.cancel(pendingCertificate, cancelOptions).then(
|
||||||
|
function () {
|
||||||
|
toaster.pop({
|
||||||
|
type: 'success',
|
||||||
|
title: pendingCertificate.name,
|
||||||
|
body: 'Successfully cancelled pending certificate!'
|
||||||
|
});
|
||||||
|
$uibModalInstance.close();
|
||||||
|
},
|
||||||
|
function (response) {
|
||||||
|
toaster.pop({
|
||||||
|
type: 'error',
|
||||||
|
title: pendingCertificate.name,
|
||||||
|
body: 'lemur-bad-request',
|
||||||
|
bodyOutputType: 'directive',
|
||||||
|
directiveData: response.data,
|
||||||
|
timeout: 100000
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
});
|
@ -0,0 +1,28 @@
|
|||||||
|
<div class="form-group">
|
||||||
|
<label class="control-label col-sm-2">
|
||||||
|
Replaces
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" ng-model="pendingCertificate.selectedReplaces" placeholder="Certificate123..."
|
||||||
|
uib-typeahead="certificate.name for certificate in certificateService.findCertificatesByName($viewValue)" typeahead-loading="loadingCertificates"
|
||||||
|
class="form-control input-md" typeahead-on-select="pendingCertificate.attachReplaces($item)"
|
||||||
|
uib-tooltip="Lemur will mark any certificates being replaced as 'inactive'"
|
||||||
|
uib-tooltip-trigger="focus" uib-tooltip-placement="top" typeahead-wait-ms="500">
|
||||||
|
<span class="input-group-btn">
|
||||||
|
<button ng-model="replaces.show" class="btn btn-md btn-default" uib-btn-checkbox btn-checkbox-true="1" btn-checkbox-false="0">
|
||||||
|
<span class="badge">{{ pendingCertificate.replaces.length || 0 }}</span>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<table class="table">
|
||||||
|
<tr ng-repeat="replaces in pendingCertificate.replaces track by $index">
|
||||||
|
<td><a class="btn btn-sm btn-info">{{ replaces.name }}</a></td>
|
||||||
|
<td><span class="text-muted">{{ replaces.description }}</span></td>
|
||||||
|
<td>
|
||||||
|
<button type="button" ng-click="pendingCertificate.removeReplaces($index)" class="btn btn-danger btn-sm pull-right">Remove</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
249
lemur/static/app/angular/pending_certificates/services.js
vendored
Normal file
249
lemur/static/app/angular/pending_certificates/services.js
vendored
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
angular.module('lemur')
|
||||||
|
.service('PendingCertificateApi', function (LemurRestangular, DomainService) {
|
||||||
|
LemurRestangular.extendModel('pending_certificates', function (obj) {
|
||||||
|
return angular.extend(obj, {
|
||||||
|
attachRole: function (role) {
|
||||||
|
this.selectedRole = null;
|
||||||
|
if (this.roles === undefined) {
|
||||||
|
this.roles = [];
|
||||||
|
}
|
||||||
|
this.roles.push(role);
|
||||||
|
},
|
||||||
|
removeRole: function (index) {
|
||||||
|
this.roles.splice(index, 1);
|
||||||
|
},
|
||||||
|
attachAuthority: function (authority) {
|
||||||
|
this.authority = authority;
|
||||||
|
this.authority.maxDate = moment(this.authority.notAfter).subtract(1, 'days').format('YYYY/MM/DD');
|
||||||
|
},
|
||||||
|
attachSubAltName: function () {
|
||||||
|
if (this.extensions === undefined) {
|
||||||
|
this.extensions = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.extensions.subAltNames === undefined) {
|
||||||
|
this.extensions.subAltNames = {'names': []};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!angular.isString(this.subAltType)) {
|
||||||
|
this.subAltType = 'DNSName';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (angular.isString(this.subAltValue) && angular.isString(this.subAltType)) {
|
||||||
|
this.extensions.subAltNames.names.push({'nameType': this.subAltType, 'value': this.subAltValue});
|
||||||
|
//this.findDuplicates();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.subAltType = null;
|
||||||
|
this.subAltValue = null;
|
||||||
|
},
|
||||||
|
removeSubAltName: function (index) {
|
||||||
|
this.extensions.subAltNames.names.splice(index, 1);
|
||||||
|
//this.findDuplicates();
|
||||||
|
},
|
||||||
|
attachCustom: function () {
|
||||||
|
if (this.extensions === undefined) {
|
||||||
|
this.extensions = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.extensions.custom === undefined) {
|
||||||
|
this.extensions.custom = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (angular.isString(this.customOid) && angular.isString(this.customEncoding) && angular.isString(this.customValue)) {
|
||||||
|
this.extensions.custom.push(
|
||||||
|
{
|
||||||
|
'oid': this.customOid,
|
||||||
|
'isCritical': this.customIsCritical || false,
|
||||||
|
'encoding': this.customEncoding,
|
||||||
|
'value': this.customValue
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.customOid = null;
|
||||||
|
this.customIsCritical = null;
|
||||||
|
this.customEncoding = null;
|
||||||
|
this.customValue = null;
|
||||||
|
},
|
||||||
|
removeCustom: function (index) {
|
||||||
|
this.extensions.custom.splice(index, 1);
|
||||||
|
},
|
||||||
|
attachDestination: function (destination) {
|
||||||
|
this.selectedDestination = null;
|
||||||
|
if (this.destinations === undefined) {
|
||||||
|
this.destinations = [];
|
||||||
|
}
|
||||||
|
this.destinations.push(destination);
|
||||||
|
},
|
||||||
|
removeDestination: function (index) {
|
||||||
|
this.destinations.splice(index, 1);
|
||||||
|
},
|
||||||
|
attachReplaces: function (replaces) {
|
||||||
|
this.selectedReplaces = null;
|
||||||
|
if (this.replaces === undefined) {
|
||||||
|
this.replaces = [];
|
||||||
|
}
|
||||||
|
this.replaces.push(replaces);
|
||||||
|
},
|
||||||
|
removeReplaces: function (index) {
|
||||||
|
this.replaces.splice(index, 1);
|
||||||
|
},
|
||||||
|
attachNotification: function (notification) {
|
||||||
|
this.selectedNotification = null;
|
||||||
|
if (this.notifications === undefined) {
|
||||||
|
this.notifications = [];
|
||||||
|
}
|
||||||
|
this.notifications.push(notification);
|
||||||
|
},
|
||||||
|
removeNotification: function (index) {
|
||||||
|
this.notifications.splice(index, 1);
|
||||||
|
},
|
||||||
|
findDuplicates: function () {
|
||||||
|
DomainService.findDomainByName(this.extensions.subAltNames[0]).then(function (domains) { //We should do a better job of searching for multiple domains
|
||||||
|
this.duplicates = domains.total;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
useTemplate: function () {
|
||||||
|
if (this.extensions === undefined) {
|
||||||
|
this.extensions = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.extensions.subAltNames === undefined) {
|
||||||
|
this.extensions.subAltNames = {'names': []};
|
||||||
|
}
|
||||||
|
|
||||||
|
var saveSubAltNames = this.extensions.subAltNames;
|
||||||
|
this.extensions = this.template.extensions;
|
||||||
|
this.extensions.subAltNames = saveSubAltNames;
|
||||||
|
},
|
||||||
|
setEncipherOrDecipher: function (value) {
|
||||||
|
if (this.extensions === undefined) {
|
||||||
|
this.extensions = {};
|
||||||
|
}
|
||||||
|
if (this.extensions.keyUsage === undefined) {
|
||||||
|
this.extensions.keyUsage = {};
|
||||||
|
}
|
||||||
|
var existingValue = this.extensions.keyUsage[value];
|
||||||
|
if (existingValue) {
|
||||||
|
// Clicked on the already-selected value
|
||||||
|
this.extensions.keyUsage.useDecipherOnly = false;
|
||||||
|
this.extensions.keyUsage.useEncipherOnly = false;
|
||||||
|
// Uncheck both radio buttons
|
||||||
|
this.encipherOrDecipher = false;
|
||||||
|
} else {
|
||||||
|
// Clicked a different value
|
||||||
|
this.extensions.keyUsage.useKeyAgreement = true;
|
||||||
|
if (value === 'useEncipherOnly') {
|
||||||
|
this.extensions.keyUsage.useDecipherOnly = false;
|
||||||
|
this.extensions.keyUsage.useEncipherOnly = true;
|
||||||
|
} else {
|
||||||
|
this.extensions.keyUsage.useEncipherOnly = false;
|
||||||
|
this.extensions.keyUsage.useDecipherOnly = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return LemurRestangular.all('pending_certificates');
|
||||||
|
})
|
||||||
|
.service('PendingCertificateService', function ($location, PendingCertificateApi, AuthorityService, AuthorityApi, LemurRestangular, DefaultService) {
|
||||||
|
var PendingCertificateService = this;
|
||||||
|
PendingCertificateService.findPendingCertificatesByName = function (filterValue) {
|
||||||
|
return PendingCertificateApi.getList({'filter[name]': filterValue})
|
||||||
|
.then(function (pendingCertificates) {
|
||||||
|
return pendingCertificates;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
PendingCertificateService.update = function (pendingCertificate) {
|
||||||
|
return LemurRestangular.copy(pendingCertificate).put();
|
||||||
|
};
|
||||||
|
|
||||||
|
PendingCertificateService.getAuthority = function (certificate) {
|
||||||
|
return certificate.customGET('authority').then(function (authority) {
|
||||||
|
certificate.authority = authority;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
PendingCertificateService.getCreator = function (certificate) {
|
||||||
|
return certificate.customGET('creator').then(function (creator) {
|
||||||
|
certificate.creator = creator;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
PendingCertificateService.getDestinations = function (certificate) {
|
||||||
|
return certificate.getList('destinations').then(function (destinations) {
|
||||||
|
certificate.destinations = destinations;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
PendingCertificateService.getNotifications = function (certificate) {
|
||||||
|
return certificate.getList('notifications').then(function (notifications) {
|
||||||
|
certificate.notifications = notifications;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
PendingCertificateService.getDomains = function (certificate) {
|
||||||
|
return certificate.getList('domains').then(function (domains) {
|
||||||
|
certificate.domains = domains;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
PendingCertificateService.getReplaces = function (certificate) {
|
||||||
|
return certificate.getList('replaces').then(function (replaces) {
|
||||||
|
certificate.replaces = replaces;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
PendingCertificateService.getDefaults = function (certificate) {
|
||||||
|
return DefaultService.get().then(function (defaults) {
|
||||||
|
if (!certificate.country) {
|
||||||
|
certificate.country = defaults.country;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!certificate.state) {
|
||||||
|
certificate.state = defaults.state;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!certificate.location) {
|
||||||
|
certificate.location = defaults.location;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!certificate.organization) {
|
||||||
|
certificate.organization = defaults.organization;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!certificate.organizationalUnit) {
|
||||||
|
certificate.organizationalUnit = defaults.organizationalUnit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!certificate.authority) {
|
||||||
|
if (!defaults.authority) {
|
||||||
|
// set the default authority
|
||||||
|
AuthorityApi.getList().then(function(authorities) {
|
||||||
|
certificate.authority = authorities[0];
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
certificate.authority = defaults.authority;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
PendingCertificateService.loadPrivateKey = function (certificate) {
|
||||||
|
return certificate.customGET('key');
|
||||||
|
};
|
||||||
|
|
||||||
|
PendingCertificateService.updateNotify = function (certificate) {
|
||||||
|
return certificate.put();
|
||||||
|
};
|
||||||
|
|
||||||
|
PendingCertificateService.cancel = function (pending_certificate, options) {
|
||||||
|
return pending_certificate.customOperation('remove', null, {}, {'Content-Type': 'application/json'}, options);
|
||||||
|
};
|
||||||
|
|
||||||
|
return PendingCertificateService;
|
||||||
|
});
|
102
lemur/static/app/angular/pending_certificates/view/view.js
vendored
Normal file
102
lemur/static/app/angular/pending_certificates/view/view.js
vendored
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
angular.module('lemur')
|
||||||
|
|
||||||
|
.config(function config($stateProvider) {
|
||||||
|
$stateProvider
|
||||||
|
.state('pending_certificates', {
|
||||||
|
url: '/pending_certificates',
|
||||||
|
templateUrl: '/angular/pending_certificates/view/view.tpl.html',
|
||||||
|
controller: 'PendingCertificatesViewController'
|
||||||
|
})
|
||||||
|
.state('pending_certificate', {
|
||||||
|
url: '/pending_certificates/:name',
|
||||||
|
templateUrl: '/angular/pending_certificates/view/view.tpl.html',
|
||||||
|
controller: 'PendingCertificatesViewController'
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
.controller('PendingCertificatesViewController', function ($q, $scope, $uibModal, $stateParams, PendingCertificateApi, PendingCertificateService, ngTableParams, toaster) {
|
||||||
|
$scope.filter = $stateParams;
|
||||||
|
$scope.pendingCertificateTable = new ngTableParams({
|
||||||
|
page: 1, // show first page
|
||||||
|
count: 10, // count per page
|
||||||
|
sorting: {
|
||||||
|
id: 'desc' // initial sorting
|
||||||
|
},
|
||||||
|
filter: $scope.filter
|
||||||
|
}, {
|
||||||
|
total: 0, // length of data
|
||||||
|
getData: function ($defer, params) {
|
||||||
|
PendingCertificateApi.getList(params.url())
|
||||||
|
.then(function (data) {
|
||||||
|
params.total(data.total);
|
||||||
|
$defer.resolve(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.edit = function (pendingCertificateId) {
|
||||||
|
var uibModalInstance = $uibModal.open({
|
||||||
|
animation: true,
|
||||||
|
controller: 'PendingCertificateEditController',
|
||||||
|
templateUrl: '/angular/pending_certificates/pending_certificate/edit.tpl.html',
|
||||||
|
size: 'lg',
|
||||||
|
backdrop: 'static',
|
||||||
|
resolve: {
|
||||||
|
editId: function () {
|
||||||
|
return pendingCertificateId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
uibModalInstance.result.then(function () {
|
||||||
|
$scope.pendingCertificateTable.reload();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.loadPrivateKey = function (pendingCertificate) {
|
||||||
|
if (pendingCertificate.privateKey !== undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PendingCertificateService.loadPrivateKey(pendingCertificate).then(
|
||||||
|
function (response) {
|
||||||
|
if (response.key === null) {
|
||||||
|
toaster.pop({
|
||||||
|
type: 'warning',
|
||||||
|
title: pendingCertificate.name,
|
||||||
|
body: 'No private key found!'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
pendingCertificate.privateKey = response.key;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
function () {
|
||||||
|
toaster.pop({
|
||||||
|
type: 'error',
|
||||||
|
title: pendingCertificate.name,
|
||||||
|
body: 'You do not have permission to view this key!',
|
||||||
|
timeout: 100000
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.cancel = function (pendingCertificateId) {
|
||||||
|
var uibModalInstance = $uibModal.open({
|
||||||
|
animation: true,
|
||||||
|
controller: 'PendingCertificateCancelController',
|
||||||
|
templateUrl: '/angular/pending_certificates/pending_certificate/cancel.tpl.html',
|
||||||
|
size: 'lg',
|
||||||
|
backdrop: 'static',
|
||||||
|
resolve: {
|
||||||
|
cancelId: function () {
|
||||||
|
return pendingCertificateId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
uibModalInstance.result.then(function () {
|
||||||
|
$scope.pendingCertificateTable.reload();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
});
|
182
lemur/static/app/angular/pending_certificates/view/view.tpl.html
Normal file
182
lemur/static/app/angular/pending_certificates/view/view.tpl.html
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<h2 class="featurette-heading">Pending Certificates
|
||||||
|
<span class="text-muted"><small>Certificates that almost made it</small></span></h2>
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<div class="btn-group">
|
||||||
|
<button ng-model="showFilter" class="btn btn-default" uib-btn-checkbox
|
||||||
|
btn-checkbox-true="1"
|
||||||
|
btn-checkbox-false="0">Filter</button>
|
||||||
|
</div>
|
||||||
|
<!--<select class="form-control" ng-model="show" ng-options="item.value as item.title for item in fields"></select>-->
|
||||||
|
<div class="clearfix"></div>
|
||||||
|
</div>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table ng-table="pendingCertificateTable" class="table" show-filter="showFilter" template-pagination="angular/pager.html">
|
||||||
|
<tbody>
|
||||||
|
<tr ng-class="{'even-row': $even }" ng-repeat-start="pendingCertificate in $data track by $index">
|
||||||
|
<td data-title="'Id'" filter="{'id': 'text'}">
|
||||||
|
{{ pendingCertificate.id }}
|
||||||
|
</td>
|
||||||
|
<td data-title="'Name'" sortable="'name'" filter="{ 'name': 'text' }">
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<li>{{ pendingCertificate.name }}</li>
|
||||||
|
<li><span class="text-muted">{{ pendingCertificate.owner }}</span></li>
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
<td data-title="'Notify'" filter="{ 'notify': 'select' }" filter-data="getPendingCertificateStatus()">
|
||||||
|
<form>
|
||||||
|
<switch ng-change="updateNotify(pendingCertificate)" id="status" name="status"
|
||||||
|
ng-model="pendingCertificate.notify" class="green small"></switch>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
<td data-title="'Issuer'" sortable="'issuer'" filter="{ 'issuer': 'text' }">
|
||||||
|
{{ pendingCertificate.authority.name || pendingCertificate.issuer }}
|
||||||
|
</td>
|
||||||
|
<td data-title="'Common Name'" filter="{ 'cn': 'text'}">
|
||||||
|
{{ pendingCertificate.cn }}
|
||||||
|
</td>
|
||||||
|
<td data-title="''" style="text-align: center; vertical-align: middle;">
|
||||||
|
<div class="btn-group pull-right" role="group" aria-label="...">
|
||||||
|
<a class="btn btn-sm btn-primary" ui-sref="pending_certificate({name: pendingCertificate.name})">Permalink</a>
|
||||||
|
<button ng-model="pendingCertificate.toggle" class="btn btn-sm btn-info" uib-btn-checkbox btn-checkbox-true="1"
|
||||||
|
btn-checkbox-false="0">More
|
||||||
|
</button>
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<button type="button" class="btn btn-sm btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||||
|
Action
|
||||||
|
<span class="caret"></span>
|
||||||
|
</button>
|
||||||
|
<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>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="warning" ng-if="pendingCertificate.toggle" ng-repeat-end>
|
||||||
|
<td colspan="12">
|
||||||
|
<uib-tabset justified="true" class="col-md-8">
|
||||||
|
<uib-tab>
|
||||||
|
<uib-tab-heading>Basic Info</uib-tab-heading>
|
||||||
|
<ul class="list-group">
|
||||||
|
<li class="list-group-item">
|
||||||
|
<strong>Creator</strong>
|
||||||
|
<span class="pull-right">
|
||||||
|
{{ pendingCertificate.user.email }}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item">
|
||||||
|
<strong>Owner</strong>
|
||||||
|
<span class="pull-right">
|
||||||
|
{{ pendingCertificate.owner }}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item">
|
||||||
|
<strong>Number of Attempts to Fetch</strong>
|
||||||
|
<span class="pull-right">
|
||||||
|
{{ pendingCertificate.numberAttempts }}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item">
|
||||||
|
<strong>Date Created</strong>
|
||||||
|
<span class="pull-right">
|
||||||
|
{{ pendingCertificate.dateCreated | date:'yyyy-MM-dd HH:mm:ss Z' }}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="list-group-item">
|
||||||
|
<strong>Description</strong>
|
||||||
|
<p>{{ pendingCertificate.description }}</p>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</uib-tab>
|
||||||
|
<uib-tab>
|
||||||
|
<uib-tab-heading>Endpoints</uib-tab-heading>
|
||||||
|
<ul class="list-group">
|
||||||
|
<li class="list-group-item" ng-repeat="endpoint in pendingCertificate.endpoints">
|
||||||
|
<span class="pull-right"><label class="label label-default">{{ endpoint.type }}</label></span>
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<li>{{ endpoint.name }}</li>
|
||||||
|
<li><span class="text-muted">{{ endpoint.dnsname }}</span></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</uib-tab>
|
||||||
|
<uib-tab>
|
||||||
|
<uib-tab-heading>Notifications</uib-tab-heading>
|
||||||
|
<ul class="list-group">
|
||||||
|
<li class="list-group-item" ng-repeat="notification in pendingCertificate.notifications">
|
||||||
|
<strong>{{ notification.label }}</strong>
|
||||||
|
<span class="pull-right">{{ notification.description}}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</uib-tab>
|
||||||
|
<uib-tab>
|
||||||
|
<uib-tab-heading>Roles</uib-tab-heading>
|
||||||
|
<ul class="list-group">
|
||||||
|
<li class="list-group-item" ng-repeat="role in pendingCertificate.roles">
|
||||||
|
<strong>{{ role.name }}</strong>
|
||||||
|
<span class="pull-right">{{ role.description}}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</uib-tab>
|
||||||
|
<uib-tab>
|
||||||
|
<uib-tab-heading>Destinations</uib-tab-heading>
|
||||||
|
<ul class="list-group">
|
||||||
|
<li class="list-group-item" ng-repeat="destination in pendingCertificate.destinations">
|
||||||
|
<strong>{{ destination.label }}</strong>
|
||||||
|
<span class="pull-right">{{ destination.description }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</uib-tab>
|
||||||
|
<uib-tab>
|
||||||
|
<uib-tab-heading>Domains</uib-tab-heading>
|
||||||
|
<div class="list-group">
|
||||||
|
<a href="#/domains/{{ domain.id }}" class="list-group-item"
|
||||||
|
ng-repeat="domain in pendingCertificate.domains">{{ domain.name }}</a>
|
||||||
|
</div>
|
||||||
|
</uib-tab>
|
||||||
|
<uib-tab>
|
||||||
|
<uib-tab-heading>Replaces</uib-tab-heading>
|
||||||
|
<ul class="list-group">
|
||||||
|
<li class="list-group-item" ng-repeat="replaces in pendingCertificate.replaces">
|
||||||
|
<strong>{{ replaces.name }}</strong>
|
||||||
|
<p>{{ replaces.description }}</p>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</uib-tab>
|
||||||
|
</uib-tabset>
|
||||||
|
<uib-tabset justified="true" class="col-md-4">
|
||||||
|
<uib-tab>
|
||||||
|
<uib-tab-heading>
|
||||||
|
Chain
|
||||||
|
<i class="glyphicon glyphicon-copy" style="cursor: pointer" clipboard text="pendingCertificate.chain"></i>
|
||||||
|
</uib-tab-heading>
|
||||||
|
<pre style="width: 100%">{{ pendingCertificate.chain }}</pre>
|
||||||
|
</uib-tab>
|
||||||
|
<uib-tab>
|
||||||
|
<uib-tab-heading>
|
||||||
|
Certificate Signing Request
|
||||||
|
<i class="glyphicon glyphicon-copy" style="cursor: pointer" clipboard text="pendingCertificate.csr"></i>
|
||||||
|
</uib-tab-heading>
|
||||||
|
<pre style="width: 100%">{{ pendingCertificate.csr }}</pre>
|
||||||
|
</uib-tab>
|
||||||
|
<uib-tab ng-click="loadPrivateKey(pendingCertificate)">
|
||||||
|
<uib-tab-heading>
|
||||||
|
Private Key
|
||||||
|
<i class="glyphicon glyphicon-copy" style="cursor: pointer" clipboard text="pendingCertificate.privateKey"></i>
|
||||||
|
</uib-tab-heading>
|
||||||
|
<pre style="width: 100%">{{ pendingCertificate.privateKey }}</pre>
|
||||||
|
</uib-tab>
|
||||||
|
</uib-tabset>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -50,6 +50,7 @@
|
|||||||
<ul class="nav navbar-nav navbar-left">
|
<ul class="nav navbar-nav navbar-left">
|
||||||
<li><a ui-sref="dashboard">Dashboard</a></li>
|
<li><a ui-sref="dashboard">Dashboard</a></li>
|
||||||
<li><a ui-sref="certificates">Certificates</a></li>
|
<li><a ui-sref="certificates">Certificates</a></li>
|
||||||
|
<li><a ui-sref="pending_certificates">Pending Certificates</a></li>
|
||||||
<li><a ui-sref="authorities">Authorities</a></li>
|
<li><a ui-sref="authorities">Authorities</a></li>
|
||||||
<li><a ui-sref="endpoints">Endpoints</a></li>
|
<li><a ui-sref="endpoints">Endpoints</a></li>
|
||||||
<li><a ui-sref="notifications">Notifications</a></li>
|
<li><a ui-sref="notifications">Notifications</a></li>
|
||||||
|
@ -14,7 +14,8 @@ from lemur.auth.service import create_token
|
|||||||
from lemur.tests.vectors import PRIVATE_KEY_STR
|
from lemur.tests.vectors import PRIVATE_KEY_STR
|
||||||
|
|
||||||
from .factories import ApiKeyFactory, AuthorityFactory, NotificationFactory, DestinationFactory, \
|
from .factories import ApiKeyFactory, AuthorityFactory, NotificationFactory, DestinationFactory, \
|
||||||
CertificateFactory, UserFactory, RoleFactory, SourceFactory, EndpointFactory, RotationPolicyFactory
|
CertificateFactory, UserFactory, RoleFactory, SourceFactory, EndpointFactory, \
|
||||||
|
RotationPolicyFactory, PendingCertificateFactory, AsyncAuthorityFactory
|
||||||
|
|
||||||
|
|
||||||
def pytest_runtest_setup(item):
|
def pytest_runtest_setup(item):
|
||||||
@ -90,6 +91,13 @@ def authority(session):
|
|||||||
return a
|
return a
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def async_authority(session):
|
||||||
|
a = AsyncAuthorityFactory()
|
||||||
|
session.commit()
|
||||||
|
return a
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def destination(session):
|
def destination(session):
|
||||||
d = DestinationFactory()
|
d = DestinationFactory()
|
||||||
@ -144,6 +152,15 @@ def user(session):
|
|||||||
return {'user': u, 'token': token}
|
return {'user': u, 'token': token}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def pending_certificate(session):
|
||||||
|
u = UserFactory()
|
||||||
|
a = AsyncAuthorityFactory()
|
||||||
|
p = PendingCertificateFactory(user=u, authority=a)
|
||||||
|
session.commit()
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def admin_user(session):
|
def admin_user(session):
|
||||||
u = UserFactory()
|
u = UserFactory()
|
||||||
@ -155,6 +172,14 @@ def admin_user(session):
|
|||||||
return {'user': u, 'token': token}
|
return {'user': u, 'token': token}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def async_issuer_plugin():
|
||||||
|
from lemur.plugins.base import register
|
||||||
|
from .plugins.issuer_plugin import TestAsyncIssuerPlugin
|
||||||
|
register(TestAsyncIssuerPlugin)
|
||||||
|
return TestAsyncIssuerPlugin
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def issuer_plugin():
|
def issuer_plugin():
|
||||||
from lemur.plugins.base import register
|
from lemur.plugins.base import register
|
||||||
|
@ -12,13 +12,14 @@ from lemur.certificates.models import Certificate
|
|||||||
from lemur.destinations.models import Destination
|
from lemur.destinations.models import Destination
|
||||||
from lemur.sources.models import Source
|
from lemur.sources.models import Source
|
||||||
from lemur.notifications.models import Notification
|
from lemur.notifications.models import Notification
|
||||||
|
from lemur.pending_certificates.models import PendingCertificate
|
||||||
from lemur.users.models import User
|
from lemur.users.models import User
|
||||||
from lemur.roles.models import Role
|
from lemur.roles.models import Role
|
||||||
from lemur.endpoints.models import Policy, Endpoint
|
from lemur.endpoints.models import Policy, Endpoint
|
||||||
from lemur.policies.models import RotationPolicy
|
from lemur.policies.models import RotationPolicy
|
||||||
from lemur.api_keys.models import ApiKey
|
from lemur.api_keys.models import ApiKey
|
||||||
|
|
||||||
from .vectors import INTERNAL_VALID_SAN_STR, PRIVATE_KEY_STR
|
from .vectors import INTERNAL_VALID_SAN_STR, PRIVATE_KEY_STR, CSR_STR
|
||||||
|
|
||||||
|
|
||||||
class BaseFactory(SQLAlchemyModelFactory):
|
class BaseFactory(SQLAlchemyModelFactory):
|
||||||
@ -140,6 +141,15 @@ class AuthorityFactory(BaseFactory):
|
|||||||
self.roles.append(role)
|
self.roles.append(role)
|
||||||
|
|
||||||
|
|
||||||
|
class AsyncAuthorityFactory(AuthorityFactory):
|
||||||
|
"""Async Authority factory."""
|
||||||
|
name = Sequence(lambda n: 'authority{0}'.format(n))
|
||||||
|
owner = 'joe@example.com'
|
||||||
|
plugin = {'slug': 'test-issuer-async'}
|
||||||
|
description = FuzzyText(length=128)
|
||||||
|
authority_certificate = SubFactory(CertificateFactory)
|
||||||
|
|
||||||
|
|
||||||
class RotationPolicyFactory(BaseFactory):
|
class RotationPolicyFactory(BaseFactory):
|
||||||
"""Rotation Factory."""
|
"""Rotation Factory."""
|
||||||
name = Sequence(lambda n: 'policy{0}'.format(n))
|
name = Sequence(lambda n: 'policy{0}'.format(n))
|
||||||
@ -204,6 +214,7 @@ class UserFactory(BaseFactory):
|
|||||||
email = Sequence(lambda n: 'user{0}@example.com'.format(n))
|
email = Sequence(lambda n: 'user{0}@example.com'.format(n))
|
||||||
active = True
|
active = True
|
||||||
password = FuzzyText(length=24)
|
password = FuzzyText(length=24)
|
||||||
|
certificates = []
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Factory Configuration."""
|
"""Factory Configuration."""
|
||||||
@ -281,3 +292,93 @@ class ApiKeyFactory(BaseFactory):
|
|||||||
|
|
||||||
if extracted:
|
if extracted:
|
||||||
self.userId = extracted.id
|
self.userId = extracted.id
|
||||||
|
|
||||||
|
|
||||||
|
class PendingCertificateFactory(BaseFactory):
|
||||||
|
"""PendingCertificate factory."""
|
||||||
|
name = Sequence(lambda n: 'pending_certificate{0}'.format(n))
|
||||||
|
external_id = 12345
|
||||||
|
csr = CSR_STR
|
||||||
|
chain = INTERNAL_VALID_SAN_STR
|
||||||
|
private_key = PRIVATE_KEY_STR
|
||||||
|
owner = 'joe@example.com'
|
||||||
|
status = FuzzyChoice(['valid', 'revoked', 'unknown'])
|
||||||
|
deleted = False
|
||||||
|
description = FuzzyText(length=128)
|
||||||
|
date_created = FuzzyDate(date(2016, 1, 1), date(2020, 1, 1))
|
||||||
|
number_attempts = 0
|
||||||
|
rename = False
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""Factory Configuration."""
|
||||||
|
model = PendingCertificate
|
||||||
|
|
||||||
|
@post_generation
|
||||||
|
def user(self, create, extracted, **kwargs):
|
||||||
|
if not create:
|
||||||
|
return
|
||||||
|
|
||||||
|
if extracted:
|
||||||
|
self.user_id = extracted.id
|
||||||
|
|
||||||
|
@post_generation
|
||||||
|
def authority(self, create, extracted, **kwargs):
|
||||||
|
if not create:
|
||||||
|
return
|
||||||
|
|
||||||
|
if extracted:
|
||||||
|
self.authority_id = extracted.id
|
||||||
|
|
||||||
|
@post_generation
|
||||||
|
def notifications(self, create, extracted, **kwargs):
|
||||||
|
if not create:
|
||||||
|
return
|
||||||
|
|
||||||
|
if extracted:
|
||||||
|
for notification in extracted:
|
||||||
|
self.notifications.append(notification)
|
||||||
|
|
||||||
|
@post_generation
|
||||||
|
def destinations(self, create, extracted, **kwargs):
|
||||||
|
if not create:
|
||||||
|
return
|
||||||
|
|
||||||
|
if extracted:
|
||||||
|
for destination in extracted:
|
||||||
|
self.destintations.append(destination)
|
||||||
|
|
||||||
|
@post_generation
|
||||||
|
def replaces(self, create, extracted, **kwargs):
|
||||||
|
if not create:
|
||||||
|
return
|
||||||
|
|
||||||
|
if extracted:
|
||||||
|
for replace in extracted:
|
||||||
|
self.replaces.append(replace)
|
||||||
|
|
||||||
|
@post_generation
|
||||||
|
def sources(self, create, extracted, **kwargs):
|
||||||
|
if not create:
|
||||||
|
return
|
||||||
|
|
||||||
|
if extracted:
|
||||||
|
for source in extracted:
|
||||||
|
self.sources.append(source)
|
||||||
|
|
||||||
|
@post_generation
|
||||||
|
def domains(self, create, extracted, **kwargs):
|
||||||
|
if not create:
|
||||||
|
return
|
||||||
|
|
||||||
|
if extracted:
|
||||||
|
for domain in extracted:
|
||||||
|
self.domains.append(domain)
|
||||||
|
|
||||||
|
@post_generation
|
||||||
|
def roles(self, create, extracted, **kwargs):
|
||||||
|
if not create:
|
||||||
|
return
|
||||||
|
|
||||||
|
if extracted:
|
||||||
|
for domain in extracted:
|
||||||
|
self.roles.append(domain)
|
||||||
|
@ -21,3 +21,29 @@ class TestIssuerPlugin(IssuerPlugin):
|
|||||||
def create_authority(options):
|
def create_authority(options):
|
||||||
role = {'username': '', 'password': '', 'name': 'test'}
|
role = {'username': '', 'password': '', 'name': 'test'}
|
||||||
return INTERNAL_VALID_SAN_STR, "", [role]
|
return INTERNAL_VALID_SAN_STR, "", [role]
|
||||||
|
|
||||||
|
|
||||||
|
class TestAsyncIssuerPlugin(IssuerPlugin):
|
||||||
|
title = 'Test Async'
|
||||||
|
slug = 'test-issuer-async'
|
||||||
|
description = 'Enables testing with pending certificates'
|
||||||
|
|
||||||
|
author = 'James Chuong'
|
||||||
|
author_url = 'https://github.com/jchuong'
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(TestAsyncIssuerPlugin, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def create_certificate(self, csr, issuer_options):
|
||||||
|
return "", "", 12345
|
||||||
|
|
||||||
|
def get_ordered_certificate(self, order_id):
|
||||||
|
return INTERNAL_VALID_LONG_STR, INTERNAL_VALID_SAN_STR, 54321
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_authority(options):
|
||||||
|
role = {'username': '', 'password': '', 'name': 'test'}
|
||||||
|
return INTERNAL_VALID_SAN_STR, "", [role]
|
||||||
|
|
||||||
|
def cancel_ordered_certificate(self, pending_certificate, **kwargs):
|
||||||
|
return True
|
||||||
|
@ -436,7 +436,7 @@ def test_get_account_number(client):
|
|||||||
|
|
||||||
def test_mint_certificate(issuer_plugin, authority):
|
def test_mint_certificate(issuer_plugin, authority):
|
||||||
from lemur.certificates.service import mint
|
from lemur.certificates.service import mint
|
||||||
cert_body, private_key, chain, external_id = mint(authority=authority, csr=CSR_STR)
|
cert_body, private_key, chain, external_id, csr = mint(authority=authority, csr=CSR_STR)
|
||||||
assert cert_body == INTERNAL_VALID_LONG_STR, INTERNAL_VALID_SAN_STR
|
assert cert_body == INTERNAL_VALID_LONG_STR, INTERNAL_VALID_SAN_STR
|
||||||
|
|
||||||
|
|
||||||
|
51
lemur/tests/test_pending_certificates.py
Normal file
51
lemur/tests/test_pending_certificates.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from .vectors import CSR_STR, INTERNAL_VALID_LONG_STR, VALID_ADMIN_API_TOKEN, VALID_ADMIN_HEADER_TOKEN, VALID_USER_HEADER_TOKEN
|
||||||
|
|
||||||
|
from lemur.pending_certificates.views import * # noqa
|
||||||
|
|
||||||
|
|
||||||
|
def test_increment_attempt(pending_certificate):
|
||||||
|
from lemur.pending_certificates.service import increment_attempt
|
||||||
|
initial_attempt = pending_certificate.number_attempts
|
||||||
|
attempts = increment_attempt(pending_certificate)
|
||||||
|
assert attempts == initial_attempt + 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_pending_certificate(async_issuer_plugin, async_authority, user):
|
||||||
|
from lemur.certificates.service import create
|
||||||
|
pending_cert = create(authority=async_authority, csr=CSR_STR, owner='joe@example.com', creator=user['user'], common_name='ACommonName')
|
||||||
|
assert pending_cert.external_id == '12345'
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_pending(pending_certificate, user, session):
|
||||||
|
import copy
|
||||||
|
from lemur.pending_certificates.service import create_certificate, get
|
||||||
|
cert = {'body': INTERNAL_VALID_LONG_STR,
|
||||||
|
'chain': None,
|
||||||
|
'external_id': 54321}
|
||||||
|
|
||||||
|
# Weird copy because the session behavior. pending_certificate is a valid object but the
|
||||||
|
# return of vars(pending_certificate) is a sessionobject, and so nothing from the pending_cert
|
||||||
|
# is used to create the certificate. Maybe a bug due to using vars(), and should copy every
|
||||||
|
# field explicitly.
|
||||||
|
pending_certificate = copy.copy(get(pending_certificate.id))
|
||||||
|
real_cert = create_certificate(pending_certificate, cert, user['user'])
|
||||||
|
assert real_cert.owner == pending_certificate.owner
|
||||||
|
assert real_cert.notify == pending_certificate.notify
|
||||||
|
assert real_cert.private_key == pending_certificate.private_key
|
||||||
|
assert real_cert.external_id == '54321'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("token,status", [
|
||||||
|
(VALID_USER_HEADER_TOKEN, 403),
|
||||||
|
(VALID_ADMIN_HEADER_TOKEN, 204),
|
||||||
|
(VALID_ADMIN_API_TOKEN, 204),
|
||||||
|
('', 401)
|
||||||
|
])
|
||||||
|
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
|
@ -43,6 +43,7 @@ class User(db.Model):
|
|||||||
profile_picture = Column(String(255))
|
profile_picture = Column(String(255))
|
||||||
roles = relationship('Role', secondary=roles_users, passive_deletes=True, backref=db.backref('user'), lazy='dynamic')
|
roles = relationship('Role', secondary=roles_users, passive_deletes=True, backref=db.backref('user'), lazy='dynamic')
|
||||||
certificates = relationship('Certificate', backref=db.backref('user'), lazy='dynamic')
|
certificates = relationship('Certificate', backref=db.backref('user'), lazy='dynamic')
|
||||||
|
pending_certificates = relationship('PendingCertificate', backref=db.backref('user'), lazy='dynamic')
|
||||||
authorities = relationship('Authority', backref=db.backref('user'), lazy='dynamic')
|
authorities = relationship('Authority', backref=db.backref('user'), lazy='dynamic')
|
||||||
keys = relationship('ApiKey', backref=db.backref('user'), lazy='dynamic')
|
keys = relationship('ApiKey', backref=db.backref('user'), lazy='dynamic')
|
||||||
logs = relationship('Log', backref=db.backref('user'), lazy='dynamic')
|
logs = relationship('Log', backref=db.backref('user'), lazy='dynamic')
|
||||||
|
Loading…
Reference in New Issue
Block a user