Merge branch 'master' into unq-const

This commit is contained in:
Curtis 2018-02-26 08:03:31 -08:00 committed by GitHub
commit 7032abf2e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 2251 additions and 31 deletions

View File

@ -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
----------- -----------

View File

@ -1,3 +1,4 @@
CloudFlare==1.7.5
Flask==0.12 Flask==0.12
Flask-RESTful==0.3.6 Flask-RESTful==0.3.6
Flask-SQLAlchemy==2.1 Flask-SQLAlchemy==2.1

View File

@ -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,
) )

View File

@ -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', [])

View File

@ -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")

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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()

View 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 ###

View File

@ -85,3 +85,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)

View File

@ -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):

View File

View 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
)
)

View 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')

View 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()

View 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)

View 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')

View File

@ -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

View File

@ -0,0 +1,76 @@
import time
import CloudFlare
from flask import current_app
def cf_api_call():
cf_key = current_app.config.get('ACME_CLOUDFLARE_KEY', '')
cf_email = current_app.config.get('ACME_CLOUDFLARE_EMAIL', '')
return CloudFlare.CloudFlare(email=cf_email, token=cf_key)
def find_zone_id(host):
elements = host.split('.')
cf = cf_api_call()
n = 1
while n < 5:
n = n + 1
domain = '.'.join(elements[-n:])
current_app.logger.debug("Trying to get ID for zone {0}".format(domain))
try:
zone = cf.zones.get(params={'name': domain, 'per_page': 1})
except Exception as e:
current_app.logger.error("Cloudflare API error: %s" % e)
pass
if len(zone) == 1:
break
if len(zone) == 0:
current_app.logger.error('No zone found')
return
else:
return zone[0]['id']
def wait_for_dns_change(change_id, account_number=None):
cf = cf_api_call()
zone_id, record_id = change_id
while True:
r = cf.zones.get(zone_id, record_id)
current_app.logger.debug("Record status: %s" % r['status'])
if r['status'] == 'active':
break
time.sleep(1)
return
def create_txt_record(host, value, account_number):
cf = cf_api_call()
zone_id = find_zone_id(host)
if not zone_id:
return
txt_record = {'name': host, 'type': 'TXT', 'content': value}
current_app.logger.debug("Creating TXT record {0} with value {1}".format(host, value))
try:
r = cf.zones.dns_records.post(zone_id, data=txt_record)
except Exception as e:
current_app.logger.error('/zones.dns_records.post %s: %s' % (txt_record['name'], e))
return zone_id, r['id']
def delete_txt_record(change_id, account_number, host, value):
cf = cf_api_call()
zone_id, record_id = change_id
current_app.logger.debug("Removing record with id {0}".format(record_id))
try:
cf.zones.dns_records.delete(zone_id, record_id)
except Exception as e:
current_app.logger.error('/zones.dns_records.post: %s' % e)

View File

