Merge branch 'master' into requirements_logging
This commit is contained in:
commit
462e757f92
@ -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.
|
||||
|
||||
|
||||
The `IssuerPlugin` exposes two functions::
|
||||
The `IssuerPlugin` exposes four functions functions::
|
||||
|
||||
def create_certificate(self, csr, issuer_options):
|
||||
# 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.
|
||||
|
||||
@ -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
|
||||
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
|
||||
-----------
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
CloudFlare==1.7.5
|
||||
Flask==0.12
|
||||
Flask-RESTful==0.3.6
|
||||
Flask-SQLAlchemy==2.1
|
||||
|
@ -27,6 +27,7 @@ from lemur.sources.views import mod as sources_bp
|
||||
from lemur.endpoints.views import mod as endpoints_bp
|
||||
from lemur.logs.views import mod as logs_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 (
|
||||
__author__, __copyright__, __email__, __license__, __summary__, __title__,
|
||||
@ -53,7 +54,8 @@ LEMUR_BLUEPRINTS = (
|
||||
sources_bp,
|
||||
endpoints_bp,
|
||||
logs_bp,
|
||||
api_key_bp
|
||||
api_key_bp,
|
||||
pending_certificates_bp,
|
||||
)
|
||||
|
||||
|
||||
|
@ -32,6 +32,9 @@ class Authority(db.Model):
|
||||
authority_certificate = relationship("Certificate", backref='root_authority', uselist=False, foreign_keys='Certificate.root_authority_id')
|
||||
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):
|
||||
self.owner = kwargs['owner']
|
||||
self.roles = kwargs.get('roles', [])
|
||||
|
@ -37,7 +37,7 @@ from lemur.constants import SUCCESS_METRIC_STATUS, FAILURE_METRIC_STATUS
|
||||
|
||||
from lemur.models import certificate_associations, certificate_source_associations, \
|
||||
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.policies.models import RotationPolicy
|
||||
@ -129,6 +129,11 @@ class Certificate(db.Model):
|
||||
secondaryjoin=id == certificate_replacement_associations.c.replaced_certificate_id, # noqa
|
||||
backref='replaced')
|
||||
|
||||
replaced_by_pending = relationship('PendingCertificate',
|
||||
secondary=pending_cert_replacement_associations,
|
||||
backref='pending_replace',
|
||||
viewonly=True)
|
||||
|
||||
logs = relationship('Log', backref='certificate')
|
||||
endpoints = relationship('Endpoint', backref='certificate')
|
||||
rotation_policy = relationship("RotationPolicy")
|
||||
|
@ -45,11 +45,14 @@ class CertificateCreationSchema(CertificateSchema):
|
||||
@post_load
|
||||
def default_notification(self, data):
|
||||
if not data['notifications']:
|
||||
notification_name = "DEFAULT_{0}".format(data['owner'].split('@')[0].upper())
|
||||
data['notifications'] += notification_service.create_default_expiration_notifications(notification_name, [data['owner']])
|
||||
|
||||
notification_name = 'DEFAULT_SECURITY'
|
||||
data['notifications'] += notification_service.create_default_expiration_notifications(notification_name, current_app.config.get('LEMUR_SECURITY_TEAM_EMAIL'))
|
||||
data['notifications'] += notification_service.create_default_expiration_notifications(
|
||||
"DEFAULT_{0}".format(data['owner'].split('@')[0].upper()),
|
||||
[data['owner']],
|
||||
)
|
||||
data['notifications'] += notification_service.create_default_expiration_notifications(
|
||||
'DEFAULT_SECURITY',
|
||||
current_app.config.get('LEMUR_SECURITY_TEAM_EMAIL')
|
||||
)
|
||||
return data
|
||||
|
||||
|
||||
|
@ -25,6 +25,7 @@ from lemur.authorities.models import Authority
|
||||
from lemur.destinations.models import Destination
|
||||
from lemur.certificates.models import Certificate
|
||||
from lemur.notifications.models import Notification
|
||||
from lemur.pending_certificates.models import PendingCertificate
|
||||
|
||||
from lemur.certificates.schemas import CertificateOutputSchema, CertificateInputSchema
|
||||
|
||||
@ -63,6 +64,9 @@ def get_by_serial(serial):
|
||||
:param serial:
|
||||
: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()
|
||||
|
||||
|
||||
@ -190,7 +194,7 @@ def mint(**kwargs):
|
||||
csr_imported.send(authority=authority, csr=csr)
|
||||
|
||||
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):
|
||||
@ -243,11 +247,12 @@ def create(**kwargs):
|
||||
"""
|
||||
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['private_key'] = private_key
|
||||
kwargs['chain'] = cert_chain
|
||||
kwargs['external_id'] = external_id
|
||||
kwargs['csr'] = csr
|
||||
|
||||
roles = create_certificate_roles(**kwargs)
|
||||
|
||||
@ -256,15 +261,20 @@ def create(**kwargs):
|
||||
else:
|
||||
kwargs['roles'] = roles
|
||||
|
||||
cert = Certificate(**kwargs)
|
||||
if cert_body:
|
||||
cert = Certificate(**kwargs)
|
||||
kwargs['creator'].certificates.append(cert)
|
||||
else:
|
||||
cert = PendingCertificate(**kwargs)
|
||||
kwargs['creator'].pending_certificates.append(cert)
|
||||
|
||||
kwargs['creator'].certificates.append(cert)
|
||||
cert.authority = kwargs['authority']
|
||||
certificate_issued.send(certificate=cert, authority=cert.authority)
|
||||
|
||||
database.commit()
|
||||
|
||||
metrics.send('certificate_issued', 'counter', 1, metric_tags=dict(owner=cert.owner, issuer=cert.issuer))
|
||||
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))
|
||||
return cert
|
||||
|
||||
|
||||
|
@ -18,6 +18,7 @@ from lemur.auth.service import AuthenticatedResource
|
||||
from lemur.auth.permissions import AuthorityPermission, CertificatePermission
|
||||
|
||||
from lemur.certificates import service
|
||||
from lemur.certificates.models import Certificate
|
||||
from lemur.plugins.base import plugins
|
||||
from lemur.certificates.schemas import (
|
||||
certificate_input_schema,
|
||||
@ -267,7 +268,9 @@ class CertificatesList(AuthenticatedResource):
|
||||
if authority_permission.can():
|
||||
data['creator'] = g.user
|
||||
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', certificate=cert)
|
||||
return cert
|
||||
|
||||
return dict(message="You are not authorized to use the authority: {0}".format(data['authority'].name)), 403
|
||||
|
@ -162,6 +162,9 @@ def domains(cert):
|
||||
entries = ext.value.get_values_for_type(x509.DNSName)
|
||||
for entry in entries:
|
||||
domains.append(entry)
|
||||
except x509.ExtensionNotFound:
|
||||
if current_app.config.get("LOG_SSL_SUBJ_ALT_NAME_ERRORS", True):
|
||||
sentry.captureException()
|
||||
except Exception as e:
|
||||
sentry.captureException()
|
||||
|
||||
|
@ -53,6 +53,19 @@ def parse_certificate(body):
|
||||
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):
|
||||
"""Returns the authority key for a given certificate in hex format"""
|
||||
parsed_cert = parse_certificate(body)
|
||||
|
@ -21,6 +21,7 @@ from lemur.reporting.cli import manager as report_manager
|
||||
from lemur.endpoints.cli import manager as endpoint_manager
|
||||
from lemur.certificates.cli import manager as certificate_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.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.endpoints.models import Endpoint # noqa
|
||||
from lemur.policies.models import RotationPolicy # noqa
|
||||
from lemur.pending_certificates.models import PendingCertificate # noqa
|
||||
|
||||
|
||||
manager = Manager(create_app)
|
||||
@ -542,6 +544,7 @@ def main():
|
||||
manager.add_command("endpoint", endpoint_manager)
|
||||
manager.add_command("report", report_manager)
|
||||
manager.add_command("policy", policy_manager)
|
||||
manager.add_command("pending_certs", pending_certificate_manager)
|
||||
manager.run()
|
||||
|
||||
|
||||
|
28
lemur/migrations/versions/449c3d5c7299_.py
Normal file
28
lemur/migrations/versions/449c3d5c7299_.py
Normal file
@ -0,0 +1,28 @@
|
||||
"""Add unique constraint to certificate_notification_associations table.
|
||||
|
||||
Revision ID: 449c3d5c7299
|
||||
Revises: 5770674184de
|
||||
Create Date: 2018-02-24 22:51:35.369229
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '449c3d5c7299'
|
||||
down_revision = '5770674184de'
|
||||
|
||||
from alembic import op
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
|
||||
db = SQLAlchemy()
|
||||
|
||||
CONSTRAINT_NAME = "uq_dest_not_ids"
|
||||
TABLE = "certificate_notification_associations"
|
||||
COLUMNS = ["notification_id", "certificate_id"]
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.create_unique_constraint(CONSTRAINT_NAME, TABLE, COLUMNS)
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_constraint(CONSTRAINT_NAME, TABLE)
|
99
lemur/migrations/versions/556ceb3e3c3e_.py
Normal file
99
lemur/migrations/versions/556ceb3e3c3e_.py
Normal file
@ -0,0 +1,99 @@
|
||||
"""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 = '449c3d5c7299'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from lemur.utils import Vault
|
||||
from sqlalchemy.dialects import postgresql
|
||||
from sqlalchemy_utils import ArrowType
|
||||
|
||||
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', Vault(), nullable=True),
|
||||
sa.Column('date_created', 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)
|
||||
# ### 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 ###
|
44
lemur/migrations/versions/5770674184de_.py
Normal file
44
lemur/migrations/versions/5770674184de_.py
Normal file
@ -0,0 +1,44 @@
|
||||
"""Remove duplicates from certificate_notification_associations.
|
||||
|
||||
Revision ID: 5770674184de
|
||||
Revises: ce547319f7be
|
||||
Create Date: 2018-02-23 15:27:30.335435
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '5770674184de'
|
||||
down_revision = 'ce547319f7be'
|
||||
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from lemur.models import certificate_notification_associations
|
||||
|
||||
db = SQLAlchemy()
|
||||
session = db.session()
|
||||
|
||||
|
||||
def upgrade():
|
||||
print("Querying for all entries in certificate_notification_associations.")
|
||||
# Query for all entries in table
|
||||
results = session.query(certificate_notification_associations).with_entities(
|
||||
certificate_notification_associations.c.certificate_id,
|
||||
certificate_notification_associations.c.notification_id,
|
||||
certificate_notification_associations.c.id,
|
||||
)
|
||||
|
||||
seen = {}
|
||||
# Iterate through all entries and mark as seen for each certificate_id and notification_id pair
|
||||
for x in results:
|
||||
# If we've seen a pair already, delete the duplicates
|
||||
if seen.get("{}-{}".format(x.certificate_id, x.notification_id)):
|
||||
print("Deleting duplicate: {}".format(x))
|
||||
d = session.query(certificate_notification_associations).filter(certificate_notification_associations.c.id==x.id)
|
||||
d.delete(synchronize_session=False)
|
||||
seen["{}-{}".format(x.certificate_id, x.notification_id)] = True
|
||||
db.session.commit()
|
||||
db.session.flush()
|
||||
|
||||
|
||||
def downgrade():
|
||||
# No way to downgrade this
|
||||
pass
|
36
lemur/migrations/versions/ce547319f7be_.py
Normal file
36
lemur/migrations/versions/ce547319f7be_.py
Normal file
@ -0,0 +1,36 @@
|
||||
"""Add id column to certificate_notification_associations.
|
||||
|
||||
Revision ID: ce547319f7be
|
||||
Revises: 5bc47fa7cac4
|
||||
Create Date: 2018-02-23 11:00:02.150561
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'ce547319f7be'
|
||||
down_revision = '5bc47fa7cac4'
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
|
||||
|
||||
db = SQLAlchemy()
|
||||
|
||||
TABLE = "certificate_notification_associations"
|
||||
|
||||
|
||||
def upgrade():
|
||||
print("Adding id column")
|
||||
op.add_column(
|
||||
TABLE,
|
||||
sa.Column('id', sa.Integer, primary_key=True, autoincrement=True)
|
||||
)
|
||||
db.session.commit()
|
||||
db.session.flush()
|
||||
|
||||
def downgrade():
|
||||
op.drop_column(TABLE, "id")
|
||||
db.session.commit()
|
||||
db.session.flush()
|
@ -8,7 +8,7 @@
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
from sqlalchemy import Column, Integer, ForeignKey, Index
|
||||
from sqlalchemy import Column, Integer, ForeignKey, Index, UniqueConstraint
|
||||
|
||||
from lemur.database import db
|
||||
|
||||
@ -41,7 +41,9 @@ certificate_notification_associations = db.Table('certificate_notification_assoc
|
||||
Column('notification_id', Integer,
|
||||
ForeignKey('notifications.id', ondelete='cascade')),
|
||||
Column('certificate_id', Integer,
|
||||
ForeignKey('certificates.id', ondelete='cascade'))
|
||||
ForeignKey('certificates.id', ondelete='cascade')),
|
||||
Column('id', Integer, primary_key=True, autoincrement=True),
|
||||
UniqueConstraint('notification_id', 'certificate_id', name='uq_dest_not_ids')
|
||||
)
|
||||
|
||||
Index('certificate_notification_associations_ix', certificate_notification_associations.c.notification_id, certificate_notification_associations.c.certificate_id)
|
||||
@ -83,3 +85,48 @@ policies_ciphers = db.Table('policies_ciphers',
|
||||
Column('policy_id', Integer, ForeignKey('policy.id')))
|
||||
|
||||
Index('policies_ciphers_ix', policies_ciphers.c.cipher_id, policies_ciphers.c.policy_id)
|
||||
|
||||
|
||||
pending_cert_destination_associations = db.Table('pending_cert_destination_associations',
|
||||
Column('destination_id', Integer,
|
||||
ForeignKey('destinations.id', ondelete='cascade')),
|
||||
Column('pending_cert_id', Integer,
|
||||
ForeignKey('pending_certs.id', ondelete='cascade'))
|
||||
)
|
||||
|
||||
Index('pending_cert_destination_associations_ix', pending_cert_destination_associations.c.destination_id, pending_cert_destination_associations.c.pending_cert_id)
|
||||
|
||||
|
||||
pending_cert_notification_associations = db.Table('pending_cert_notification_associations',
|
||||
Column('notification_id', Integer,
|
||||
ForeignKey('notifications.id', ondelete='cascade')),
|
||||
Column('pending_cert_id', Integer,
|
||||
ForeignKey('pending_certs.id', ondelete='cascade'))
|
||||
)
|
||||
|
||||
Index('pending_cert_notification_associations_ix', pending_cert_notification_associations.c.notification_id, pending_cert_notification_associations.c.pending_cert_id)
|
||||
|
||||
pending_cert_source_associations = db.Table('pending_cert_source_associations',
|
||||
Column('source_id', Integer,
|
||||
ForeignKey('sources.id', ondelete='cascade')),
|
||||
Column('pending_cert_id', Integer,
|
||||
ForeignKey('pending_certs.id', ondelete='cascade'))
|
||||
)
|
||||
|
||||
Index('pending_cert_source_associations_ix', pending_cert_source_associations.c.source_id, pending_cert_source_associations.c.pending_cert_id)
|
||||
|
||||
pending_cert_replacement_associations = db.Table('pending_cert_replacement_associations',
|
||||
Column('replaced_certificate_id', Integer,
|
||||
ForeignKey('certificates.id', ondelete='cascade')),
|
||||
Column('pending_cert_id', Integer,
|
||||
ForeignKey('pending_certs.id', ondelete='cascade'))
|
||||
)
|
||||
|
||||
Index('pending_cert_replacement_associations_ix', pending_cert_replacement_associations.c.replaced_certificate_id, pending_cert_replacement_associations.c.pending_cert_id)
|
||||
|
||||
pending_cert_role_associations = db.Table('pending_cert_role_associations',
|
||||
Column('pending_cert_id', Integer, ForeignKey('pending_certs.id')),
|
||||
Column('role_id', Integer, ForeignKey('roles.id'))
|
||||
)
|
||||
|
||||
Index('pending_cert_role_associations_ix', pending_cert_role_associations.c.pending_cert_id, pending_cert_role_associations.c.role_id)
|
||||
|
@ -11,7 +11,8 @@ from sqlalchemy_utils import JSONType
|
||||
|
||||
from lemur.database import db
|
||||
from lemur.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):
|
||||
@ -29,6 +30,13 @@ class Notification(db.Model):
|
||||
backref="notification",
|
||||
cascade='all,delete'
|
||||
)
|
||||
pending_certificates = relationship(
|
||||
"PendingCertificate",
|
||||
secondary=pending_cert_notification_associations,
|
||||
passive_deletes=True,
|
||||
backref="notification",
|
||||
cascade='all,delete'
|
||||
)
|
||||
|
||||
@property
|
||||
def plugin(self):
|
||||
|
0
lemur/pending_certificates/__init__.py
Normal file
0
lemur/pending_certificates/__init__.py
Normal file
48
lemur/pending_certificates/cli.py
Normal file
48
lemur/pending_certificates/cli.py
Normal file
@ -0,0 +1,48 @@
|
||||
"""
|
||||
.. module: lemur.pending_certificates.cli
|
||||
|
||||
.. moduleauthor:: James Chuong <jchuong@instartlogic.com>
|
||||
"""
|
||||
|
||||
from flask_script import Manager
|
||||
|
||||
from lemur.pending_certificates import service as pending_certificate_service
|
||||
from lemur.plugins.base import plugins
|
||||
from lemur.users import service as user_service
|
||||
|
||||
manager = Manager(usage="Handles pending certificate related tasks.")
|
||||
|
||||
|
||||
@manager.option('-i', dest='ids', action='append', help='IDs of pending certificates to fetch')
|
||||
def fetch(ids):
|
||||
"""
|
||||
Attempt to get full certificates for each pending certificate listed.
|
||||
|
||||
Args:
|
||||
ids: a list of ids of PendingCertificates (passed in by manager options when run as CLI)
|
||||
`python manager.py pending_certs fetch -i 123 321 all`
|
||||
"""
|
||||
new = 0
|
||||
failed = 0
|
||||
pending_certs = pending_certificate_service.get_pending_certs(ids)
|
||||
user = user_service.get_by_username('lemur')
|
||||
|
||||
for cert in pending_certs:
|
||||
authority = plugins.get(cert.authority.plugin_name)
|
||||
real_cert = authority.get_ordered_certificate(cert.external_id)
|
||||
if real_cert:
|
||||
# If a real certificate was returned from issuer, then create it in Lemur and delete
|
||||
# the pending certificate
|
||||
pending_certificate_service.create_certificate(cert, real_cert, user)
|
||||
pending_certificate_service.delete(cert)
|
||||
# add metrics to metrics extension
|
||||
new += 1
|
||||
else:
|
||||
pending_certificate_service.increment_attempt(cert)
|
||||
failed += 1
|
||||
print(
|
||||
"[+] Certificates: New: {new} Failed: {failed}".format(
|
||||
new=new,
|
||||
failed=failed
|
||||
)
|
||||
)
|
95
lemur/pending_certificates/models.py
Normal file
95
lemur/pending_certificates/models.py
Normal file
@ -0,0 +1,95 @@
|
||||
"""
|
||||
.. module: lemur.pending_certificates.models
|
||||
Copyright (c) 2017 and onwards Instart Logic, Inc. All rights reserved.
|
||||
.. moduleauthor:: James Chuong <jchuong@instartlogic.com>
|
||||
"""
|
||||
from datetime import datetime as dt
|
||||
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy import Integer, ForeignKey, String, PassiveDefault, func, Column, Text, Boolean
|
||||
from sqlalchemy_utils.types.arrow import ArrowType
|
||||
|
||||
import lemur.common.utils
|
||||
from lemur.certificates.models import get_or_increase_name
|
||||
from lemur.common import defaults
|
||||
from lemur.database import db
|
||||
from lemur.utils import Vault
|
||||
|
||||
from lemur.models import pending_cert_source_associations, \
|
||||
pending_cert_destination_associations, pending_cert_notification_associations, \
|
||||
pending_cert_replacement_associations, pending_cert_role_associations
|
||||
|
||||
|
||||
class PendingCertificate(db.Model):
|
||||
__tablename__ = 'pending_certs'
|
||||
id = Column(Integer, primary_key=True)
|
||||
external_id = Column(String(128))
|
||||
owner = Column(String(128), nullable=False)
|
||||
name = Column(String(256), unique=True)
|
||||
description = Column(String(1024))
|
||||
notify = Column(Boolean, default=True)
|
||||
number_attempts = Column(Integer)
|
||||
rename = Column(Boolean, default=True)
|
||||
|
||||
cn = Column(String(128))
|
||||
csr = Column(Text(), nullable=False)
|
||||
chain = Column(Text())
|
||||
private_key = Column(Vault, nullable=True)
|
||||
|
||||
date_created = Column(ArrowType, PassiveDefault(func.now()), nullable=False)
|
||||
|
||||
status = Column(String(128))
|
||||
|
||||
rotation = Column(Boolean, default=False)
|
||||
user_id = Column(Integer, ForeignKey('users.id'))
|
||||
authority_id = Column(Integer, ForeignKey('authorities.id', ondelete="CASCADE"))
|
||||
root_authority_id = Column(Integer, ForeignKey('authorities.id', ondelete="CASCADE"))
|
||||
rotation_policy_id = Column(Integer, ForeignKey('rotation_policies.id'))
|
||||
|
||||
notifications = relationship('Notification', secondary=pending_cert_notification_associations, backref='pending_cert', passive_deletes=True)
|
||||
destinations = relationship('Destination', secondary=pending_cert_destination_associations, backref='pending_cert', passive_deletes=True)
|
||||
sources = relationship('Source', secondary=pending_cert_source_associations, backref='pending_cert', passive_deletes=True)
|
||||
roles = relationship('Role', secondary=pending_cert_role_associations, backref='pending_cert', passive_deletes=True)
|
||||
replaces = relationship('Certificate',
|
||||
secondary=pending_cert_replacement_associations,
|
||||
backref='pending_cert',
|
||||
passive_deletes=True)
|
||||
|
||||
rotation_policy = relationship("RotationPolicy")
|
||||
|
||||
sensitive_fields = ('private_key',)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.csr = kwargs.get('csr')
|
||||
self.private_key = kwargs.get('private_key', "")
|
||||
if self.private_key:
|
||||
# If the request does not send private key, the key exists but the value is None
|
||||
self.private_key = self.private_key.strip()
|
||||
self.external_id = kwargs.get('external_id')
|
||||
|
||||
# when destinations are appended they require a valid name.
|
||||
if kwargs.get('name'):
|
||||
self.name = get_or_increase_name(defaults.text_to_slug(kwargs['name']), 0)
|
||||
self.rename = False
|
||||
else:
|
||||
# TODO: Fix auto-generated name, it should be renamed on creation
|
||||
self.name = get_or_increase_name(
|
||||
defaults.certificate_name(kwargs['common_name'], kwargs['authority'].name,
|
||||
dt.now(), dt.now(), False), self.external_id)
|
||||
self.rename = True
|
||||
|
||||
self.cn = defaults.common_name(lemur.common.utils.parse_csr(self.csr))
|
||||
self.owner = kwargs['owner']
|
||||
self.number_attempts = 0
|
||||
|
||||
if kwargs.get('chain'):
|
||||
self.chain = kwargs['chain'].strip()
|
||||
|
||||
self.notify = kwargs.get('notify', True)
|
||||
self.destinations = kwargs.get('destinations', [])
|
||||
self.notifications = kwargs.get('notifications', [])
|
||||
self.description = kwargs.get('description')
|
||||
self.roles = list(set(kwargs.get('roles', [])))
|
||||
self.replaces = kwargs.get('replaces', [])
|
||||
self.rotation = kwargs.get('rotation')
|
||||
self.rotation_policy = kwargs.get('rotation_policy')
|
101
lemur/pending_certificates/schemas.py
Normal file
101
lemur/pending_certificates/schemas.py
Normal file
@ -0,0 +1,101 @@
|
||||
from marshmallow import fields, post_load
|
||||
|
||||
from lemur.schemas import (
|
||||
AssociatedCertificateSchema,
|
||||
AssociatedDestinationSchema,
|
||||
AssociatedNotificationSchema,
|
||||
AssociatedRoleSchema,
|
||||
EndpointNestedOutputSchema,
|
||||
ExtensionSchema
|
||||
)
|
||||
|
||||
from lemur.common.schema import LemurInputSchema, LemurOutputSchema
|
||||
from lemur.users.schemas import UserNestedOutputSchema
|
||||
from lemur.authorities.schemas import AuthorityNestedOutputSchema
|
||||
from lemur.certificates.schemas import CertificateNestedOutputSchema
|
||||
from lemur.destinations.schemas import DestinationNestedOutputSchema
|
||||
from lemur.domains.schemas import DomainNestedOutputSchema
|
||||
from lemur.notifications.schemas import NotificationNestedOutputSchema
|
||||
from lemur.roles.schemas import RoleNestedOutputSchema
|
||||
from lemur.policies.schemas import RotationPolicyNestedOutputSchema
|
||||
|
||||
from lemur.notifications import service as notification_service
|
||||
|
||||
|
||||
class PendingCertificateSchema(LemurInputSchema):
|
||||
owner = fields.Email(required=True)
|
||||
description = fields.String(missing='', allow_none=True)
|
||||
|
||||
|
||||
class PendingCertificateOutputSchema(LemurOutputSchema):
|
||||
id = fields.Integer()
|
||||
external_id = fields.String()
|
||||
csr = fields.String()
|
||||
chain = fields.String()
|
||||
deleted = fields.Boolean(default=False)
|
||||
description = fields.String()
|
||||
issuer = fields.String()
|
||||
name = fields.String()
|
||||
number_attempts = fields.Integer()
|
||||
date_created = fields.Date()
|
||||
|
||||
rotation = fields.Boolean()
|
||||
|
||||
# Note aliasing is the first step in deprecating these fields.
|
||||
notify = fields.Boolean()
|
||||
active = fields.Boolean(attribute='notify')
|
||||
|
||||
cn = fields.String()
|
||||
common_name = fields.String(attribute='cn')
|
||||
|
||||
owner = fields.Email()
|
||||
|
||||
status = fields.String()
|
||||
user = fields.Nested(UserNestedOutputSchema)
|
||||
|
||||
extensions = fields.Nested(ExtensionSchema)
|
||||
|
||||
# associated objects
|
||||
domains = fields.Nested(DomainNestedOutputSchema, many=True)
|
||||
destinations = fields.Nested(DestinationNestedOutputSchema, many=True)
|
||||
notifications = fields.Nested(NotificationNestedOutputSchema, many=True)
|
||||
replaces = fields.Nested(CertificateNestedOutputSchema, many=True)
|
||||
authority = fields.Nested(AuthorityNestedOutputSchema)
|
||||
roles = fields.Nested(RoleNestedOutputSchema, many=True)
|
||||
endpoints = fields.Nested(EndpointNestedOutputSchema, many=True, missing=[])
|
||||
replaced_by = fields.Nested(CertificateNestedOutputSchema, many=True, attribute='replaced')
|
||||
rotation_policy = fields.Nested(RotationPolicyNestedOutputSchema)
|
||||
|
||||
|
||||
class PendingCertificateEditInputSchema(PendingCertificateSchema):
|
||||
owner = fields.String()
|
||||
|
||||
notify = fields.Boolean()
|
||||
rotation = fields.Boolean()
|
||||
|
||||
destinations = fields.Nested(AssociatedDestinationSchema, missing=[], many=True)
|
||||
notifications = fields.Nested(AssociatedNotificationSchema, missing=[], many=True)
|
||||
replaces = fields.Nested(AssociatedCertificateSchema, missing=[], many=True)
|
||||
roles = fields.Nested(AssociatedRoleSchema, missing=[], many=True)
|
||||
|
||||
@post_load
|
||||
def enforce_notifications(self, data):
|
||||
"""
|
||||
Ensures that when an owner changes, default notifications are added for the new owner.
|
||||
Old owner notifications are retained unless explicitly removed.
|
||||
:param data:
|
||||
:return:
|
||||
"""
|
||||
if data['owner']:
|
||||
notification_name = "DEFAULT_{0}".format(data['owner'].split('@')[0].upper())
|
||||
data['notifications'] += notification_service.create_default_expiration_notifications(notification_name, [data['owner']])
|
||||
return data
|
||||
|
||||
|
||||
class PendingCertificateCancelSchema(LemurInputSchema):
|
||||
note = fields.String()
|
||||
|
||||
|
||||
pending_certificate_output_schema = PendingCertificateOutputSchema()
|
||||
pending_certificate_edit_input_schema = PendingCertificateEditInputSchema()
|
||||
pending_certificate_cancel_schema = PendingCertificateCancelSchema()
|
219
lemur/pending_certificates/service.py
Normal file
219
lemur/pending_certificates/service.py
Normal file
@ -0,0 +1,219 @@
|
||||
"""
|
||||
.. module: lemur.pending_certificates.service
|
||||
Copyright (c) 2017 and onwards Instart Logic, Inc. All rights reserved.
|
||||
.. moduleauthor:: James Chuong <jchuong@instartlogic.com>
|
||||
"""
|
||||
import arrow
|
||||
|
||||
from sqlalchemy import or_, cast, Boolean, Integer
|
||||
|
||||
from lemur import database
|
||||
from lemur.plugins.base import plugins
|
||||
|
||||
from lemur.roles.models import Role
|
||||
from lemur.domains.models import Domain
|
||||
from lemur.authorities.models import Authority
|
||||
from lemur.destinations.models import Destination
|
||||
from lemur.notifications.models import Notification
|
||||
from lemur.pending_certificates.models import PendingCertificate
|
||||
|
||||
from lemur.certificates import service as certificate_service
|
||||
from lemur.users import service as user_service
|
||||
|
||||
from lemur.certificates.schemas import CertificateUploadInputSchema
|
||||
|
||||
|
||||
def get(pending_cert_id):
|
||||
"""
|
||||
Retrieve pending certificate by ID
|
||||
"""
|
||||
return database.get(PendingCertificate, pending_cert_id)
|
||||
|
||||
|
||||
def get_by_external_id(issuer, external_id):
|
||||
"""
|
||||
Retrieves a pending certificate by its issuer and external_id
|
||||
Since external_id is not necessarily unique between CAs
|
||||
|
||||
:param issuer:
|
||||
:param external_id:
|
||||
:return: PendingCertificate or None
|
||||
"""
|
||||
if isinstance(external_id, int):
|
||||
external_id = str(external_id)
|
||||
return PendingCertificate.query \
|
||||
.filter(PendingCertificate.authority_id == issuer.id) \
|
||||
.filter(PendingCertificate.external_id == external_id) \
|
||||
.one_or_none()
|
||||
|
||||
|
||||
def get_by_name(pending_cert_name):
|
||||
"""
|
||||
Retrieve pending certificate by name
|
||||
"""
|
||||
return database.get(PendingCertificate, pending_cert_name, field='name')
|
||||
|
||||
|
||||
def delete(pending_certificate):
|
||||
database.delete(pending_certificate)
|
||||
|
||||
|
||||
def get_pending_certs(pending_ids):
|
||||
"""
|
||||
Retrieve a list of pending certs given a list of ids
|
||||
Filters out non-existing pending certs
|
||||
"""
|
||||
pending_certs = []
|
||||
if 'all' in pending_ids:
|
||||
query = database.session_query(PendingCertificate)
|
||||
return database.find_all(query, PendingCertificate, {}).all()
|
||||
else:
|
||||
for pending_id in pending_ids:
|
||||
pending_cert = get(pending_id)
|
||||
if pending_cert:
|
||||
pending_certs.append(pending_cert)
|
||||
return pending_certs
|
||||
|
||||
|
||||
def create_certificate(pending_certificate, certificate, user):
|
||||
"""
|
||||
Create and store a certificate with pending certificate's info
|
||||
Args:
|
||||
pending_certificate: PendingCertificate which will populate the certificate
|
||||
certificate: dict from Authority, which contains the body, chain and external id
|
||||
user: User that called this function, used as 'creator' of the certificate if it does
|
||||
not have an owner
|
||||
"""
|
||||
certificate['owner'] = pending_certificate.owner
|
||||
data, errors = CertificateUploadInputSchema().load(certificate)
|
||||
if errors:
|
||||
raise Exception("Unable to create certificate: {reasons}".format(reasons=errors))
|
||||
|
||||
data.update(vars(pending_certificate))
|
||||
# Copy relationships, vars doesn't copy this without explicit fields
|
||||
data['notifications'] = list(pending_certificate.notifications)
|
||||
data['destinations'] = list(pending_certificate.destinations)
|
||||
data['sources'] = list(pending_certificate.sources)
|
||||
data['roles'] = list(pending_certificate.roles)
|
||||
data['replaces'] = list(pending_certificate.replaces)
|
||||
data['rotation_policy'] = pending_certificate.rotation_policy
|
||||
|
||||
# Replace external id and chain with the one fetched from source
|
||||
data['external_id'] = certificate['external_id']
|
||||
data['chain'] = certificate['chain']
|
||||
creator = user_service.get_by_email(pending_certificate.owner)
|
||||
if not creator:
|
||||
# Owner of the pending certificate is not the creator, so use the current user who called
|
||||
# this as the creator (usually lemur)
|
||||
creator = user
|
||||
|
||||
if pending_certificate.rename:
|
||||
# If generating name from certificate, remove the one from pending certificate
|
||||
del data['name']
|
||||
data['creator'] = creator
|
||||
cert = certificate_service.import_certificate(**data)
|
||||
database.update(cert)
|
||||
return cert
|
||||
|
||||
|
||||
def increment_attempt(pending_certificate):
|
||||
"""
|
||||
Increments pending certificate attempt counter and updates it in the database.
|
||||
"""
|
||||
pending_certificate.number_attempts += 1
|
||||
database.update(pending_certificate)
|
||||
return pending_certificate.number_attempts
|
||||
|
||||
|
||||
def update(pending_cert_id, **kwargs):
|
||||
"""
|
||||
Updates a pending certificate. The allowed fields are validated by
|
||||
PendingCertificateEditInputSchema.
|
||||
"""
|
||||
pending_cert = get(pending_cert_id)
|
||||
for key, value in kwargs.items():
|
||||
setattr(pending_cert, key, value)
|
||||
return database.update(pending_cert)
|
||||
|
||||
|
||||
def cancel(pending_certificate, **kwargs):
|
||||
"""
|
||||
Cancel a pending certificate. A check should be done prior to this function to decide to
|
||||
revoke the certificate or just abort cancelling.
|
||||
Args:
|
||||
pending_certificate: PendingCertificate to be cancelled
|
||||
Returns: the pending certificate if successful, raises Exception if there was an issue
|
||||
"""
|
||||
plugin = plugins.get(pending_certificate.authority.plugin_name)
|
||||
plugin.cancel_ordered_certificate(pending_certificate, **kwargs)
|
||||
pending_certificate.status = 'Cancelled'
|
||||
database.update(pending_certificate)
|
||||
return pending_certificate
|
||||
|
||||
|
||||
def render(args):
|
||||
query = database.session_query(PendingCertificate)
|
||||
time_range = args.pop('time_range')
|
||||
destination_id = args.pop('destination_id')
|
||||
notification_id = args.pop('notification_id', None)
|
||||
show = args.pop('show')
|
||||
# owner = args.pop('owner')
|
||||
# creator = args.pop('creator') # TODO we should enabling filtering by owner
|
||||
|
||||
filt = args.pop('filter')
|
||||
|
||||
if filt:
|
||||
terms = filt.split(';')
|
||||
|
||||
if 'issuer' in terms:
|
||||
# we can't rely on issuer being correct in the cert directly so we combine queries
|
||||
sub_query = database.session_query(Authority.id)\
|
||||
.filter(Authority.name.ilike('%{0}%'.format(terms[1])))\
|
||||
.subquery()
|
||||
|
||||
query = query.filter(
|
||||
or_(
|
||||
PendingCertificate.issuer.ilike('%{0}%'.format(terms[1])),
|
||||
PendingCertificate.authority_id.in_(sub_query)
|
||||
)
|
||||
)
|
||||
|
||||
elif 'destination' in terms:
|
||||
query = query.filter(PendingCertificate.destinations.any(Destination.id == terms[1]))
|
||||
elif 'notify' in filt:
|
||||
query = query.filter(PendingCertificate.notify == cast(terms[1], Boolean))
|
||||
elif 'active' in filt:
|
||||
query = query.filter(PendingCertificate.active == terms[1])
|
||||
elif 'cn' in terms:
|
||||
query = query.filter(
|
||||
or_(
|
||||
PendingCertificate.cn.ilike('%{0}%'.format(terms[1])),
|
||||
PendingCertificate.domains.any(Domain.name.ilike('%{0}%'.format(terms[1])))
|
||||
)
|
||||
)
|
||||
elif 'id' in terms:
|
||||
query = query.filter(PendingCertificate.id == cast(terms[1], Integer))
|
||||
else:
|
||||
query = database.filter(query, PendingCertificate, terms)
|
||||
|
||||
if show:
|
||||
sub_query = database.session_query(Role.name).filter(Role.user_id == args['user'].id).subquery()
|
||||
query = query.filter(
|
||||
or_(
|
||||
PendingCertificate.user_id == args['user'].id,
|
||||
PendingCertificate.owner.in_(sub_query)
|
||||
)
|
||||
)
|
||||
|
||||
if destination_id:
|
||||
query = query.filter(PendingCertificate.destinations.any(Destination.id == destination_id))
|
||||
|
||||
if notification_id:
|
||||
query = query.filter(PendingCertificate.notifications.any(Notification.id == notification_id))
|
||||
|
||||
if time_range:
|
||||
to = arrow.now().replace(weeks=+time_range).format('YYYY-MM-DD')
|
||||
now = arrow.now().format('YYYY-MM-DD')
|
||||
query = query.filter(PendingCertificate.not_after <= to).filter(PendingCertificate.not_after >= now)
|
||||
|
||||
return database.sort_and_page(query, PendingCertificate, args)
|
424
lemur/pending_certificates/views.py
Normal file
424
lemur/pending_certificates/views.py
Normal file
@ -0,0 +1,424 @@
|
||||
"""
|
||||
.. module: lemur.pending_certificates.views
|
||||
:platform: Unix
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: James Chuong <jchuong@instartlogic.com>
|
||||
"""
|
||||
from flask import Blueprint, g, make_response, jsonify
|
||||
from flask_restful import Api, reqparse
|
||||
|
||||
from lemur.auth.service import AuthenticatedResource
|
||||
from lemur.auth.permissions import CertificatePermission
|
||||
|
||||
from lemur.common.schema import validate_schema
|
||||
from lemur.common.utils import paginated_parser
|
||||
|
||||
from lemur.pending_certificates import service
|
||||
from lemur.roles import service as role_service
|
||||
|
||||
from lemur.pending_certificates.schemas import (
|
||||
pending_certificate_output_schema,
|
||||
pending_certificate_edit_input_schema,
|
||||
pending_certificate_cancel_schema,
|
||||
)
|
||||
|
||||
mod = Blueprint('pending_certificates', __name__)
|
||||
api = Api(mod)
|
||||
|
||||
|
||||
class PendingCertificatesList(AuthenticatedResource):
|
||||
def __init__(self):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(PendingCertificatesList, self).__init__()
|
||||
|
||||
@validate_schema(None, pending_certificate_output_schema)
|
||||
def get(self):
|
||||
"""
|
||||
.. http:get:: /pending_certificates
|
||||
|
||||
List of pending certificates
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /pending_certificates HTTP/1.1
|
||||
Host: example.com
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"status": null,
|
||||
"cn": "*.test.example.net",
|
||||
"chain": "",
|
||||
"authority": {
|
||||
"active": true,
|
||||
"owner": "secure@example.com",
|
||||
"id": 1,
|
||||
"description": "verisign test authority",
|
||||
"name": "verisign"
|
||||
},
|
||||
"owner": "joe@example.com",
|
||||
"serial": "82311058732025924142789179368889309156",
|
||||
"id": 2288,
|
||||
"issuer": "SymantecCorporation",
|
||||
"notBefore": "2016-06-03T00:00:00+00:00",
|
||||
"notAfter": "2018-01-12T23:59:59+00:00",
|
||||
"destinations": [],
|
||||
"description": null,
|
||||
"deleted": null,
|
||||
"notifications": [{
|
||||
"id": 1
|
||||
}],
|
||||
"signingAlgorithm": "sha256",
|
||||
"user": {
|
||||
"username": "jane",
|
||||
"active": true,
|
||||
"email": "jane@example.com",
|
||||
"id": 2
|
||||
},
|
||||
"active": true,
|
||||
"domains": [{
|
||||
"sensitive": false,
|
||||
"id": 1090,
|
||||
"name": "*.test.example.net"
|
||||
}],
|
||||
"rotation": true,
|
||||
"rotationPolicy": {"name": "default"},
|
||||
"replaces": [],
|
||||
"replaced": [],
|
||||
"name": "WILDCARD.test.example.net-SymantecCorporation-20160603-20180112",
|
||||
"roles": [{
|
||||
"id": 464,
|
||||
"description": "This is a google group based role created by Lemur",
|
||||
"name": "joe@example.com"
|
||||
}],
|
||||
"san": null
|
||||
}
|
||||
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
|
||||
"""
|
||||
parser = paginated_parser.copy()
|
||||
parser.add_argument('timeRange', type=int, dest='time_range', location='args')
|
||||
parser.add_argument('owner', type=bool, location='args')
|
||||
parser.add_argument('id', type=str, location='args')
|
||||
parser.add_argument('active', type=bool, location='args')
|
||||
parser.add_argument('destinationId', type=int, dest="destination_id", location='args')
|
||||
parser.add_argument('creator', type=str, location='args')
|
||||
parser.add_argument('show', type=str, location='args')
|
||||
args = parser.parse_args()
|
||||
args['user'] = g.user
|
||||
return service.render(args)
|
||||
|
||||
|
||||
class PendingCertificates(AuthenticatedResource):
|
||||
def __init__(self):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(PendingCertificates, self).__init__()
|
||||
|
||||
@validate_schema(None, pending_certificate_output_schema)
|
||||
def get(self, pending_certificate_id):
|
||||
"""
|
||||
.. http:get:: /pending_certificates/1
|
||||
|
||||
One pending certificate
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /pending_certificates/1 HTTP/1.1
|
||||
Host: example.com
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"status": null,
|
||||
"cn": "*.test.example.net",
|
||||
"chain": "",
|
||||
"authority": {
|
||||
"active": true,
|
||||
"owner": "secure@example.com",
|
||||
"id": 1,
|
||||
"description": "verisign test authority",
|
||||
"name": "verisign"
|
||||
},
|
||||
"owner": "joe@example.com",
|
||||
"serial": "82311058732025924142789179368889309156",
|
||||
"id": 1,
|
||||
"issuer": "SymantecCorporation",
|
||||
"notBefore": "2016-06-03T00:00:00+00:00",
|
||||
"notAfter": "2018-01-12T23:59:59+00:00",
|
||||
"destinations": [],
|
||||
"description": null,
|
||||
"deleted": null,
|
||||
"notifications": [{
|
||||
"id": 1
|
||||
}],
|
||||
"signingAlgorithm": "sha256",
|
||||
"user": {
|
||||
"username": "jane",
|
||||
"active": true,
|
||||
"email": "jane@example.com",
|
||||
"id": 2
|
||||
},
|
||||
"active": true,
|
||||
"domains": [{
|
||||
"sensitive": false,
|
||||
"id": 1090,
|
||||
"name": "*.test.example.net"
|
||||
}],
|
||||
"rotation": true,
|
||||
"rotationPolicy": {"name": "default"},
|
||||
"replaces": [],
|
||||
"replaced": [],
|
||||
"name": "WILDCARD.test.example.net-SymantecCorporation-20160603-20180112",
|
||||
"roles": [{
|
||||
"id": 464,
|
||||
"description": "This is a google group based role created by Lemur",
|
||||
"name": "joe@example.com"
|
||||
}],
|
||||
"san": null
|
||||
}
|
||||
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
|
||||
"""
|
||||
return service.get(pending_certificate_id)
|
||||
|
||||
@validate_schema(pending_certificate_edit_input_schema, pending_certificate_output_schema)
|
||||
def put(self, pending_certificate_id, data=None):
|
||||
"""
|
||||
.. http:put:: /pending_certificates/1
|
||||
|
||||
Update a pending certificate
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PUT /pending certificates/1 HTTP/1.1
|
||||
Host: example.com
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
{
|
||||
"owner": "jimbob@example.com",
|
||||
"active": false
|
||||
"notifications": [],
|
||||
"destinations": [],
|
||||
"replacements": []
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"status": null,
|
||||
"cn": "*.test.example.net",
|
||||
"chain": "",
|
||||
"authority": {
|
||||
"active": true,
|
||||
"owner": "secure@example.com",
|
||||
"id": 1,
|
||||
"description": "verisign test authority",
|
||||
"name": "verisign"
|
||||
},
|
||||
"owner": "joe@example.com",
|
||||
"serial": "82311058732025924142789179368889309156",
|
||||
"id": 2288,
|
||||
"issuer": "SymantecCorporation",
|
||||
"destinations": [],
|
||||
"description": null,
|
||||
"deleted": null,
|
||||
"notifications": [{
|
||||
"id": 1
|
||||
}]
|
||||
"user": {
|
||||
"username": "jane",
|
||||
"active": true,
|
||||
"email": "jane@example.com",
|
||||
"id": 2
|
||||
},
|
||||
"active": true,
|
||||
"number_attempts": 1,
|
||||
"csr": "-----BEGIN CERTIFICATE REQUEST-----...",
|
||||
"external_id": 12345,
|
||||
"domains": [{
|
||||
"sensitive": false,
|
||||
"id": 1090,
|
||||
"name": "*.test.example.net"
|
||||
}],
|
||||
"replaces": [],
|
||||
"name": "WILDCARD.test.example.net-SymantecCorporation-20160603-20180112",
|
||||
"roles": [{
|
||||
"id": 464,
|
||||
"description": "This is a google group based role created by Lemur",
|
||||
"name": "joe@example.com"
|
||||
}],
|
||||
"rotation": true,
|
||||
"rotationPolicy": {"name": "default"},
|
||||
}
|
||||
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
|
||||
"""
|
||||
pending_cert = service.get(pending_certificate_id)
|
||||
|
||||
if not pending_cert:
|
||||
return dict(message="Cannot find specified pending certificate"), 404
|
||||
|
||||
# allow creators
|
||||
if g.current_user != pending_cert.user:
|
||||
owner_role = role_service.get_by_name(pending_cert.owner)
|
||||
permission = CertificatePermission(owner_role, [x.name for x in pending_cert.roles])
|
||||
|
||||
if not permission.can():
|
||||
return dict(message='You are not authorized to update this certificate'), 403
|
||||
|
||||
for destination in data['destinations']:
|
||||
if destination.plugin.requires_key:
|
||||
if not pending_cert.private_key:
|
||||
return dict(
|
||||
message='Unable to add destination: {0}. Certificate does not have required private key.'.format(
|
||||
destination.label
|
||||
)
|
||||
), 400
|
||||
|
||||
pending_cert = service.update(pending_certificate_id, **data)
|
||||
return pending_cert
|
||||
|
||||
@validate_schema(pending_certificate_cancel_schema, None)
|
||||
def delete(self, pending_certificate_id, data=None):
|
||||
"""
|
||||
.. http:delete:: /pending_certificates/1
|
||||
|
||||
Cancel and delete a pending certificate
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
DELETE /pending certificates/1 HTTP/1.1
|
||||
Host: example.com
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
{
|
||||
"note": "Why I am cancelling this order"
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 204 No Content
|
||||
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 204: no error
|
||||
:statuscode 401: unauthenticated
|
||||
:statuscode 403: unauthorized
|
||||
:statuscode 404: pending certificate id not found
|
||||
:statuscode 500: internal error
|
||||
"""
|
||||
pending_cert = service.get(pending_certificate_id)
|
||||
|
||||
if not pending_cert:
|
||||
return dict(message="Cannot find specified pending certificate"), 404
|
||||
|
||||
# allow creators
|
||||
if g.current_user != pending_cert.user:
|
||||
owner_role = role_service.get_by_name(pending_cert.owner)
|
||||
permission = CertificatePermission(owner_role, [x.name for x in pending_cert.roles])
|
||||
|
||||
if not permission.can():
|
||||
return dict(message='You are not authorized to update this certificate'), 403
|
||||
|
||||
if service.cancel(pending_cert, **data):
|
||||
service.delete(pending_cert)
|
||||
return('', 204)
|
||||
else:
|
||||
# service.cancel raises exception if there was an issue, but this will ensure something
|
||||
# is relayed to user in case of something unexpected (unsuccessful update somehow).
|
||||
return dict(message="Unexpected error occurred while trying to cancel this certificate"), 500
|
||||
|
||||
|
||||
class PendingCertificatePrivateKey(AuthenticatedResource):
|
||||
def __init__(self):
|
||||
super(PendingCertificatePrivateKey, self).__init__()
|
||||
|
||||
def get(self, pending_certificate_id):
|
||||
"""
|
||||
.. http:get:: /pending_certificates/1/key
|
||||
|
||||
Retrieves the private key for a given pneding certificate
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /pending_certificates/1/key HTTP/1.1
|
||||
Host: example.com
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"key": "-----BEGIN ..."
|
||||
}
|
||||
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
"""
|
||||
cert = service.get(pending_certificate_id)
|
||||
if not cert:
|
||||
return dict(message="Cannot find specified pending certificate"), 404
|
||||
|
||||
# allow creators
|
||||
if g.current_user != cert.user:
|
||||
owner_role = role_service.get_by_name(cert.owner)
|
||||
permission = CertificatePermission(owner_role, [x.name for x in cert.roles])
|
||||
|
||||
if not permission.can():
|
||||
return dict(message='You are not authorized to view this key'), 403
|
||||
|
||||
response = make_response(jsonify(key=cert.private_key), 200)
|
||||
response.headers['cache-control'] = 'private, max-age=0, no-cache, no-store'
|
||||
response.headers['pragma'] = 'no-cache'
|
||||
return response
|
||||
|
||||
|
||||
api.add_resource(PendingCertificatesList, '/pending_certificates', endpoint='pending_certificates')
|
||||
api.add_resource(PendingCertificates, '/pending_certificates/<int:pending_certificate_id>', endpoint='pending_certificate')
|
||||
api.add_resource(PendingCertificatePrivateKey, '/pending_certificates/<int:pending_certificate_id>/key', endpoint='privateKeyPendingCertificates')
|
@ -24,3 +24,9 @@ class IssuerPlugin(Plugin):
|
||||
|
||||
def revoke_certificate(self, certificate, comments):
|
||||
raise NotImplementedError
|
||||
|
||||
def get_ordered_certificate(self, order_id):
|
||||
raise NotImplementedError
|
||||
|
||||
def cancel_ordered_certificate(self, pending_cert, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
76
lemur/plugins/lemur_acme/cloudflare.py
Normal file
76
lemur/plugins/lemur_acme/cloudflare.py
Normal 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)
|
@ -25,8 +25,6 @@ from lemur.common.utils import validate_conf
|
||||
from lemur.plugins.bases import IssuerPlugin
|
||||
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):
|
||||
for combo in authz.body.resolved_combinations:
|
||||
@ -45,12 +43,13 @@ class AuthorizationRecord(object):
|
||||
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)
|
||||
|
||||
[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(acme_client.key),
|
||||
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):
|
||||
wait_for_r53_change(authz_record.change_id, account_number=account_number)
|
||||
def complete_dns_challenge(acme_client, account_number, authz_record, dns_provider):
|
||||
dns_provider.wait_for_dns_change(authz_record.change_id, account_number=account_number)
|
||||
|
||||
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
|
||||
).decode('utf-8')
|
||||
|
||||
# https://github.com/alex/letsencrypt-aws/commit/853ea7f93f141fe18d9ef12aee6b3388f98b4830
|
||||
pem_certificate_chain = b"\n".join(
|
||||
OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, cert)
|
||||
pem_certificate_chain = "\n".join(
|
||||
OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, cert.decode("utf-8"))
|
||||
for cert in acme_client.fetch_chain(cert_response)
|
||||
).decode('utf-8')
|
||||
|
||||
current_app.logger.debug("{0} {1}".format(type(pem_certificate). type(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'))
|
||||
|
||||
current_app.logger.debug("Connecting with directory at {0}".format(directory_url))
|
||||
client = Client(directory_url, key)
|
||||
|
||||
registration = client.register(
|
||||
messages.NewRegistration.from_data(email=email)
|
||||
)
|
||||
|
||||
current_app.logger.debug("Connected: {0}".format(registration.uri))
|
||||
|
||||
client.agree_to_tos(registration)
|
||||
return client, registration
|
||||
|
||||
@ -129,26 +131,30 @@ def get_domains(options):
|
||||
:param options:
|
||||
:return:
|
||||
"""
|
||||
current_app.logger.debug("Fetching domains")
|
||||
|
||||
domains = [options['common_name']]
|
||||
if options.get('extensions'):
|
||||
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
|
||||
|
||||
|
||||
def get_authorizations(acme_client, account_number, domains):
|
||||
def get_authorizations(acme_client, account_number, domains, dns_provider):
|
||||
authorizations = []
|
||||
try:
|
||||
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)
|
||||
|
||||
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:
|
||||
for authz_record in authorizations:
|
||||
dns_challenge = authz_record.dns_challenge
|
||||
delete_txt_record(
|
||||
dns_provider.delete_txt_record(
|
||||
authz_record.change_id,
|
||||
account_number,
|
||||
dns_challenge.validation_domain_name(authz_record.host),
|
||||
@ -177,6 +183,9 @@ class ACMEIssuerPlugin(IssuerPlugin):
|
||||
]
|
||||
|
||||
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)
|
||||
|
||||
def create_certificate(self, csr, issuer_options):
|
||||
@ -191,7 +200,7 @@ class ACMEIssuerPlugin(IssuerPlugin):
|
||||
acme_client, registration = setup_acme_client()
|
||||
account_number = current_app.config.get('ACME_AWS_ACCOUNT_NUMBER')
|
||||
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)
|
||||
# TODO add external ID (if possible)
|
||||
return pem_certificate, pem_certificate_chain, None
|
||||
|
@ -3,7 +3,7 @@ from lemur.plugins.lemur_aws.sts import sts_client
|
||||
|
||||
|
||||
@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
|
||||
|
||||
while True:
|
||||
|
@ -258,8 +258,7 @@ class DigiCertIssuerPlugin(IssuerPlugin):
|
||||
"""Wrap the Digicert Issuer API."""
|
||||
title = 'DigiCert'
|
||||
slug = 'digicert-issuer'
|
||||
description = "Enables the creation of certificates by"
|
||||
"the DigiCert REST API."
|
||||
description = "Enables the creation of certificates by the DigiCert REST API."
|
||||
version = digicert.VERSION
|
||||
|
||||
author = 'Kevin Glisson'
|
||||
@ -326,6 +325,39 @@ class DigiCertIssuerPlugin(IssuerPlugin):
|
||||
response = self.session.put(create_url, data=json.dumps({'comments': comments}))
|
||||
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
|
||||
def create_authority(options):
|
||||
"""Create an authority.
|
||||
|
@ -1,6 +1,8 @@
|
||||
import pytest
|
||||
import arrow
|
||||
import json
|
||||
from unittest.mock import patch
|
||||
|
||||
from freezegun import freeze_time
|
||||
|
||||
from lemur.tests.vectors import CSR_STR
|
||||
@ -175,3 +177,22 @@ ghi
|
||||
|
||||
assert cert == "-----BEGIN CERTIFICATE-----\nabc\n-----END CERTIFICATE-----"
|
||||
assert intermediate == "-----BEGIN CERTIFICATE-----\ndef\n-----END CERTIFICATE-----"
|
||||
|
||||
|
||||
@patch("lemur.pending_certificates.models.PendingCertificate")
|
||||
def test_cancel_ordered_certificate(mock_pending_cert):
|
||||
import requests_mock
|
||||
from lemur.plugins.lemur_digicert.plugin import DigiCertIssuerPlugin
|
||||
|
||||
mock_pending_cert.external_id = 1234
|
||||
subject = DigiCertIssuerPlugin()
|
||||
adapter = requests_mock.Adapter()
|
||||
adapter.register_uri('PUT', 'mock://www.digicert.com/services/v2/order/certificate/1234/status', status_code=204)
|
||||
adapter.register_uri('PUT', 'mock://www.digicert.com/services/v2/order/certificate/111/status', status_code=404)
|
||||
subject.session.mount('mock', adapter)
|
||||
data = {'note': 'Test'}
|
||||
subject.cancel_ordered_certificate(mock_pending_cert, **data)
|
||||
|
||||
# A non-existing order id, does not raise exception because if it doesn't exist, then it doesn't matter
|
||||
mock_pending_cert.external_id = 111
|
||||
subject.cancel_ordered_certificate(mock_pending_cert, **data)
|
||||
|
@ -14,7 +14,8 @@ from sqlalchemy import Boolean, Column, Integer, String, Text, ForeignKey
|
||||
|
||||
from lemur.database import db
|
||||
from lemur.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):
|
||||
@ -30,6 +31,7 @@ class Role(db.Model):
|
||||
third_party = Column(Boolean)
|
||||
users = relationship("User", secondary=roles_users, passive_deletes=True, backref="role")
|
||||
certificates = relationship("Certificate", secondary=roles_certificates, backref="role")
|
||||
pending_certificates = relationship("PendingCertificate", secondary=pending_cert_role_associations, backref="role")
|
||||
|
||||
sensitive_fields = ('password',)
|
||||
|
||||
|
@ -140,6 +140,8 @@ def sync_certificates(source, user):
|
||||
for e in exists:
|
||||
if certificate.get('external_id'):
|
||||
e.external_id = certificate['external_id']
|
||||
if certificate.get('authority_id'):
|
||||
e.authority_id = certificate['authority_id']
|
||||
certificate_update(e, source)
|
||||
updated += 1
|
||||
|
||||
|
@ -0,0 +1,25 @@
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" ng-click="exit()" aria-label="Exit"><span artia-hidden="true">×</span>
|
||||
</button>
|
||||
<h3 class="modal-title">Cancel <span class="text-muted"><small>{{ pendingCertificate.name }}</small></span></h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="cancelForm" class="form-horizontal" role="form" novalidate>
|
||||
<div class="form-group"
|
||||
ng-class="{'has-error': cancelForm.note.$invalid, 'has-success': !cancelForm.note.$invalid&&cancelForm.note.$dirty}">
|
||||
<label class="control-label col-sm-2">
|
||||
Note for Cancelling
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea name="note" ng-model="cancelOptions.note" placeholder="Reason for cancelling"
|
||||
class="form-control" required></textarea>
|
||||
<p ng-show="cancelForm.note.$invalid && !cancelForm.note.$pristine" class="help-block">You must give a
|
||||
reason for cancelling a pending certificate order.</p>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" ng-click="cancel(pendingCertificate, cancelOptions)" ng-disabled="cancelForm.$invalid" class="btn btn-success">Cancel Certificate
|
||||
</button>
|
||||
<button ng-click="exit()" class="btn btn-danger">Exit</button>
|
||||
</div>
|
@ -0,0 +1,29 @@
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-2">
|
||||
Destinations
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<div class="input-group">
|
||||
<input type="text" ng-model="pendingCertificate.selectedDestination" placeholder="AWS..."
|
||||
uib-typeahead="destination.label for destination in destinationService.findDestinationsByName($viewValue)" typeahead-loading="loadingDestinations"
|
||||
class="form-control input-md" typeahead-on-select="pendingCertificate.attachDestination($item)"
|
||||
uib-tooltip="Lemur can upload certificates to any pre-defined destination"
|
||||
uib-tooltip-trigger="focus" uib-tooltip-placement="top"
|
||||
typeahead-wait-ms="500">
|
||||
<span class="input-group-btn">
|
||||
<button ng-model="destinations.show" class="btn btn-md btn-default" uib-btn-checkbox btn-checkbox-true="1" btn-checkbox-false="0">
|
||||
<span class="badge">{{ pendingCertificate.destinations.length || 0 }}</span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<table class="table">
|
||||
<tr ng-repeat="destination in pendingCertificate.destinations track by $index">
|
||||
<td><a class="btn btn-sm btn-info" href="#/destinations/{{ destination.id }}/pendingCertificates">{{ destination.label }}</a></td>
|
||||
<td><span class="text-muted">{{ destination.description }}</span></td>
|
||||
<td>
|
||||
<button type="button" ng-click="pendingCertificate.removeDestination($index)" class="btn btn-danger btn-sm pull-right">Remove</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,50 @@
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" ng-click="cancel()" aria-label="Close"><span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<h3 class="modal-title">Edit <span class="text-muted"><small>{{ pendingCertificate.name }}</small></span></h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="editForm" class="form-horizontal" role="form" novalidate>
|
||||
<div class="form-group"
|
||||
ng-class="{'has-error': editForm.owner.$invalid, 'has-success': !editForm.owner.$invalid&&editForm.owner.$dirty}">
|
||||
<label class="control-label col-sm-2">
|
||||
Owner
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="email" name="owner" ng-model="pendingCertificate.owner" placeholder="owner@example.com"
|
||||
class="form-control" required/>
|
||||
|
||||
<p ng-show="editForm.owner.$invalid && !editForm.owner.$pristine" class="help-block">Enter a valid
|
||||
email.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group"
|
||||
ng-class="{'has-error': editForm.description.$invalid, 'has-success': !editForm.$invalid&&editForm.description.$dirty}">
|
||||
<label class="control-label col-sm-2">
|
||||
Description
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea name="description" ng-model="pendingCertificate.description" placeholder="Something elegant"
|
||||
class="form-control" required></textarea>
|
||||
<p ng-show="editForm.description.$invalid && !editForm.description.$pristine" class="help-block">You must give a
|
||||
short description about this authority will be used for, this description should only include alphanumeric
|
||||
characters</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-2">
|
||||
Roles
|
||||
</label>
|
||||
<div class="col-sm-10" ng-model="pendingCertificate" role-select></div>
|
||||
</div>
|
||||
<div ng-include="'angular/pending_certificates/pending_certificate/replaces.tpl.html'"></div>
|
||||
<div ng-include="'angular/pending_certificates/pending_certificate/notifications.tpl.html'"></div>
|
||||
|
||||
<div ng-include="'angular/pending_certificates/pending_certificate/destinations.tpl.html'"></div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" ng-click="save(pendingCertificate)" ng-disabled="editForm.$invalid" class="btn btn-success">Save
|
||||
</button>
|
||||
<button ng-click="cancel()" class="btn btn-danger">Cancel</button>
|
||||
</div>
|
@ -0,0 +1,28 @@
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-2">
|
||||
Notifications
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<div class="input-group">
|
||||
<input type="text" ng-model="pendingCertificate.selectedNotification" placeholder="Email"
|
||||
uib-typeahead="notification.label for notification in notificationService.findNotificationsByName($viewValue)" typeahead-loading="loadingDestinations"
|
||||
class="form-control input-md" typeahead-on-select="pendingCertificate.attachNotification($item)"
|
||||
uib-tooltip="By default Lemur will always notify you about this (pending) certificate through Email notifications."
|
||||
uib-tooltip-trigger="focus" tooltip-placement="top" typeahead-wait-ms="500">
|
||||
<span class="input-group-btn">
|
||||
<button ng-model="notifications.show" class="btn btn-md btn-default" uib-btn-checkbox btn-checkbox-true="1" btn-checkbox-false="0">
|
||||
<span class="badge">{{ pendingCertificate.notifications.length || 0 }}</span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<table class="table">
|
||||
<tr ng-repeat="notification in pendingCertificate.notifications track by $index">
|
||||
<td><a class="btn btn-sm btn-info" href="#/notifications/{{ notification.id }}/pendingCertificates">{{ notification.label }}</a></td>
|
||||
<td><span class="text-muted">{{ notification.description }}</span></td>
|
||||
<td>
|
||||
<button type="button" ng-click="pendingCertificate.removeNotification($index)" class="btn btn-danger btn-sm pull-right">Remove</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
72
lemur/static/app/angular/pending_certificates/pending_certificate/pending_certificate.js
vendored
Normal file
72
lemur/static/app/angular/pending_certificates/pending_certificate/pending_certificate.js
vendored
Normal file
@ -0,0 +1,72 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('lemur')
|
||||
.controller('PendingCertificateEditController', function ($scope, $uibModalInstance, PendingCertificateApi, PendingCertificateService, CertificateService, DestinationService, NotificationService, toaster, editId) {
|
||||
PendingCertificateApi.get(editId).then(function (pendingCertificate) {
|
||||
$scope.pendingCertificate = pendingCertificate;
|
||||
});
|
||||
|
||||
$scope.cancel = function () {
|
||||
$uibModalInstance.dismiss('cancel');
|
||||
};
|
||||
|
||||
$scope.save = function (pendingCertificate) {
|
||||
PendingCertificateService.update(pendingCertificate).then(
|
||||
function () {
|
||||
toaster.pop({
|
||||
type: 'success',
|
||||
title: pendingCertificate.name,
|
||||
body: 'Successfully updated!'
|
||||
});
|
||||
$uibModalInstance.close();
|
||||
},
|
||||
function (response) {
|
||||
toaster.pop({
|
||||
type: 'error',
|
||||
title: pendingCertificate.name,
|
||||
body: 'lemur-bad-request',
|
||||
bodyOutputType: 'directive',
|
||||
directiveData: response.data,
|
||||
timeout: 100000
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.pendingCertificateService = PendingCertificateService;
|
||||
$scope.certificateService = CertificateService;
|
||||
$scope.destinationService = DestinationService;
|
||||
$scope.notificationService = NotificationService;
|
||||
})
|
||||
.controller('PendingCertificateCancelController', function ($scope, $uibModalInstance, PendingCertificateApi, PendingCertificateService, toaster, cancelId) {
|
||||
PendingCertificateApi.get(cancelId).then(function (pendingCertificate) {
|
||||
$scope.pendingCertificate = pendingCertificate;
|
||||
});
|
||||
|
||||
$scope.exit = function () {
|
||||
$uibModalInstance.dismiss('cancel');
|
||||
};
|
||||
|
||||
$scope.cancel = function (pendingCertificate, cancelOptions) {
|
||||
PendingCertificateService.cancel(pendingCertificate, cancelOptions).then(
|
||||
function () {
|
||||
toaster.pop({
|
||||
type: 'success',
|
||||
title: pendingCertificate.name,
|
||||
body: 'Successfully cancelled pending certificate!'
|
||||
});
|
||||
$uibModalInstance.close();
|
||||
},
|
||||
function (response) {
|
||||
toaster.pop({
|
||||
type: 'error',
|
||||
title: pendingCertificate.name,
|
||||
body: 'lemur-bad-request',
|
||||
bodyOutputType: 'directive',
|
||||
directiveData: response.data,
|
||||
timeout: 100000
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
});
|
@ -0,0 +1,28 @@
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-2">
|
||||
Replaces
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<div class="input-group">
|
||||
<input type="text" ng-model="pendingCertificate.selectedReplaces" placeholder="Certificate123..."
|
||||
uib-typeahead="certificate.name for certificate in certificateService.findCertificatesByName($viewValue)" typeahead-loading="loadingCertificates"
|
||||
class="form-control input-md" typeahead-on-select="pendingCertificate.attachReplaces($item)"
|
||||
uib-tooltip="Lemur will mark any certificates being replaced as 'inactive'"
|
||||
uib-tooltip-trigger="focus" uib-tooltip-placement="top" typeahead-wait-ms="500">
|
||||
<span class="input-group-btn">
|
||||
<button ng-model="replaces.show" class="btn btn-md btn-default" uib-btn-checkbox btn-checkbox-true="1" btn-checkbox-false="0">
|
||||
<span class="badge">{{ pendingCertificate.replaces.length || 0 }}</span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<table class="table">
|
||||
<tr ng-repeat="replaces in pendingCertificate.replaces track by $index">
|
||||
<td><a class="btn btn-sm btn-info">{{ replaces.name }}</a></td>
|
||||
<td><span class="text-muted">{{ replaces.description }}</span></td>
|
||||
<td>
|
||||
<button type="button" ng-click="pendingCertificate.removeReplaces($index)" class="btn btn-danger btn-sm pull-right">Remove</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
249
lemur/static/app/angular/pending_certificates/services.js
vendored
Normal file
249
lemur/static/app/angular/pending_certificates/services.js
vendored
Normal file
@ -0,0 +1,249 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('lemur')
|
||||
.service('PendingCertificateApi', function (LemurRestangular, DomainService) {
|
||||
LemurRestangular.extendModel('pending_certificates', function (obj) {
|
||||
return angular.extend(obj, {
|
||||
attachRole: function (role) {
|
||||
this.selectedRole = null;
|
||||
if (this.roles === undefined) {
|
||||
this.roles = [];
|
||||
}
|
||||
this.roles.push(role);
|
||||
},
|
||||
removeRole: function (index) {
|
||||
this.roles.splice(index, 1);
|
||||
},
|
||||
attachAuthority: function (authority) {
|
||||
this.authority = authority;
|
||||
this.authority.maxDate = moment(this.authority.notAfter).subtract(1, 'days').format('YYYY/MM/DD');
|
||||
},
|
||||
attachSubAltName: function () {
|
||||
if (this.extensions === undefined) {
|
||||
this.extensions = {};
|
||||
}
|
||||
|
||||
if (this.extensions.subAltNames === undefined) {
|
||||
this.extensions.subAltNames = {'names': []};
|
||||
}
|
||||
|
||||
if (!angular.isString(this.subAltType)) {
|
||||
this.subAltType = 'DNSName';
|
||||
}
|
||||
|
||||
if (angular.isString(this.subAltValue) && angular.isString(this.subAltType)) {
|
||||
this.extensions.subAltNames.names.push({'nameType': this.subAltType, 'value': this.subAltValue});
|
||||
//this.findDuplicates();
|
||||
}
|
||||
|
||||
this.subAltType = null;
|
||||
this.subAltValue = null;
|
||||
},
|
||||
removeSubAltName: function (index) {
|
||||
this.extensions.subAltNames.names.splice(index, 1);
|
||||
//this.findDuplicates();
|
||||
},
|
||||
attachCustom: function () {
|
||||
if (this.extensions === undefined) {
|
||||
this.extensions = {};
|
||||
}
|
||||
|
||||
if (this.extensions.custom === undefined) {
|
||||
this.extensions.custom = [];
|
||||
}
|
||||
|
||||
if (angular.isString(this.customOid) && angular.isString(this.customEncoding) && angular.isString(this.customValue)) {
|
||||
this.extensions.custom.push(
|
||||
{
|
||||
'oid': this.customOid,
|
||||
'isCritical': this.customIsCritical || false,
|
||||
'encoding': this.customEncoding,
|
||||
'value': this.customValue
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
this.customOid = null;
|
||||
this.customIsCritical = null;
|
||||
this.customEncoding = null;
|
||||
this.customValue = null;
|
||||
},
|
||||
removeCustom: function (index) {
|
||||
this.extensions.custom.splice(index, 1);
|
||||
},
|
||||
attachDestination: function (destination) {
|
||||
this.selectedDestination = null;
|
||||
if (this.destinations === undefined) {
|
||||
this.destinations = [];
|
||||
}
|
||||
this.destinations.push(destination);
|
||||
},
|
||||
removeDestination: function (index) {
|
||||
this.destinations.splice(index, 1);
|
||||
},
|
||||
attachReplaces: function (replaces) {
|
||||
this.selectedReplaces = null;
|
||||
if (this.replaces === undefined) {
|
||||
this.replaces = [];
|
||||
}
|
||||
this.replaces.push(replaces);
|
||||
},
|
||||
removeReplaces: function (index) {
|
||||
this.replaces.splice(index, 1);
|
||||
},
|
||||
attachNotification: function (notification) {
|
||||
this.selectedNotification = null;
|
||||
if (this.notifications === undefined) {
|
||||
this.notifications = [];
|
||||
}
|
||||
this.notifications.push(notification);
|
||||
},
|
||||
removeNotification: function (index) {
|
||||
this.notifications.splice(index, 1);
|
||||
},
|
||||
findDuplicates: function () {
|
||||
DomainService.findDomainByName(this.extensions.subAltNames[0]).then(function (domains) { //We should do a better job of searching for multiple domains
|
||||
this.duplicates = domains.total;
|
||||
});
|
||||
},
|
||||
useTemplate: function () {
|
||||
if (this.extensions === undefined) {
|
||||
this.extensions = {};
|
||||
}
|
||||
|
||||
if (this.extensions.subAltNames === undefined) {
|
||||
this.extensions.subAltNames = {'names': []};
|
||||
}
|
||||
|
||||
var saveSubAltNames = this.extensions.subAltNames;
|
||||
this.extensions = this.template.extensions;
|
||||
this.extensions.subAltNames = saveSubAltNames;
|
||||
},
|
||||
setEncipherOrDecipher: function (value) {
|
||||
if (this.extensions === undefined) {
|
||||
this.extensions = {};
|
||||
}
|
||||
if (this.extensions.keyUsage === undefined) {
|
||||
this.extensions.keyUsage = {};
|
||||
}
|
||||
var existingValue = this.extensions.keyUsage[value];
|
||||
if (existingValue) {
|
||||
// Clicked on the already-selected value
|
||||
this.extensions.keyUsage.useDecipherOnly = false;
|
||||
this.extensions.keyUsage.useEncipherOnly = false;
|
||||
// Uncheck both radio buttons
|
||||
this.encipherOrDecipher = false;
|
||||
} else {
|
||||
// Clicked a different value
|
||||
this.extensions.keyUsage.useKeyAgreement = true;
|
||||
if (value === 'useEncipherOnly') {
|
||||
this.extensions.keyUsage.useDecipherOnly = false;
|
||||
this.extensions.keyUsage.useEncipherOnly = true;
|
||||
} else {
|
||||
this.extensions.keyUsage.useEncipherOnly = false;
|
||||
this.extensions.keyUsage.useDecipherOnly = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
return LemurRestangular.all('pending_certificates');
|
||||
})
|
||||
.service('PendingCertificateService', function ($location, PendingCertificateApi, AuthorityService, AuthorityApi, LemurRestangular, DefaultService) {
|
||||
var PendingCertificateService = this;
|
||||
PendingCertificateService.findPendingCertificatesByName = function (filterValue) {
|
||||
return PendingCertificateApi.getList({'filter[name]': filterValue})
|
||||
.then(function (pendingCertificates) {
|
||||
return pendingCertificates;
|
||||
});
|
||||
};
|
||||
|
||||
PendingCertificateService.update = function (pendingCertificate) {
|
||||
return LemurRestangular.copy(pendingCertificate).put();
|
||||
};
|
||||
|
||||
PendingCertificateService.getAuthority = function (certificate) {
|
||||
return certificate.customGET('authority').then(function (authority) {
|
||||
certificate.authority = authority;
|
||||
});
|
||||
};
|
||||
|
||||
PendingCertificateService.getCreator = function (certificate) {
|
||||
return certificate.customGET('creator').then(function (creator) {
|
||||
certificate.creator = creator;
|
||||
});
|
||||
};
|
||||
|
||||
PendingCertificateService.getDestinations = function (certificate) {
|
||||
return certificate.getList('destinations').then(function (destinations) {
|
||||
certificate.destinations = destinations;
|
||||
});
|
||||
};
|
||||
|
||||
PendingCertificateService.getNotifications = function (certificate) {
|
||||
return certificate.getList('notifications').then(function (notifications) {
|
||||
certificate.notifications = notifications;
|
||||
});
|
||||
};
|
||||
|
||||
PendingCertificateService.getDomains = function (certificate) {
|
||||
return certificate.getList('domains').then(function (domains) {
|
||||
certificate.domains = domains;
|
||||
});
|
||||
};
|
||||
|
||||
PendingCertificateService.getReplaces = function (certificate) {
|
||||
return certificate.getList('replaces').then(function (replaces) {
|
||||
certificate.replaces = replaces;
|
||||
});
|
||||
};
|
||||
|
||||
PendingCertificateService.getDefaults = function (certificate) {
|
||||
return DefaultService.get().then(function (defaults) {
|
||||
if (!certificate.country) {
|
||||
certificate.country = defaults.country;
|
||||
}
|
||||
|
||||
if (!certificate.state) {
|
||||
certificate.state = defaults.state;
|
||||
}
|
||||
|
||||
if (!certificate.location) {
|
||||
certificate.location = defaults.location;
|
||||
}
|
||||
|
||||
if (!certificate.organization) {
|
||||
certificate.organization = defaults.organization;
|
||||
}
|
||||
|
||||
if (!certificate.organizationalUnit) {
|
||||
certificate.organizationalUnit = defaults.organizationalUnit;
|
||||
}
|
||||
|
||||
if (!certificate.authority) {
|
||||
if (!defaults.authority) {
|
||||
// set the default authority
|
||||
AuthorityApi.getList().then(function(authorities) {
|
||||
certificate.authority = authorities[0];
|
||||
});
|
||||
} else {
|
||||
certificate.authority = defaults.authority;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
PendingCertificateService.loadPrivateKey = function (certificate) {
|
||||
return certificate.customGET('key');
|
||||
};
|
||||
|
||||
PendingCertificateService.updateNotify = function (certificate) {
|
||||
return certificate.put();
|
||||
};
|
||||
|
||||
PendingCertificateService.cancel = function (pending_certificate, options) {
|
||||
return pending_certificate.customOperation('remove', null, {}, {'Content-Type': 'application/json'}, options);
|
||||
};
|
||||
|
||||
return PendingCertificateService;
|
||||
});
|
102
lemur/static/app/angular/pending_certificates/view/view.js
vendored
Normal file
102
lemur/static/app/angular/pending_certificates/view/view.js
vendored
Normal file
@ -0,0 +1,102 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('lemur')
|
||||
|
||||
.config(function config($stateProvider) {
|
||||
$stateProvider
|
||||
.state('pending_certificates', {
|
||||
url: '/pending_certificates',
|
||||
templateUrl: '/angular/pending_certificates/view/view.tpl.html',
|
||||
controller: 'PendingCertificatesViewController'
|
||||
})
|
||||
.state('pending_certificate', {
|
||||
url: '/pending_certificates/:name',
|
||||
templateUrl: '/angular/pending_certificates/view/view.tpl.html',
|
||||
controller: 'PendingCertificatesViewController'
|
||||
});
|
||||
})
|
||||
|
||||
.controller('PendingCertificatesViewController', function ($q, $scope, $uibModal, $stateParams, PendingCertificateApi, PendingCertificateService, ngTableParams, toaster) {
|
||||
$scope.filter = $stateParams;
|
||||
$scope.pendingCertificateTable = new ngTableParams({
|
||||
page: 1, // show first page
|
||||
count: 10, // count per page
|
||||
sorting: {
|
||||
id: 'desc' // initial sorting
|
||||
},
|
||||
filter: $scope.filter
|
||||
}, {
|
||||
total: 0, // length of data
|
||||
getData: function ($defer, params) {
|
||||
PendingCertificateApi.getList(params.url())
|
||||
.then(function (data) {
|
||||
params.total(data.total);
|
||||
$defer.resolve(data);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$scope.edit = function (pendingCertificateId) {
|
||||
var uibModalInstance = $uibModal.open({
|
||||
animation: true,
|
||||
controller: 'PendingCertificateEditController',
|
||||
templateUrl: '/angular/pending_certificates/pending_certificate/edit.tpl.html',
|
||||
size: 'lg',
|
||||
backdrop: 'static',
|
||||
resolve: {
|
||||
editId: function () {
|
||||
return pendingCertificateId;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
uibModalInstance.result.then(function () {
|
||||
$scope.pendingCertificateTable.reload();
|
||||
});
|
||||
};
|
||||
|
||||
$scope.loadPrivateKey = function (pendingCertificate) {
|
||||
if (pendingCertificate.privateKey !== undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
PendingCertificateService.loadPrivateKey(pendingCertificate).then(
|
||||
function (response) {
|
||||
if (response.key === null) {
|
||||
toaster.pop({
|
||||
type: 'warning',
|
||||
title: pendingCertificate.name,
|
||||
body: 'No private key found!'
|
||||
});
|
||||
} else {
|
||||
pendingCertificate.privateKey = response.key;
|
||||
}
|
||||
},
|
||||
function () {
|
||||
toaster.pop({
|
||||
type: 'error',
|
||||
title: pendingCertificate.name,
|
||||
body: 'You do not have permission to view this key!',
|
||||
timeout: 100000
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.cancel = function (pendingCertificateId) {
|
||||
var uibModalInstance = $uibModal.open({
|
||||
animation: true,
|
||||
controller: 'PendingCertificateCancelController',
|
||||
templateUrl: '/angular/pending_certificates/pending_certificate/cancel.tpl.html',
|
||||
size: 'lg',
|
||||
backdrop: 'static',
|
||||
resolve: {
|
||||
cancelId: function () {
|
||||
return pendingCertificateId;
|
||||
}
|
||||
}
|
||||
});
|
||||
uibModalInstance.result.then(function () {
|
||||
$scope.pendingCertificateTable.reload();
|
||||
});
|
||||
};
|
||||
});
|
182
lemur/static/app/angular/pending_certificates/view/view.tpl.html
Normal file
182
lemur/static/app/angular/pending_certificates/view/view.tpl.html
Normal file
@ -0,0 +1,182 @@
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h2 class="featurette-heading">Pending Certificates
|
||||
<span class="text-muted"><small>Certificates that almost made it</small></span></h2>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<div class="btn-group">
|
||||
<button ng-model="showFilter" class="btn btn-default" uib-btn-checkbox
|
||||
btn-checkbox-true="1"
|
||||
btn-checkbox-false="0">Filter</button>
|
||||
</div>
|
||||
<!--<select class="form-control" ng-model="show" ng-options="item.value as item.title for item in fields"></select>-->
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table ng-table="pendingCertificateTable" class="table" show-filter="showFilter" template-pagination="angular/pager.html">
|
||||
<tbody>
|
||||
<tr ng-class="{'even-row': $even }" ng-repeat-start="pendingCertificate in $data track by $index">
|
||||
<td data-title="'Id'" filter="{'id': 'text'}">
|
||||
{{ pendingCertificate.id }}
|
||||
</td>
|
||||
<td data-title="'Name'" sortable="'name'" filter="{ 'name': 'text' }">
|
||||
<ul class="list-unstyled">
|
||||
<li>{{ pendingCertificate.name }}</li>
|
||||
<li><span class="text-muted">{{ pendingCertificate.owner }}</span></li>
|
||||
</ul>
|
||||
</td>
|
||||
<td data-title="'Notify'" filter="{ 'notify': 'select' }" filter-data="getPendingCertificateStatus()">
|
||||
<form>
|
||||
<switch ng-change="updateNotify(pendingCertificate)" id="status" name="status"
|
||||
ng-model="pendingCertificate.notify" class="green small"></switch>
|
||||
</form>
|
||||
</td>
|
||||
<td data-title="'Issuer'" sortable="'issuer'" filter="{ 'issuer': 'text' }">
|
||||
{{ pendingCertificate.authority.name || pendingCertificate.issuer }}
|
||||
</td>
|
||||
<td data-title="'Common Name'" filter="{ 'cn': 'text'}">
|
||||
{{ pendingCertificate.cn }}
|
||||
</td>
|
||||
<td data-title="''" style="text-align: center; vertical-align: middle;">
|
||||
<div class="btn-group pull-right" role="group" aria-label="...">
|
||||
<a class="btn btn-sm btn-primary" ui-sref="pending_certificate({name: pendingCertificate.name})">Permalink</a>
|
||||
<button ng-model="pendingCertificate.toggle" class="btn btn-sm btn-info" uib-btn-checkbox btn-checkbox-true="1"
|
||||
btn-checkbox-false="0">More
|
||||
</button>
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-sm btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
Action
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href ng-click="edit(pendingCertificate.id)">Edit</a></li>
|
||||
<li><a href ng-click="cancel(pendingCertificate.id)">Cancel</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="warning" ng-if="pendingCertificate.toggle" ng-repeat-end>
|
||||
<td colspan="12">
|
||||
<uib-tabset justified="true" class="col-md-8">
|
||||
<uib-tab>
|
||||
<uib-tab-heading>Basic Info</uib-tab-heading>
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item">
|
||||
<strong>Creator</strong>
|
||||
<span class="pull-right">
|
||||
{{ pendingCertificate.user.email }}
|
||||
</span>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<strong>Owner</strong>
|
||||
<span class="pull-right">
|
||||
{{ pendingCertificate.owner }}
|
||||
</span>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<strong>Number of Attempts to Fetch</strong>
|
||||
<span class="pull-right">
|
||||
{{ pendingCertificate.numberAttempts }}
|
||||
</span>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<strong>Date Created</strong>
|
||||
<span class="pull-right">
|
||||
{{ pendingCertificate.dateCreated | date:'yyyy-MM-dd HH:mm:ss Z' }}
|
||||
</span>
|
||||
</li>
|
||||
|
||||
<li class="list-group-item">
|
||||
<strong>Description</strong>
|
||||
<p>{{ pendingCertificate.description }}</p>
|
||||
</li>
|
||||
</ul>
|
||||
</uib-tab>
|
||||
<uib-tab>
|
||||
<uib-tab-heading>Endpoints</uib-tab-heading>
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item" ng-repeat="endpoint in pendingCertificate.endpoints">
|
||||
<span class="pull-right"><label class="label label-default">{{ endpoint.type }}</label></span>
|
||||
<ul class="list-unstyled">
|
||||
<li>{{ endpoint.name }}</li>
|
||||
<li><span class="text-muted">{{ endpoint.dnsname }}</span></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</uib-tab>
|
||||
<uib-tab>
|
||||
<uib-tab-heading>Notifications</uib-tab-heading>
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item" ng-repeat="notification in pendingCertificate.notifications">
|
||||
<strong>{{ notification.label }}</strong>
|
||||
<span class="pull-right">{{ notification.description}}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</uib-tab>
|
||||
<uib-tab>
|
||||
<uib-tab-heading>Roles</uib-tab-heading>
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item" ng-repeat="role in pendingCertificate.roles">
|
||||
<strong>{{ role.name }}</strong>
|
||||
<span class="pull-right">{{ role.description}}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</uib-tab>
|
||||
<uib-tab>
|
||||
<uib-tab-heading>Destinations</uib-tab-heading>
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item" ng-repeat="destination in pendingCertificate.destinations">
|
||||
<strong>{{ destination.label }}</strong>
|
||||
<span class="pull-right">{{ destination.description }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</uib-tab>
|
||||
<uib-tab>
|
||||
<uib-tab-heading>Domains</uib-tab-heading>
|
||||
<div class="list-group">
|
||||
<a href="#/domains/{{ domain.id }}" class="list-group-item"
|
||||
ng-repeat="domain in pendingCertificate.domains">{{ domain.name }}</a>
|
||||
</div>
|
||||
</uib-tab>
|
||||
<uib-tab>
|
||||
<uib-tab-heading>Replaces</uib-tab-heading>
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item" ng-repeat="replaces in pendingCertificate.replaces">
|
||||
<strong>{{ replaces.name }}</strong>
|
||||
<p>{{ replaces.description }}</p>
|
||||
</li>
|
||||
</ul>
|
||||
</uib-tab>
|
||||
</uib-tabset>
|
||||
<uib-tabset justified="true" class="col-md-4">
|
||||
<uib-tab>
|
||||
<uib-tab-heading>
|
||||
Chain
|
||||
<i class="glyphicon glyphicon-copy" style="cursor: pointer" clipboard text="pendingCertificate.chain"></i>
|
||||
</uib-tab-heading>
|
||||
<pre style="width: 100%">{{ pendingCertificate.chain }}</pre>
|
||||
</uib-tab>
|
||||
<uib-tab>
|
||||
<uib-tab-heading>
|
||||
Certificate Signing Request
|
||||
<i class="glyphicon glyphicon-copy" style="cursor: pointer" clipboard text="pendingCertificate.csr"></i>
|
||||
</uib-tab-heading>
|
||||
<pre style="width: 100%">{{ pendingCertificate.csr }}</pre>
|
||||
</uib-tab>
|
||||
<uib-tab ng-click="loadPrivateKey(pendingCertificate)">
|
||||
<uib-tab-heading>
|
||||
Private Key
|
||||
<i class="glyphicon glyphicon-copy" style="cursor: pointer" clipboard text="pendingCertificate.privateKey"></i>
|
||||
</uib-tab-heading>
|
||||
<pre style="width: 100%">{{ pendingCertificate.privateKey }}</pre>
|
||||
</uib-tab>
|
||||
</uib-tabset>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -50,6 +50,7 @@
|
||||
<ul class="nav navbar-nav navbar-left">
|
||||
<li><a ui-sref="dashboard">Dashboard</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="endpoints">Endpoints</a></li>
|
||||
<li><a ui-sref="notifications">Notifications</a></li>
|
||||
|
@ -14,7 +14,8 @@ from lemur.auth.service import create_token
|
||||
from lemur.tests.vectors import PRIVATE_KEY_STR
|
||||
|
||||
from .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):
|
||||
@ -90,6 +91,13 @@ def authority(session):
|
||||
return a
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def async_authority(session):
|
||||
a = AsyncAuthorityFactory()
|
||||
session.commit()
|
||||
return a
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def destination(session):
|
||||
d = DestinationFactory()
|
||||
@ -144,6 +152,15 @@ def user(session):
|
||||
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
|
||||
def admin_user(session):
|
||||
u = UserFactory()
|
||||
@ -155,6 +172,14 @@ def admin_user(session):
|
||||
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
|
||||
def issuer_plugin():
|
||||
from lemur.plugins.base import register
|
||||
|
@ -12,13 +12,14 @@ from lemur.certificates.models import Certificate
|
||||
from lemur.destinations.models import Destination
|
||||
from lemur.sources.models import Source
|
||||
from lemur.notifications.models import Notification
|
||||
from lemur.pending_certificates.models import PendingCertificate
|
||||
from lemur.users.models import User
|
||||
from lemur.roles.models import Role
|
||||
from lemur.endpoints.models import Policy, Endpoint
|
||||
from lemur.policies.models import RotationPolicy
|
||||
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):
|
||||
@ -140,6 +141,15 @@ class AuthorityFactory(BaseFactory):
|
||||
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):
|
||||
"""Rotation Factory."""
|
||||
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))
|
||||
active = True
|
||||
password = FuzzyText(length=24)
|
||||
certificates = []
|
||||
|
||||
class Meta:
|
||||
"""Factory Configuration."""
|
||||
@ -281,3 +292,93 @@ class ApiKeyFactory(BaseFactory):
|
||||
|
||||
if extracted:
|
||||
self.userId = extracted.id
|
||||
|
||||
|
||||
class PendingCertificateFactory(BaseFactory):
|
||||
"""PendingCertificate factory."""
|
||||
name = Sequence(lambda n: 'pending_certificate{0}'.format(n))
|
||||
external_id = 12345
|
||||
csr = CSR_STR
|
||||
chain = INTERNAL_VALID_SAN_STR
|
||||
private_key = PRIVATE_KEY_STR
|
||||
owner = 'joe@example.com'
|
||||
status = FuzzyChoice(['valid', 'revoked', 'unknown'])
|
||||
deleted = False
|
||||
description = FuzzyText(length=128)
|
||||
date_created = FuzzyDate(date(2016, 1, 1), date(2020, 1, 1))
|
||||
number_attempts = 0
|
||||
rename = False
|
||||
|
||||
class Meta:
|
||||
"""Factory Configuration."""
|
||||
model = PendingCertificate
|
||||
|
||||
@post_generation
|
||||
def user(self, create, extracted, **kwargs):
|
||||
if not create:
|
||||
return
|
||||
|
||||
if extracted:
|
||||
self.user_id = extracted.id
|
||||
|
||||
@post_generation
|
||||
def authority(self, create, extracted, **kwargs):
|
||||
if not create:
|
||||
return
|
||||
|
||||
if extracted:
|
||||
self.authority_id = extracted.id
|
||||
|
||||
@post_generation
|
||||
def notifications(self, create, extracted, **kwargs):
|
||||
if not create:
|
||||
return
|
||||
|
||||
if extracted:
|
||||
for notification in extracted:
|
||||
self.notifications.append(notification)
|
||||
|
||||
@post_generation
|
||||
def destinations(self, create, extracted, **kwargs):
|
||||
if not create:
|
||||
return
|
||||
|
||||
if extracted:
|
||||
for destination in extracted:
|
||||
self.destintations.append(destination)
|
||||
|
||||
@post_generation
|
||||
def replaces(self, create, extracted, **kwargs):
|
||||
if not create:
|
||||
return
|
||||
|
||||
if extracted:
|
||||
for replace in extracted:
|
||||
self.replaces.append(replace)
|
||||
|
||||
@post_generation
|
||||
def sources(self, create, extracted, **kwargs):
|
||||
if not create:
|
||||
return
|
||||
|
||||
if extracted:
|
||||
for source in extracted:
|
||||
self.sources.append(source)
|
||||
|
||||
@post_generation
|
||||
def domains(self, create, extracted, **kwargs):
|
||||
if not create:
|
||||
return
|
||||
|
||||
if extracted:
|
||||
for domain in extracted:
|
||||
self.domains.append(domain)
|
||||
|
||||
@post_generation
|
||||
def roles(self, create, extracted, **kwargs):
|
||||
if not create:
|
||||
return
|
||||
|
||||
if extracted:
|
||||
for domain in extracted:
|
||||
self.roles.append(domain)
|
||||
|
@ -21,3 +21,29 @@ class TestIssuerPlugin(IssuerPlugin):
|
||||
def create_authority(options):
|
||||
role = {'username': '', 'password': '', 'name': 'test'}
|
||||
return INTERNAL_VALID_SAN_STR, "", [role]
|
||||
|
||||
|
||||
class TestAsyncIssuerPlugin(IssuerPlugin):
|
||||
title = 'Test Async'
|
||||
slug = 'test-issuer-async'
|
||||
description = 'Enables testing with pending certificates'
|
||||
|
||||
author = 'James Chuong'
|
||||
author_url = 'https://github.com/jchuong'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(TestAsyncIssuerPlugin, self).__init__(*args, **kwargs)
|
||||
|
||||
def create_certificate(self, csr, issuer_options):
|
||||
return "", "", 12345
|
||||
|
||||
def get_ordered_certificate(self, order_id):
|
||||
return INTERNAL_VALID_LONG_STR, INTERNAL_VALID_SAN_STR, 54321
|
||||
|
||||
@staticmethod
|
||||
def create_authority(options):
|
||||
role = {'username': '', 'password': '', 'name': 'test'}
|
||||
return INTERNAL_VALID_SAN_STR, "", [role]
|
||||
|
||||
def cancel_ordered_certificate(self, pending_certificate, **kwargs):
|
||||
return True
|
||||
|
@ -436,7 +436,7 @@ def test_get_account_number(client):
|
||||
|
||||
def test_mint_certificate(issuer_plugin, authority):
|
||||
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
|
||||
|
||||
|
||||
|
51
lemur/tests/test_pending_certificates.py
Normal file
51
lemur/tests/test_pending_certificates.py
Normal file
@ -0,0 +1,51 @@
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from .vectors import CSR_STR, INTERNAL_VALID_LONG_STR, VALID_ADMIN_API_TOKEN, VALID_ADMIN_HEADER_TOKEN, VALID_USER_HEADER_TOKEN
|
||||
|
||||
from lemur.pending_certificates.views import * # noqa
|
||||
|
||||
|
||||
def test_increment_attempt(pending_certificate):
|
||||
from lemur.pending_certificates.service import increment_attempt
|
||||
initial_attempt = pending_certificate.number_attempts
|
||||
attempts = increment_attempt(pending_certificate)
|
||||
assert attempts == initial_attempt + 1
|
||||
|
||||
|
||||
def test_create_pending_certificate(async_issuer_plugin, async_authority, user):
|
||||
from lemur.certificates.service import create
|
||||
pending_cert = create(authority=async_authority, csr=CSR_STR, owner='joe@example.com', creator=user['user'], common_name='ACommonName')
|
||||
assert pending_cert.external_id == '12345'
|
||||
|
||||
|
||||
def test_create_pending(pending_certificate, user, session):
|
||||
import copy
|
||||
from lemur.pending_certificates.service import create_certificate, get
|
||||
cert = {'body': INTERNAL_VALID_LONG_STR,
|
||||
'chain': None,
|
||||
'external_id': 54321}
|
||||
|
||||
# Weird copy because the session behavior. pending_certificate is a valid object but the
|
||||
# return of vars(pending_certificate) is a sessionobject, and so nothing from the pending_cert
|
||||
# is used to create the certificate. Maybe a bug due to using vars(), and should copy every
|
||||
# field explicitly.
|
||||
pending_certificate = copy.copy(get(pending_certificate.id))
|
||||
real_cert = create_certificate(pending_certificate, cert, user['user'])
|
||||
assert real_cert.owner == pending_certificate.owner
|
||||
assert real_cert.notify == pending_certificate.notify
|
||||
assert real_cert.private_key == pending_certificate.private_key
|
||||
assert real_cert.external_id == '54321'
|
||||
|
||||
|
||||
@pytest.mark.parametrize("token,status", [
|
||||
(VALID_USER_HEADER_TOKEN, 403),
|
||||
(VALID_ADMIN_HEADER_TOKEN, 204),
|
||||
(VALID_ADMIN_API_TOKEN, 204),
|
||||
('', 401)
|
||||
])
|
||||
def test_pending_cancel(client, pending_certificate, token, status):
|
||||
assert client.delete(api.url_for(PendingCertificates, pending_certificate_id=pending_certificate.id),
|
||||
data=json.dumps({'note': "unit test", 'send_email': False}),
|
||||
headers=token).status_code == status
|
@ -43,6 +43,7 @@ class User(db.Model):
|
||||
profile_picture = Column(String(255))
|
||||
roles = relationship('Role', secondary=roles_users, passive_deletes=True, 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')
|
||||
keys = relationship('ApiKey', backref=db.backref('user'), lazy='dynamic')
|
||||
logs = relationship('Log', backref=db.backref('user'), lazy='dynamic')
|
||||
|
4
setup.py
4
setup.py
@ -144,8 +144,8 @@ setup(
|
||||
'cfssl_issuer = lemur.plugins.lemur_cfssl.plugin:CfsslIssuerPlugin',
|
||||
'digicert_issuer = lemur.plugins.lemur_digicert.plugin:DigiCertIssuerPlugin',
|
||||
'digicert_cis_issuer = lemur.plugins.lemur_digicert.plugin:DigiCertCISIssuerPlugin',
|
||||
'digicert_cis_source = lemur.plugins.lemur_digicert.plugin:DigiCertCISSourcePlugin'
|
||||
'csr_export = lemur.plugins.lemur_csr.plugin:CSRExportPlugin',
|
||||
'digicert_cis_source = lemur.plugins.lemur_digicert.plugin:DigiCertCISSourcePlugin',
|
||||
'csr_export = lemur.plugins.lemur_csr.plugin:CSRExportPlugin'
|
||||
],
|
||||
},
|
||||
classifiers=[
|
||||
|
Loading…
Reference in New Issue
Block a user