@ -25,8 +25,6 @@ from lemur.common.utils import validate_conf
from lemur.plugins.bases import IssuerPlugin from lemur.plugins.bases import IssuerPlugin
from lemur.plugins import lemur_acme as acme from lemur.plugins import lemur_acme as acme
from .route53 import delete_txt_record, create_txt_record, wait_for_r53_change
def find_dns_challenge(authz): def find_dns_challenge(authz):
for combo in authz.body.resolved_combinations: for combo in authz.body.resolved_combinations:
@ -45,12 +43,13 @@ class AuthorizationRecord(object):
self.change_id = change_id self.change_id = change_id
def start_dns_challenge(acme_client, account_number, host): def start_dns_challenge(acme_client, account_number, host, dns_provider):
current_app.logger.debug("Starting DNS challenge for {0}".format(host))
authz = acme_client.request_domain_challenges(host) authz = acme_client.request_domain_challenges(host)
[dns_challenge] = find_dns_challenge(authz) [dns_challenge] = find_dns_challenge(authz)
change_id = create_txt_record( change_id = dns_provider.create_txt_record(
dns_challenge.validation_domain_name(host), dns_challenge.validation_domain_name(host),
dns_challenge.validation(acme_client.key), dns_challenge.validation(acme_client.key),
account_number account_number
@ -64,8 +63,8 @@ def start_dns_challenge(acme_client, account_number, host):
) )
def complete_dns_challenge(acme_client, account_number, authz_record): def complete_dns_challenge(acme_client, account_number, authz_record, dns_provider):
wait_for_r53_change(authz_record.change_id, account_number=account_number) dns_provider.wait_for_dns_change(authz_record.change_id, account_number=account_number)
response = authz_record.dns_challenge.response(acme_client.key) response = authz_record.dns_challenge.response(acme_client.key)
@ -96,12 +95,12 @@ def request_certificate(acme_client, authorizations, csr):
OpenSSL.crypto.FILETYPE_PEM, cert_response.body OpenSSL.crypto.FILETYPE_PEM, cert_response.body
).decode('utf-8') ).decode('utf-8')
# https://github.com/alex/letsencrypt-aws/commit/853ea7f93f141fe18d9ef12aee6b3388f98b4830 pem_certificate_chain = "\n".join(
pem_certificate_chain = b"\n".join( OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, cert.decode("utf-8"))
OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, cert)
for cert in acme_client.fetch_chain(cert_response) for cert in acme_client.fetch_chain(cert_response)
).decode('utf-8') ).decode('utf-8')
current_app.logger.debug("{0} {1}".format(type(pem_certificate). type(pem_certificate_chain)))
return pem_certificate, pem_certificate_chain return pem_certificate, pem_certificate_chain
@ -113,12 +112,15 @@ def setup_acme_client():
key = jose.JWKRSA(key=generate_private_key('RSA2048')) key = jose.JWKRSA(key=generate_private_key('RSA2048'))
current_app.logger.debug("Connecting with directory at {0}".format(directory_url))
client = Client(directory_url, key) client = Client(directory_url, key)
registration = client.register( registration = client.register(
messages.NewRegistration.from_data(email=email) messages.NewRegistration.from_data(email=email)
) )
current_app.logger.debug("Connected: {0}".format(registration.uri))
client.agree_to_tos(registration) client.agree_to_tos(registration)
return client, registration return client, registration
@ -129,26 +131,30 @@ def get_domains(options):
:param options: :param options:
:return: :return:
""" """
current_app.logger.debug("Fetching domains")
domains = [options['common_name']] domains = [options['common_name']]
if options.get('extensions'): if options.get('extensions'):
for name in options['extensions']['sub_alt_names']['names']: for name in options['extensions']['sub_alt_names']['names']:
domains.append(name.value) domains.append(name)
current_app.logger.debug("Got these domains: {0}".format(domains))
return domains return domains
def get_authorizations(acme_client, account_number, domains): def get_authorizations(acme_client, account_number, domains, dns_provider):
authorizations = [] authorizations = []
try: try:
for domain in domains: for domain in domains:
authz_record = start_dns_challenge(acme_client, account_number, domain) authz_record = start_dns_challenge(acme_client, account_number, domain, dns_provider)
authorizations.append(authz_record) authorizations.append(authz_record)
for authz_record in authorizations: for authz_record in authorizations:
complete_dns_challenge(acme_client, account_number, authz_record) complete_dns_challenge(acme_client, account_number, authz_record, dns_provider)
finally: finally:
for authz_record in authorizations: for authz_record in authorizations:
dns_challenge = authz_record.dns_challenge dns_challenge = authz_record.dns_challenge
delete_txt_record( dns_provider.delete_txt_record(
authz_record.change_id, authz_record.change_id,
account_number, account_number,
dns_challenge.validation_domain_name(authz_record.host), dns_challenge.validation_domain_name(authz_record.host),
@ -177,6 +183,9 @@ class ACMEIssuerPlugin(IssuerPlugin):
] ]
validate_conf(current_app, required_vars) validate_conf(current_app, required_vars)
self.dns_provider_name = current_app.config.get('ACME_DNS_PROVIDER', 'route53')
current_app.logger.debug("Using DNS provider: {0}".format(self.dns_provider_name))
self.dns_provider = __import__(self.dns_provider_name, globals(), locals(), [], 1)
super(ACMEIssuerPlugin, self).__init__(*args, **kwargs) super(ACMEIssuerPlugin, self).__init__(*args, **kwargs)
def create_certificate(self, csr, issuer_options): def create_certificate(self, csr, issuer_options):
@ -191,7 +200,7 @@ class ACMEIssuerPlugin(IssuerPlugin):
acme_client, registration = setup_acme_client() acme_client, registration = setup_acme_client()
account_number = current_app.config.get('ACME_AWS_ACCOUNT_NUMBER') account_number = current_app.config.get('ACME_AWS_ACCOUNT_NUMBER')
domains = get_domains(issuer_options) domains = get_domains(issuer_options)
authorizations = get_authorizations(acme_client, account_number, domains) authorizations = get_authorizations(acme_client, account_number, domains, self.dns_provider)
pem_certificate, pem_certificate_chain = request_certificate(acme_client, authorizations, csr) pem_certificate, pem_certificate_chain = request_certificate(acme_client, authorizations, csr)
# TODO add external ID (if possible) # TODO add external ID (if possible)
return pem_certificate, pem_certificate_chain, None return pem_certificate, pem_certificate_chain, None

View File

@ -3,7 +3,7 @@ from lemur.plugins.lemur_aws.sts import sts_client
@sts_client('route53') @sts_client('route53')
def wait_for_r53_change(change_id, client=None): def wait_for_dns_change(change_id, client=None):
_, change_id = change_id _, change_id = change_id
while True: while True:

View File

@ -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.

View File

@ -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)

View File

@ -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',)

View File

@ -0,0 +1,25 @@
<div class="modal-header">
<button type="button" class="close" ng-click="exit()" aria-label="Exit"><span artia-hidden="true">&times;</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>

View File

@ -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>

View File

@ -0,0 +1,50 @@
<div class="modal-header">
<button type="button" class="close" ng-click="cancel()" aria-label="Close"><span aria-hidden="true">&times;</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>

View File

@ -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>

View 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
});
});
};
});

View File

@ -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>

View 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;
});

View 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();
});
};
});

View 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>

View File

@ -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>

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View 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

View File

@ -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')

View File

@ -34,6 +34,7 @@ with open(os.path.join(ROOT, 'lemur', '__about__.py')) as f:
install_requires = [ install_requires = [
'CloudFlare==1.7.5',
'Flask==0.12', 'Flask==0.12',
'Flask-RESTful==0.3.6', 'Flask-RESTful==0.3.6',
'Flask-SQLAlchemy==2.1', 'Flask-SQLAlchemy==2.1',