Merge branch 'master' into unq-const
This commit is contained in:
commit
7032abf2e7
|
@ -100,10 +100,16 @@ If you have a third party or internal service that creates authorities (EJBCA, e
|
||||||
it can treat any issuer plugin as both a source of creating new certificates as well as new authorities.
|
it can treat any issuer plugin as both a source of creating new certificates as well as new authorities.
|
||||||
|
|
||||||
|
|
||||||
The `IssuerPlugin` exposes two functions::
|
The `IssuerPlugin` exposes four functions functions::
|
||||||
|
|
||||||
def create_certificate(self, csr, issuer_options):
|
def create_certificate(self, csr, issuer_options):
|
||||||
# requests.get('a third party')
|
# requests.get('a third party')
|
||||||
|
def revoke_certificate(self, certificate, comments):
|
||||||
|
# requests.put('a third party')
|
||||||
|
def get_ordered_certificate(self, order_id):
|
||||||
|
# requests.get('already existing certificate')
|
||||||
|
def canceled_ordered_certificate(self, pending_cert, **kwargs):
|
||||||
|
# requests.put('cancel an order that has yet to be issued')
|
||||||
|
|
||||||
Lemur will pass a dictionary of all possible options for certificate creation. Including a valid CSR, and the raw options associated with the request.
|
Lemur will pass a dictionary of all possible options for certificate creation. Including a valid CSR, and the raw options associated with the request.
|
||||||
|
|
||||||
|
@ -139,6 +145,19 @@ The `IssuerPlugin` doesn't have any options like Destination, Source, and Notifi
|
||||||
any fields you might need to submit a request to a third party. If there are additional options you need
|
any fields you might need to submit a request to a third party. If there are additional options you need
|
||||||
in your plugin feel free to open an issue, or look into adding additional options to issuers yourself.
|
in your plugin feel free to open an issue, or look into adding additional options to issuers yourself.
|
||||||
|
|
||||||
|
Asynchronous Certificates
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
An issuer may take some time to actually issue a certificate for an order. In this case, a `PendingCertificate` is returned, which holds information to recreate a `Certificate` object at a later time. Then, `get_ordered_certificate()` should be run periodically via `python manage.py pending_certs fetch -i all` to attempt to retrieve an ordered certificate::
|
||||||
|
|
||||||
|
def get_ordered_ceriticate(self, order_id):
|
||||||
|
# order_id is the external id of the order, not the external_id of the certificate
|
||||||
|
# retrieve an order, and check if there is an issued certificate attached to it
|
||||||
|
|
||||||
|
`cancel_ordered_certificate()` should be implemented to allow an ordered certificate to be canceled before it is issued::
|
||||||
|
def cancel_ordered_certificate(self, pending_cert, **kwargs):
|
||||||
|
# pending_cert should contain the necessary information to match an order
|
||||||
|
# kwargs can be given to provide information to the issuer for canceling
|
||||||
|
|
||||||
Destination
|
Destination
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
CloudFlare==1.7.5
|
||||||
Flask==0.12
|
Flask==0.12
|
||||||
Flask-RESTful==0.3.6
|
Flask-RESTful==0.3.6
|
||||||
Flask-SQLAlchemy==2.1
|
Flask-SQLAlchemy==2.1
|
||||||
|
|
|
@ -27,6 +27,7 @@ from lemur.sources.views import mod as sources_bp
|
||||||
from lemur.endpoints.views import mod as endpoints_bp
|
from lemur.endpoints.views import mod as endpoints_bp
|
||||||
from lemur.logs.views import mod as logs_bp
|
from lemur.logs.views import mod as logs_bp
|
||||||
from lemur.api_keys.views import mod as api_key_bp
|
from lemur.api_keys.views import mod as api_key_bp
|
||||||
|
from lemur.pending_certificates.views import mod as pending_certificates_bp
|
||||||
|
|
||||||
from lemur.__about__ import (
|
from lemur.__about__ import (
|
||||||
__author__, __copyright__, __email__, __license__, __summary__, __title__,
|
__author__, __copyright__, __email__, __license__, __summary__, __title__,
|
||||||
|
@ -53,7 +54,8 @@ LEMUR_BLUEPRINTS = (
|
||||||
sources_bp,
|
sources_bp,
|
||||||
endpoints_bp,
|
endpoints_bp,
|
||||||
logs_bp,
|
logs_bp,
|
||||||
api_key_bp
|
api_key_bp,
|
||||||
|
pending_certificates_bp,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -32,6 +32,9 @@ class Authority(db.Model):
|
||||||
authority_certificate = relationship("Certificate", backref='root_authority', uselist=False, foreign_keys='Certificate.root_authority_id')
|
authority_certificate = relationship("Certificate", backref='root_authority', uselist=False, foreign_keys='Certificate.root_authority_id')
|
||||||
certificates = relationship("Certificate", backref='authority', foreign_keys='Certificate.authority_id')
|
certificates = relationship("Certificate", backref='authority', foreign_keys='Certificate.authority_id')
|
||||||
|
|
||||||
|
authority_pending_certificate = relationship("PendingCertificate", backref='root_authority', uselist=False, foreign_keys='PendingCertificate.root_authority_id')
|
||||||
|
pending_certificates = relationship('PendingCertificate', backref='authority', foreign_keys='PendingCertificate.authority_id')
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
self.owner = kwargs['owner']
|
self.owner = kwargs['owner']
|
||||||
self.roles = kwargs.get('roles', [])
|
self.roles = kwargs.get('roles', [])
|
||||||
|
|
|
@ -37,7 +37,7 @@ from lemur.constants import SUCCESS_METRIC_STATUS, FAILURE_METRIC_STATUS
|
||||||
|
|
||||||
from lemur.models import certificate_associations, certificate_source_associations, \
|
from lemur.models import certificate_associations, certificate_source_associations, \
|
||||||
certificate_destination_associations, certificate_notification_associations, \
|
certificate_destination_associations, certificate_notification_associations, \
|
||||||
certificate_replacement_associations, roles_certificates
|
certificate_replacement_associations, roles_certificates, pending_cert_replacement_associations
|
||||||
|
|
||||||
from lemur.domains.models import Domain
|
from lemur.domains.models import Domain
|
||||||
from lemur.policies.models import RotationPolicy
|
from lemur.policies.models import RotationPolicy
|
||||||
|
@ -129,6 +129,11 @@ class Certificate(db.Model):
|
||||||
secondaryjoin=id == certificate_replacement_associations.c.replaced_certificate_id, # noqa
|
secondaryjoin=id == certificate_replacement_associations.c.replaced_certificate_id, # noqa
|
||||||
backref='replaced')
|
backref='replaced')
|
||||||
|
|
||||||
|
replaced_by_pending = relationship('PendingCertificate',
|
||||||
|
secondary=pending_cert_replacement_associations,
|
||||||
|
backref='pending_replace',
|
||||||
|
viewonly=True)
|
||||||
|
|
||||||
logs = relationship('Log', backref='certificate')
|
logs = relationship('Log', backref='certificate')
|
||||||
endpoints = relationship('Endpoint', backref='certificate')
|
endpoints = relationship('Endpoint', backref='certificate')
|
||||||
rotation_policy = relationship("RotationPolicy")
|
rotation_policy = relationship("RotationPolicy")
|
||||||
|
|
|
@ -25,6 +25,7 @@ from lemur.authorities.models import Authority
|
||||||
from lemur.destinations.models import Destination
|
from lemur.destinations.models import Destination
|
||||||
from lemur.certificates.models import Certificate
|
from lemur.certificates.models import Certificate
|
||||||
from lemur.notifications.models import Notification
|
from lemur.notifications.models import Notification
|
||||||
|
from lemur.pending_certificates.models import PendingCertificate
|
||||||
|
|
||||||
from lemur.certificates.schemas import CertificateOutputSchema, CertificateInputSchema
|
from lemur.certificates.schemas import CertificateOutputSchema, CertificateInputSchema
|
||||||
|
|
||||||
|
@ -63,6 +64,9 @@ def get_by_serial(serial):
|
||||||
:param serial:
|
:param serial:
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
|
if isinstance(serial, int):
|
||||||
|
# although serial is a number, the DB column is String(128)
|
||||||
|
serial = str(serial)
|
||||||
return Certificate.query.filter(Certificate.serial == serial).all()
|
return Certificate.query.filter(Certificate.serial == serial).all()
|
||||||
|
|
||||||
|
|
||||||
|
@ -190,7 +194,7 @@ def mint(**kwargs):
|
||||||
csr_imported.send(authority=authority, csr=csr)
|
csr_imported.send(authority=authority, csr=csr)
|
||||||
|
|
||||||
cert_body, cert_chain, external_id = issuer.create_certificate(csr, kwargs)
|
cert_body, cert_chain, external_id = issuer.create_certificate(csr, kwargs)
|
||||||
return cert_body, private_key, cert_chain, external_id
|
return cert_body, private_key, cert_chain, external_id, csr
|
||||||
|
|
||||||
|
|
||||||
def import_certificate(**kwargs):
|
def import_certificate(**kwargs):
|
||||||
|
@ -243,11 +247,12 @@ def create(**kwargs):
|
||||||
"""
|
"""
|
||||||
Creates a new certificate.
|
Creates a new certificate.
|
||||||
"""
|
"""
|
||||||
cert_body, private_key, cert_chain, external_id = mint(**kwargs)
|
cert_body, private_key, cert_chain, external_id, csr = mint(**kwargs)
|
||||||
kwargs['body'] = cert_body
|
kwargs['body'] = cert_body
|
||||||
kwargs['private_key'] = private_key
|
kwargs['private_key'] = private_key
|
||||||
kwargs['chain'] = cert_chain
|
kwargs['chain'] = cert_chain
|
||||||
kwargs['external_id'] = external_id
|
kwargs['external_id'] = external_id
|
||||||
|
kwargs['csr'] = csr
|
||||||
|
|
||||||
roles = create_certificate_roles(**kwargs)
|
roles = create_certificate_roles(**kwargs)
|
||||||
|
|
||||||
|
@ -256,15 +261,20 @@ def create(**kwargs):
|
||||||
else:
|
else:
|
||||||
kwargs['roles'] = roles
|
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']
|
cert.authority = kwargs['authority']
|
||||||
certificate_issued.send(certificate=cert, authority=cert.authority)
|
|
||||||
|
|
||||||
database.commit()
|
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
|
return cert
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ from lemur.auth.service import AuthenticatedResource
|
||||||
from lemur.auth.permissions import AuthorityPermission, CertificatePermission
|
from lemur.auth.permissions import AuthorityPermission, CertificatePermission
|
||||||
|
|
||||||
from lemur.certificates import service
|
from lemur.certificates import service
|
||||||
|
from lemur.certificates.models import Certificate
|
||||||
from lemur.plugins.base import plugins
|
from lemur.plugins.base import plugins
|
||||||
from lemur.certificates.schemas import (
|
from lemur.certificates.schemas import (
|
||||||
certificate_input_schema,
|
certificate_input_schema,
|
||||||
|
@ -267,7 +268,9 @@ class CertificatesList(AuthenticatedResource):
|
||||||
if authority_permission.can():
|
if authority_permission.can():
|
||||||
data['creator'] = g.user
|
data['creator'] = g.user
|
||||||
cert = service.create(**data)
|
cert = service.create(**data)
|
||||||
log_service.create(g.user, 'create_cert', certificate=cert)
|
if isinstance(cert, Certificate):
|
||||||
|
# only log if created, not pending
|
||||||
|
log_service.create(g.user, 'create_cert', 'OK', certificate=cert)
|
||||||
return cert
|
return cert
|
||||||
|
|
||||||
return dict(message="You are not authorized to use the authority: {0}".format(data['authority'].name)), 403
|
return dict(message="You are not authorized to use the authority: {0}".format(data['authority'].name)), 403
|
||||||
|
|
|
@ -53,6 +53,19 @@ def parse_certificate(body):
|
||||||
return x509.load_pem_x509_certificate(body, default_backend())
|
return x509.load_pem_x509_certificate(body, default_backend())
|
||||||
|
|
||||||
|
|
||||||
|
def parse_csr(csr):
|
||||||
|
"""
|
||||||
|
Helper function that parses a CSR.
|
||||||
|
|
||||||
|
:param csr:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if isinstance(csr, str):
|
||||||
|
csr = csr.encode('utf-8')
|
||||||
|
|
||||||
|
return x509.load_pem_x509_csr(csr, default_backend())
|
||||||
|
|
||||||
|
|
||||||
def get_authority_key(body):
|
def get_authority_key(body):
|
||||||
"""Returns the authority key for a given certificate in hex format"""
|
"""Returns the authority key for a given certificate in hex format"""
|
||||||
parsed_cert = parse_certificate(body)
|
parsed_cert = parse_certificate(body)
|
||||||
|
|
|
@ -21,6 +21,7 @@ from lemur.reporting.cli import manager as report_manager
|
||||||
from lemur.endpoints.cli import manager as endpoint_manager
|
from lemur.endpoints.cli import manager as endpoint_manager
|
||||||
from lemur.certificates.cli import manager as certificate_manager
|
from lemur.certificates.cli import manager as certificate_manager
|
||||||
from lemur.notifications.cli import manager as notification_manager
|
from lemur.notifications.cli import manager as notification_manager
|
||||||
|
from lemur.pending_certificates.cli import manager as pending_certificate_manager
|
||||||
|
|
||||||
from lemur import database
|
from lemur import database
|
||||||
from lemur.users import service as user_service
|
from lemur.users import service as user_service
|
||||||
|
@ -44,6 +45,7 @@ from lemur.sources.models import Source # noqa
|
||||||
from lemur.logs.models import Log # noqa
|
from lemur.logs.models import Log # noqa
|
||||||
from lemur.endpoints.models import Endpoint # noqa
|
from lemur.endpoints.models import Endpoint # noqa
|
||||||
from lemur.policies.models import RotationPolicy # noqa
|
from lemur.policies.models import RotationPolicy # noqa
|
||||||
|
from lemur.pending_certificates.models import PendingCertificate # noqa
|
||||||
|
|
||||||
|
|
||||||
manager = Manager(create_app)
|
manager = Manager(create_app)
|
||||||
|
@ -542,6 +544,7 @@ def main():
|
||||||
manager.add_command("endpoint", endpoint_manager)
|
manager.add_command("endpoint", endpoint_manager)
|
||||||
manager.add_command("report", report_manager)
|
manager.add_command("report", report_manager)
|
||||||
manager.add_command("policy", policy_manager)
|
manager.add_command("policy", policy_manager)
|
||||||
|
manager.add_command("pending_certs", pending_certificate_manager)
|
||||||
manager.run()
|
manager.run()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,103 @@
|
||||||
|
"""Add Pending Certificates models and relations
|
||||||
|
|
||||||
|
Revision ID: 556ceb3e3c3e
|
||||||
|
Revises: 47baffaae1a7
|
||||||
|
Create Date: 2018-01-05 01:18:45.571595
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '556ceb3e3c3e'
|
||||||
|
down_revision = '47baffaae1a7'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('pending_certs',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('external_id', sa.String(length=128), nullable=True),
|
||||||
|
sa.Column('owner', sa.String(length=128), nullable=False),
|
||||||
|
sa.Column('name', sa.String(length=256), nullable=True),
|
||||||
|
sa.Column('description', sa.String(length=1024), nullable=True),
|
||||||
|
sa.Column('notify', sa.Boolean(), nullable=True),
|
||||||
|
sa.Column('number_attempts', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('rename', sa.Boolean(), nullable=True),
|
||||||
|
sa.Column('cn', sa.String(length=128), nullable=True),
|
||||||
|
sa.Column('csr', sa.Text(), nullable=False),
|
||||||
|
sa.Column('chain', sa.Text(), nullable=True),
|
||||||
|
sa.Column('private_key', lemur.utils.Vault(), nullable=True),
|
||||||
|
sa.Column('date_created', sqlalchemy_utils.types.arrow.ArrowType(), server_default=sa.text('now()'), nullable=False),
|
||||||
|
sa.Column('status', sa.String(length=128), nullable=True),
|
||||||
|
sa.Column('rotation', sa.Boolean(), nullable=True),
|
||||||
|
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('authority_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('root_authority_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('rotation_policy_id', sa.Integer(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['authority_id'], ['authorities.id'], ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['root_authority_id'], ['authorities.id'], ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['rotation_policy_id'], ['rotation_policies.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('name')
|
||||||
|
)
|
||||||
|
op.create_table('pending_cert_destination_associations',
|
||||||
|
sa.Column('destination_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('pending_cert_id', sa.Integer(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['destination_id'], ['destinations.id'], ondelete='cascade'),
|
||||||
|
sa.ForeignKeyConstraint(['pending_cert_id'], ['pending_certs.id'], ondelete='cascade')
|
||||||
|
)
|
||||||
|
op.create_index('pending_cert_destination_associations_ix', 'pending_cert_destination_associations', ['destination_id', 'pending_cert_id'], unique=False)
|
||||||
|
op.create_table('pending_cert_notification_associations',
|
||||||
|
sa.Column('notification_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('pending_cert_id', sa.Integer(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['notification_id'], ['notifications.id'], ondelete='cascade'),
|
||||||
|
sa.ForeignKeyConstraint(['pending_cert_id'], ['pending_certs.id'], ondelete='cascade')
|
||||||
|
)
|
||||||
|
op.create_index('pending_cert_notification_associations_ix', 'pending_cert_notification_associations', ['notification_id', 'pending_cert_id'], unique=False)
|
||||||
|
op.create_table('pending_cert_replacement_associations',
|
||||||
|
sa.Column('replaced_certificate_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('pending_cert_id', sa.Integer(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['pending_cert_id'], ['pending_certs.id'], ondelete='cascade'),
|
||||||
|
sa.ForeignKeyConstraint(['replaced_certificate_id'], ['certificates.id'], ondelete='cascade')
|
||||||
|
)
|
||||||
|
op.create_index('pending_cert_replacement_associations_ix', 'pending_cert_replacement_associations', ['replaced_certificate_id', 'pending_cert_id'], unique=False)
|
||||||
|
op.create_table('pending_cert_role_associations',
|
||||||
|
sa.Column('pending_cert_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('role_id', sa.Integer(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['pending_cert_id'], ['pending_certs.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['role_id'], ['roles.id'], )
|
||||||
|
)
|
||||||
|
op.create_index('pending_cert_role_associations_ix', 'pending_cert_role_associations', ['pending_cert_id', 'role_id'], unique=False)
|
||||||
|
op.create_table('pending_cert_source_associations',
|
||||||
|
sa.Column('source_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('pending_cert_id', sa.Integer(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['pending_cert_id'], ['pending_certs.id'], ondelete='cascade'),
|
||||||
|
sa.ForeignKeyConstraint(['source_id'], ['sources.id'], ondelete='cascade')
|
||||||
|
)
|
||||||
|
op.create_index('pending_cert_source_associations_ix', 'pending_cert_source_associations', ['source_id', 'pending_cert_id'], unique=False)
|
||||||
|
op.create_table('roles_authorities',
|
||||||
|
sa.Column('authority_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('role_id', sa.Integer(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['authority_id'], ['authorities.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['role_id'], ['roles.id'], )
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_index('pending_cert_source_associations_ix', table_name='pending_cert_source_associations')
|
||||||
|
op.drop_table('pending_cert_source_associations')
|
||||||
|
op.drop_index('pending_cert_role_associations_ix', table_name='pending_cert_role_associations')
|
||||||
|
op.drop_table('pending_cert_role_associations')
|
||||||
|
op.drop_index('pending_cert_replacement_associations_ix', table_name='pending_cert_replacement_associations')
|
||||||
|
op.drop_table('pending_cert_replacement_associations')
|
||||||
|
op.drop_index('pending_cert_notification_associations_ix', table_name='pending_cert_notification_associations')
|
||||||
|
op.drop_table('pending_cert_notification_associations')
|
||||||
|
op.drop_index('pending_cert_destination_associations_ix', table_name='pending_cert_destination_associations')
|
||||||
|
op.drop_table('pending_cert_destination_associations')
|
||||||
|
op.drop_table('pending_certs')
|
||||||
|
# ### end Alembic commands ###
|
|
@ -85,3 +85,48 @@ policies_ciphers = db.Table('policies_ciphers',
|
||||||
Column('policy_id', Integer, ForeignKey('policy.id')))
|
Column('policy_id', Integer, ForeignKey('policy.id')))
|
||||||
|
|
||||||
Index('policies_ciphers_ix', policies_ciphers.c.cipher_id, policies_ciphers.c.policy_id)
|
Index('policies_ciphers_ix', policies_ciphers.c.cipher_id, policies_ciphers.c.policy_id)
|
||||||
|
|
||||||
|
|
||||||
|
pending_cert_destination_associations = db.Table('pending_cert_destination_associations',
|
||||||
|
Column('destination_id', Integer,
|
||||||
|
ForeignKey('destinations.id', ondelete='cascade')),
|
||||||
|
Column('pending_cert_id', Integer,
|
||||||
|
ForeignKey('pending_certs.id', ondelete='cascade'))
|
||||||
|
)
|
||||||
|
|
||||||
|
Index('pending_cert_destination_associations_ix', pending_cert_destination_associations.c.destination_id, pending_cert_destination_associations.c.pending_cert_id)
|
||||||
|
|
||||||
|
|
||||||
|
pending_cert_notification_associations = db.Table('pending_cert_notification_associations',
|
||||||
|
Column('notification_id', Integer,
|
||||||
|
ForeignKey('notifications.id', ondelete='cascade')),
|
||||||
|
Column('pending_cert_id', Integer,
|
||||||
|
ForeignKey('pending_certs.id', ondelete='cascade'))
|
||||||
|
)
|
||||||
|
|
||||||
|
Index('pending_cert_notification_associations_ix', pending_cert_notification_associations.c.notification_id, pending_cert_notification_associations.c.pending_cert_id)
|
||||||
|
|
||||||
|
pending_cert_source_associations = db.Table('pending_cert_source_associations',
|
||||||
|
Column('source_id', Integer,
|
||||||
|
ForeignKey('sources.id', ondelete='cascade')),
|
||||||
|
Column('pending_cert_id', Integer,
|
||||||
|
ForeignKey('pending_certs.id', ondelete='cascade'))
|
||||||
|
)
|
||||||
|
|
||||||
|
Index('pending_cert_source_associations_ix', pending_cert_source_associations.c.source_id, pending_cert_source_associations.c.pending_cert_id)
|
||||||
|
|
||||||
|
pending_cert_replacement_associations = db.Table('pending_cert_replacement_associations',
|
||||||
|
Column('replaced_certificate_id', Integer,
|
||||||
|
ForeignKey('certificates.id', ondelete='cascade')),
|
||||||
|
Column('pending_cert_id', Integer,
|
||||||
|
ForeignKey('pending_certs.id', ondelete='cascade'))
|
||||||
|
)
|
||||||
|
|
||||||
|
Index('pending_cert_replacement_associations_ix', pending_cert_replacement_associations.c.replaced_certificate_id, pending_cert_replacement_associations.c.pending_cert_id)
|
||||||
|
|
||||||
|
pending_cert_role_associations = db.Table('pending_cert_role_associations',
|
||||||
|
Column('pending_cert_id', Integer, ForeignKey('pending_certs.id')),
|
||||||
|
Column('role_id', Integer, ForeignKey('roles.id'))
|
||||||
|
)
|
||||||
|
|
||||||
|
Index('pending_cert_role_associations_ix', pending_cert_role_associations.c.pending_cert_id, pending_cert_role_associations.c.role_id)
|
||||||
|
|
|
@ -11,7 +11,8 @@ from sqlalchemy_utils import JSONType
|
||||||
|
|
||||||
from lemur.database import db
|
from lemur.database import db
|
||||||
from lemur.plugins.base import plugins
|
from lemur.plugins.base import plugins
|
||||||
from lemur.models import certificate_notification_associations
|
from lemur.models import certificate_notification_associations, \
|
||||||
|
pending_cert_notification_associations
|
||||||
|
|
||||||
|
|
||||||
class Notification(db.Model):
|
class Notification(db.Model):
|
||||||
|
@ -29,6 +30,13 @@ class Notification(db.Model):
|
||||||
backref="notification",
|
backref="notification",
|
||||||
cascade='all,delete'
|
cascade='all,delete'
|
||||||
)
|
)
|
||||||
|
pending_certificates = relationship(
|
||||||
|
"PendingCertificate",
|
||||||
|
secondary=pending_cert_notification_associations,
|
||||||
|
passive_deletes=True,
|
||||||
|
backref="notification",
|
||||||
|
cascade='all,delete'
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def plugin(self):
|
def plugin(self):
|
||||||
|
|
|
@ -0,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
|
||||||
|
)
|
||||||
|
)
|
|
@ -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')
|
|
@ -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()
|
|
@ -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)
|
|
@ -0,0 +1,424 @@
|
||||||
|
"""
|
||||||
|
.. module: lemur.pending_certificates.views
|
||||||
|
:platform: Unix
|
||||||
|
:license: Apache, see LICENSE for more details.
|
||||||
|
.. moduleauthor:: James Chuong <jchuong@instartlogic.com>
|
||||||
|
"""
|
||||||
|
from flask import Blueprint, g, make_response, jsonify
|
||||||
|
from flask_restful import Api, reqparse
|
||||||
|
|
||||||
|
from lemur.auth.service import AuthenticatedResource
|
||||||
|
from lemur.auth.permissions import CertificatePermission
|
||||||
|
|
||||||
|
from lemur.common.schema import validate_schema
|
||||||
|
from lemur.common.utils import paginated_parser
|
||||||
|
|
||||||
|
from lemur.pending_certificates import service
|
||||||
|
from lemur.roles import service as role_service
|
||||||
|
|
||||||
|
from lemur.pending_certificates.schemas import (
|
||||||
|
pending_certificate_output_schema,
|
||||||
|
pending_certificate_edit_input_schema,
|
||||||
|
pending_certificate_cancel_schema,
|
||||||
|
)
|
||||||
|
|
||||||
|
mod = Blueprint('pending_certificates', __name__)
|
||||||
|
api = Api(mod)
|
||||||
|
|
||||||
|
|
||||||
|
class PendingCertificatesList(AuthenticatedResource):
|
||||||
|
def __init__(self):
|
||||||
|
self.reqparse = reqparse.RequestParser()
|
||||||
|
super(PendingCertificatesList, self).__init__()
|
||||||
|
|
||||||
|
@validate_schema(None, pending_certificate_output_schema)
|
||||||
|
def get(self):
|
||||||
|
"""
|
||||||
|
.. http:get:: /pending_certificates
|
||||||
|
|
||||||
|
List of pending certificates
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /pending_certificates HTTP/1.1
|
||||||
|
Host: example.com
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: text/javascript
|
||||||
|
|
||||||
|
{
|
||||||
|
"status": null,
|
||||||
|
"cn": "*.test.example.net",
|
||||||
|
"chain": "",
|
||||||
|
"authority": {
|
||||||
|
"active": true,
|
||||||
|
"owner": "secure@example.com",
|
||||||
|
"id": 1,
|
||||||
|
"description": "verisign test authority",
|
||||||
|
"name": "verisign"
|
||||||
|
},
|
||||||
|
"owner": "joe@example.com",
|
||||||
|
"serial": "82311058732025924142789179368889309156",
|
||||||
|
"id": 2288,
|
||||||
|
"issuer": "SymantecCorporation",
|
||||||
|
"notBefore": "2016-06-03T00:00:00+00:00",
|
||||||
|
"notAfter": "2018-01-12T23:59:59+00:00",
|
||||||
|
"destinations": [],
|
||||||
|
"description": null,
|
||||||
|
"deleted": null,
|
||||||
|
"notifications": [{
|
||||||
|
"id": 1
|
||||||
|
}],
|
||||||
|
"signingAlgorithm": "sha256",
|
||||||
|
"user": {
|
||||||
|
"username": "jane",
|
||||||
|
"active": true,
|
||||||
|
"email": "jane@example.com",
|
||||||
|
"id": 2
|
||||||
|
},
|
||||||
|
"active": true,
|
||||||
|
"domains": [{
|
||||||
|
"sensitive": false,
|
||||||
|
"id": 1090,
|
||||||
|
"name": "*.test.example.net"
|
||||||
|
}],
|
||||||
|
"rotation": true,
|
||||||
|
"rotationPolicy": {"name": "default"},
|
||||||
|
"replaces": [],
|
||||||
|
"replaced": [],
|
||||||
|
"name": "WILDCARD.test.example.net-SymantecCorporation-20160603-20180112",
|
||||||
|
"roles": [{
|
||||||
|
"id": 464,
|
||||||
|
"description": "This is a google group based role created by Lemur",
|
||||||
|
"name": "joe@example.com"
|
||||||
|
}],
|
||||||
|
"san": null
|
||||||
|
}
|
||||||
|
|
||||||
|
:reqheader Authorization: OAuth token to authenticate
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 403: unauthenticated
|
||||||
|
|
||||||
|
"""
|
||||||
|
parser = paginated_parser.copy()
|
||||||
|
parser.add_argument('timeRange', type=int, dest='time_range', location='args')
|
||||||
|
parser.add_argument('owner', type=bool, location='args')
|
||||||
|
parser.add_argument('id', type=str, location='args')
|
||||||
|
parser.add_argument('active', type=bool, location='args')
|
||||||
|
parser.add_argument('destinationId', type=int, dest="destination_id", location='args')
|
||||||
|
parser.add_argument('creator', type=str, location='args')
|
||||||
|
parser.add_argument('show', type=str, location='args')
|
||||||
|
args = parser.parse_args()
|
||||||
|
args['user'] = g.user
|
||||||
|
return service.render(args)
|
||||||
|
|
||||||
|
|
||||||
|
class PendingCertificates(AuthenticatedResource):
|
||||||
|
def __init__(self):
|
||||||
|
self.reqparse = reqparse.RequestParser()
|
||||||
|
super(PendingCertificates, self).__init__()
|
||||||
|
|
||||||
|
@validate_schema(None, pending_certificate_output_schema)
|
||||||
|
def get(self, pending_certificate_id):
|
||||||
|
"""
|
||||||
|
.. http:get:: /pending_certificates/1
|
||||||
|
|
||||||
|
One pending certificate
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /pending_certificates/1 HTTP/1.1
|
||||||
|
Host: example.com
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: text/javascript
|
||||||
|
|
||||||
|
{
|
||||||
|
"status": null,
|
||||||
|
"cn": "*.test.example.net",
|
||||||
|
"chain": "",
|
||||||
|
"authority": {
|
||||||
|
"active": true,
|
||||||
|
"owner": "secure@example.com",
|
||||||
|
"id": 1,
|
||||||
|
"description": "verisign test authority",
|
||||||
|
"name": "verisign"
|
||||||
|
},
|
||||||
|
"owner": "joe@example.com",
|
||||||
|
"serial": "82311058732025924142789179368889309156",
|
||||||
|
"id": 1,
|
||||||
|
"issuer": "SymantecCorporation",
|
||||||
|
"notBefore": "2016-06-03T00:00:00+00:00",
|
||||||
|
"notAfter": "2018-01-12T23:59:59+00:00",
|
||||||
|
"destinations": [],
|
||||||
|
"description": null,
|
||||||
|
"deleted": null,
|
||||||
|
"notifications": [{
|
||||||
|
"id": 1
|
||||||
|
}],
|
||||||
|
"signingAlgorithm": "sha256",
|
||||||
|
"user": {
|
||||||
|
"username": "jane",
|
||||||
|
"active": true,
|
||||||
|
"email": "jane@example.com",
|
||||||
|
"id": 2
|
||||||
|
},
|
||||||
|
"active": true,
|
||||||
|
"domains": [{
|
||||||
|
"sensitive": false,
|
||||||
|
"id": 1090,
|
||||||
|
"name": "*.test.example.net"
|
||||||
|
}],
|
||||||
|
"rotation": true,
|
||||||
|
"rotationPolicy": {"name": "default"},
|
||||||
|
"replaces": [],
|
||||||
|
"replaced": [],
|
||||||
|
"name": "WILDCARD.test.example.net-SymantecCorporation-20160603-20180112",
|
||||||
|
"roles": [{
|
||||||
|
"id": 464,
|
||||||
|
"description": "This is a google group based role created by Lemur",
|
||||||
|
"name": "joe@example.com"
|
||||||
|
}],
|
||||||
|
"san": null
|
||||||
|
}
|
||||||
|
|
||||||
|
:reqheader Authorization: OAuth token to authenticate
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 403: unauthenticated
|
||||||
|
|
||||||
|
"""
|
||||||
|
return service.get(pending_certificate_id)
|
||||||
|
|
||||||
|
@validate_schema(pending_certificate_edit_input_schema, pending_certificate_output_schema)
|
||||||
|
def put(self, pending_certificate_id, data=None):
|
||||||
|
"""
|
||||||
|
.. http:put:: /pending_certificates/1
|
||||||
|
|
||||||
|
Update a pending certificate
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
PUT /pending certificates/1 HTTP/1.1
|
||||||
|
Host: example.com
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
{
|
||||||
|
"owner": "jimbob@example.com",
|
||||||
|
"active": false
|
||||||
|
"notifications": [],
|
||||||
|
"destinations": [],
|
||||||
|
"replacements": []
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: text/javascript
|
||||||
|
|
||||||
|
{
|
||||||
|
"status": null,
|
||||||
|
"cn": "*.test.example.net",
|
||||||
|
"chain": "",
|
||||||
|
"authority": {
|
||||||
|
"active": true,
|
||||||
|
"owner": "secure@example.com",
|
||||||
|
"id": 1,
|
||||||
|
"description": "verisign test authority",
|
||||||
|
"name": "verisign"
|
||||||
|
},
|
||||||
|
"owner": "joe@example.com",
|
||||||
|
"serial": "82311058732025924142789179368889309156",
|
||||||
|
"id": 2288,
|
||||||
|
"issuer": "SymantecCorporation",
|
||||||
|
"destinations": [],
|
||||||
|
"description": null,
|
||||||
|
"deleted": null,
|
||||||
|
"notifications": [{
|
||||||
|
"id": 1
|
||||||
|
}]
|
||||||
|
"user": {
|
||||||
|
"username": "jane",
|
||||||
|
"active": true,
|
||||||
|
"email": "jane@example.com",
|
||||||
|
"id": 2
|
||||||
|
},
|
||||||
|
"active": true,
|
||||||
|
"number_attempts": 1,
|
||||||
|
"csr": "-----BEGIN CERTIFICATE REQUEST-----...",
|
||||||
|
"external_id": 12345,
|
||||||
|
"domains": [{
|
||||||
|
"sensitive": false,
|
||||||
|
"id": 1090,
|
||||||
|
"name": "*.test.example.net"
|
||||||
|
}],
|
||||||
|
"replaces": [],
|
||||||
|
"name": "WILDCARD.test.example.net-SymantecCorporation-20160603-20180112",
|
||||||
|
"roles": [{
|
||||||
|
"id": 464,
|
||||||
|
"description": "This is a google group based role created by Lemur",
|
||||||
|
"name": "joe@example.com"
|
||||||
|
}],
|
||||||
|
"rotation": true,
|
||||||
|
"rotationPolicy": {"name": "default"},
|
||||||
|
}
|
||||||
|
|
||||||
|
:reqheader Authorization: OAuth token to authenticate
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 403: unauthenticated
|
||||||
|
|
||||||
|
"""
|
||||||
|
pending_cert = service.get(pending_certificate_id)
|
||||||
|
|
||||||
|
if not pending_cert:
|
||||||
|
return dict(message="Cannot find specified pending certificate"), 404
|
||||||
|
|
||||||
|
# allow creators
|
||||||
|
if g.current_user != pending_cert.user:
|
||||||
|
owner_role = role_service.get_by_name(pending_cert.owner)
|
||||||
|
permission = CertificatePermission(owner_role, [x.name for x in pending_cert.roles])
|
||||||
|
|
||||||
|
if not permission.can():
|
||||||
|
return dict(message='You are not authorized to update this certificate'), 403
|
||||||
|
|
||||||
|
for destination in data['destinations']:
|
||||||
|
if destination.plugin.requires_key:
|
||||||
|
if not pending_cert.private_key:
|
||||||
|
return dict(
|
||||||
|
message='Unable to add destination: {0}. Certificate does not have required private key.'.format(
|
||||||
|
destination.label
|
||||||
|
)
|
||||||
|
), 400
|
||||||
|
|
||||||
|
pending_cert = service.update(pending_certificate_id, **data)
|
||||||
|
return pending_cert
|
||||||
|
|
||||||
|
@validate_schema(pending_certificate_cancel_schema, None)
|
||||||
|
def delete(self, pending_certificate_id, data=None):
|
||||||
|
"""
|
||||||
|
.. http:delete:: /pending_certificates/1
|
||||||
|
|
||||||
|
Cancel and delete a pending certificate
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
DELETE /pending certificates/1 HTTP/1.1
|
||||||
|
Host: example.com
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
{
|
||||||
|
"note": "Why I am cancelling this order"
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 204 No Content
|
||||||
|
|
||||||
|
:reqheader Authorization: OAuth token to authenticate
|
||||||
|
:statuscode 204: no error
|
||||||
|
:statuscode 401: unauthenticated
|
||||||
|
:statuscode 403: unauthorized
|
||||||
|
:statuscode 404: pending certificate id not found
|
||||||
|
:statuscode 500: internal error
|
||||||
|
"""
|
||||||
|
pending_cert = service.get(pending_certificate_id)
|
||||||
|
|
||||||
|
if not pending_cert:
|
||||||
|
return dict(message="Cannot find specified pending certificate"), 404
|
||||||
|
|
||||||
|
# allow creators
|
||||||
|
if g.current_user != pending_cert.user:
|
||||||
|
owner_role = role_service.get_by_name(pending_cert.owner)
|
||||||
|
permission = CertificatePermission(owner_role, [x.name for x in pending_cert.roles])
|
||||||
|
|
||||||
|
if not permission.can():
|
||||||
|
return dict(message='You are not authorized to update this certificate'), 403
|
||||||
|
|
||||||
|
if service.cancel(pending_cert, **data):
|
||||||
|
service.delete(pending_cert)
|
||||||
|
return('', 204)
|
||||||
|
else:
|
||||||
|
# service.cancel raises exception if there was an issue, but this will ensure something
|
||||||
|
# is relayed to user in case of something unexpected (unsuccessful update somehow).
|
||||||
|
return dict(message="Unexpected error occurred while trying to cancel this certificate"), 500
|
||||||
|
|
||||||
|
|
||||||
|
class PendingCertificatePrivateKey(AuthenticatedResource):
|
||||||
|
def __init__(self):
|
||||||
|
super(PendingCertificatePrivateKey, self).__init__()
|
||||||
|
|
||||||
|
def get(self, pending_certificate_id):
|
||||||
|
"""
|
||||||
|
.. http:get:: /pending_certificates/1/key
|
||||||
|
|
||||||
|
Retrieves the private key for a given pneding certificate
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /pending_certificates/1/key HTTP/1.1
|
||||||
|
Host: example.com
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: text/javascript
|
||||||
|
|
||||||
|
{
|
||||||
|
"key": "-----BEGIN ..."
|
||||||
|
}
|
||||||
|
|
||||||
|
:reqheader Authorization: OAuth token to authenticate
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 403: unauthenticated
|
||||||
|
"""
|
||||||
|
cert = service.get(pending_certificate_id)
|
||||||
|
if not cert:
|
||||||
|
return dict(message="Cannot find specified pending certificate"), 404
|
||||||
|
|
||||||
|
# allow creators
|
||||||
|
if g.current_user != cert.user:
|
||||||
|
owner_role = role_service.get_by_name(cert.owner)
|
||||||
|
permission = CertificatePermission(owner_role, [x.name for x in cert.roles])
|
||||||
|
|
||||||
|
if not permission.can():
|
||||||
|
return dict(message='You are not authorized to view this key'), 403
|
||||||
|
|
||||||
|
response = make_response(jsonify(key=cert.private_key), 200)
|
||||||
|
response.headers['cache-control'] = 'private, max-age=0, no-cache, no-store'
|
||||||
|
response.headers['pragma'] = 'no-cache'
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
api.add_resource(PendingCertificatesList, '/pending_certificates', endpoint='pending_certificates')
|
||||||
|
api.add_resource(PendingCertificates, '/pending_certificates/<int:pending_certificate_id>', endpoint='pending_certificate')
|
||||||
|
api.add_resource(PendingCertificatePrivateKey, '/pending_certificates/<int:pending_certificate_id>/key', endpoint='privateKeyPendingCertificates')
|
|
@ -24,3 +24,9 @@ class IssuerPlugin(Plugin):
|
||||||
|
|
||||||
def revoke_certificate(self, certificate, comments):
|
def revoke_certificate(self, certificate, comments):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def get_ordered_certificate(self, order_id):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def cancel_ordered_certificate(self, pending_cert, **kwargs):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
|
@ -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.bases import IssuerPlugin
|
||||||
from lemur.plugins import lemur_acme as acme
|
from lemur.plugins import lemur_acme as acme
|
||||||
|
|
||||||
from .route53 import delete_txt_record, create_txt_record, wait_for_r53_change
|
|
||||||
|
|
||||||
|
|
||||||
def find_dns_challenge(authz):
|
def find_dns_challenge(authz):
|
||||||
for combo in authz.body.resolved_combinations:
|
for combo in authz.body.resolved_combinations:
|
||||||
|
@ -45,12 +43,13 @@ class AuthorizationRecord(object):
|
||||||
self.change_id = change_id
|
self.change_id = change_id
|
||||||
|
|
||||||
|
|
||||||
def start_dns_challenge(acme_client, account_number, host):
|
def start_dns_challenge(acme_client, account_number, host, dns_provider):
|
||||||
|
current_app.logger.debug("Starting DNS challenge for {0}".format(host))
|
||||||
authz = acme_client.request_domain_challenges(host)
|
authz = acme_client.request_domain_challenges(host)
|
||||||
|
|
||||||
[dns_challenge] = find_dns_challenge(authz)
|
[dns_challenge] = find_dns_challenge(authz)
|
||||||
|
|
||||||
change_id = create_txt_record(
|
change_id = dns_provider.create_txt_record(
|
||||||
dns_challenge.validation_domain_name(host),
|
dns_challenge.validation_domain_name(host),
|
||||||
dns_challenge.validation(acme_client.key),
|
dns_challenge.validation(acme_client.key),
|
||||||
account_number
|
account_number
|
||||||
|
@ -64,8 +63,8 @@ def start_dns_challenge(acme_client, account_number, host):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def complete_dns_challenge(acme_client, account_number, authz_record):
|
def complete_dns_challenge(acme_client, account_number, authz_record, dns_provider):
|
||||||
wait_for_r53_change(authz_record.change_id, account_number=account_number)
|
dns_provider.wait_for_dns_change(authz_record.change_id, account_number=account_number)
|
||||||
|
|
||||||
response = authz_record.dns_challenge.response(acme_client.key)
|
response = authz_record.dns_challenge.response(acme_client.key)
|
||||||
|
|
||||||
|
@ -96,12 +95,12 @@ def request_certificate(acme_client, authorizations, csr):
|
||||||
OpenSSL.crypto.FILETYPE_PEM, cert_response.body
|
OpenSSL.crypto.FILETYPE_PEM, cert_response.body
|
||||||
).decode('utf-8')
|
).decode('utf-8')
|
||||||
|
|
||||||
# https://github.com/alex/letsencrypt-aws/commit/853ea7f93f141fe18d9ef12aee6b3388f98b4830
|
pem_certificate_chain = "\n".join(
|
||||||
pem_certificate_chain = b"\n".join(
|
OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, cert.decode("utf-8"))
|
||||||
OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, cert)
|
|
||||||
for cert in acme_client.fetch_chain(cert_response)
|
for cert in acme_client.fetch_chain(cert_response)
|
||||||
).decode('utf-8')
|
).decode('utf-8')
|
||||||
|
|
||||||
|
current_app.logger.debug("{0} {1}".format(type(pem_certificate). type(pem_certificate_chain)))
|
||||||
return pem_certificate, pem_certificate_chain
|
return pem_certificate, pem_certificate_chain
|
||||||
|
|
||||||
|
|
||||||
|
@ -113,12 +112,15 @@ def setup_acme_client():
|
||||||
|
|
||||||
key = jose.JWKRSA(key=generate_private_key('RSA2048'))
|
key = jose.JWKRSA(key=generate_private_key('RSA2048'))
|
||||||
|
|
||||||
|
current_app.logger.debug("Connecting with directory at {0}".format(directory_url))
|
||||||
client = Client(directory_url, key)
|
client = Client(directory_url, key)
|
||||||
|
|
||||||
registration = client.register(
|
registration = client.register(
|
||||||
messages.NewRegistration.from_data(email=email)
|
messages.NewRegistration.from_data(email=email)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
current_app.logger.debug("Connected: {0}".format(registration.uri))
|
||||||
|
|
||||||
client.agree_to_tos(registration)
|
client.agree_to_tos(registration)
|
||||||
return client, registration
|
return client, registration
|
||||||
|
|
||||||
|
@ -129,26 +131,30 @@ def get_domains(options):
|
||||||
:param options:
|
:param options:
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
|
current_app.logger.debug("Fetching domains")
|
||||||
|
|
||||||
domains = [options['common_name']]
|
domains = [options['common_name']]
|
||||||
if options.get('extensions'):
|
if options.get('extensions'):
|
||||||
for name in options['extensions']['sub_alt_names']['names']:
|
for name in options['extensions']['sub_alt_names']['names']:
|
||||||
domains.append(name.value)
|
domains.append(name)
|
||||||
|
|
||||||
|
current_app.logger.debug("Got these domains: {0}".format(domains))
|
||||||
return domains
|
return domains
|
||||||
|
|
||||||
|
|
||||||
def get_authorizations(acme_client, account_number, domains):
|
def get_authorizations(acme_client, account_number, domains, dns_provider):
|
||||||
authorizations = []
|
authorizations = []
|
||||||
try:
|
try:
|
||||||
for domain in domains:
|
for domain in domains:
|
||||||
authz_record = start_dns_challenge(acme_client, account_number, domain)
|
authz_record = start_dns_challenge(acme_client, account_number, domain, dns_provider)
|
||||||
authorizations.append(authz_record)
|
authorizations.append(authz_record)
|
||||||
|
|
||||||
for authz_record in authorizations:
|
for authz_record in authorizations:
|
||||||
complete_dns_challenge(acme_client, account_number, authz_record)
|
complete_dns_challenge(acme_client, account_number, authz_record, dns_provider)
|
||||||
finally:
|
finally:
|
||||||
for authz_record in authorizations:
|
for authz_record in authorizations:
|
||||||
dns_challenge = authz_record.dns_challenge
|
dns_challenge = authz_record.dns_challenge
|
||||||
delete_txt_record(
|
dns_provider.delete_txt_record(
|
||||||
authz_record.change_id,
|
authz_record.change_id,
|
||||||
account_number,
|
account_number,
|
||||||
dns_challenge.validation_domain_name(authz_record.host),
|
dns_challenge.validation_domain_name(authz_record.host),
|
||||||
|
@ -177,6 +183,9 @@ class ACMEIssuerPlugin(IssuerPlugin):
|
||||||
]
|
]
|
||||||
|
|
||||||
validate_conf(current_app, required_vars)
|
validate_conf(current_app, required_vars)
|
||||||
|
self.dns_provider_name = current_app.config.get('ACME_DNS_PROVIDER', 'route53')
|
||||||
|
current_app.logger.debug("Using DNS provider: {0}".format(self.dns_provider_name))
|
||||||
|
self.dns_provider = __import__(self.dns_provider_name, globals(), locals(), [], 1)
|
||||||
super(ACMEIssuerPlugin, self).__init__(*args, **kwargs)
|
super(ACMEIssuerPlugin, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
def create_certificate(self, csr, issuer_options):
|
def create_certificate(self, csr, issuer_options):
|
||||||
|
@ -191,7 +200,7 @@ class ACMEIssuerPlugin(IssuerPlugin):
|
||||||
acme_client, registration = setup_acme_client()
|
acme_client, registration = setup_acme_client()
|
||||||
account_number = current_app.config.get('ACME_AWS_ACCOUNT_NUMBER')
|
account_number = current_app.config.get('ACME_AWS_ACCOUNT_NUMBER')
|
||||||
domains = get_domains(issuer_options)
|
domains = get_domains(issuer_options)
|
||||||
authorizations = get_authorizations(acme_client, account_number, domains)
|
authorizations = get_authorizations(acme_client, account_number, domains, self.dns_provider)
|
||||||
pem_certificate, pem_certificate_chain = request_certificate(acme_client, authorizations, csr)
|
pem_certificate, pem_certificate_chain = request_certificate(acme_client, authorizations, csr)
|
||||||
# TODO add external ID (if possible)
|
# TODO add external ID (if possible)
|
||||||
return pem_certificate, pem_certificate_chain, None
|
return pem_certificate, pem_certificate_chain, None
|
||||||
|
|
|
@ -3,7 +3,7 @@ from lemur.plugins.lemur_aws.sts import sts_client
|
||||||
|
|
||||||
|
|
||||||
@sts_client('route53')
|
@sts_client('route53')
|
||||||
def wait_for_r53_change(change_id, client=None):
|
def wait_for_dns_change(change_id, client=None):
|
||||||
_, change_id = change_id
|
_, change_id = change_id
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
|
|
@ -326,6 +326,39 @@ class DigiCertIssuerPlugin(IssuerPlugin):
|
||||||
response = self.session.put(create_url, data=json.dumps({'comments': comments}))
|
response = self.session.put(create_url, data=json.dumps({'comments': comments}))
|
||||||
return handle_response(response)
|
return handle_response(response)
|
||||||
|
|
||||||
|
def get_ordered_certificate(self, order_id):
|
||||||
|
""" Retrieve a certificate via order id """
|
||||||
|
base_url = current_app.config.get('DIGICERT_URL')
|
||||||
|
try:
|
||||||
|
certificate_id = get_certificate_id(self.session, base_url, order_id)
|
||||||
|
except Exception as ex:
|
||||||
|
return None
|
||||||
|
certificate_url = "{0}/services/v2/certificate/{1}/download/format/pem_all".format(base_url, certificate_id)
|
||||||
|
end_entity, intermediate, root = pem.parse(self.session.get(certificate_url).content)
|
||||||
|
cert = {'body': "\n".join(str(end_entity).splitlines()),
|
||||||
|
'chain': "\n".join(str(intermediate).splitlines()),
|
||||||
|
'external_id': str(certificate_id)}
|
||||||
|
return cert
|
||||||
|
|
||||||
|
def cancel_ordered_certificate(self, pending_cert, **kwargs):
|
||||||
|
""" Set the certificate order to canceled """
|
||||||
|
base_url = current_app.config.get('DIGICERT_URL')
|
||||||
|
api_url = "{0}/services/v2/order/certificate/{1}/status".format(base_url, pending_cert.external_id)
|
||||||
|
payload = {
|
||||||
|
'status': 'CANCELED',
|
||||||
|
'note': kwargs.get('note')
|
||||||
|
}
|
||||||
|
response = self.session.put(api_url, data=json.dumps(payload))
|
||||||
|
if response.status_code == 404:
|
||||||
|
# not well documented by Digicert, but either the certificate does not exist or we
|
||||||
|
# don't own that order (someone else's order id!). Either way, we can just ignore it
|
||||||
|
# and have it removed from Lemur
|
||||||
|
current_app.logger.warning(
|
||||||
|
"Digicert Plugin tried to cancel pending certificate {0} but it does not exist!".format(pending_cert.name))
|
||||||
|
elif response.status_code != 204:
|
||||||
|
current_app.logger.debug("{0} code {1}".format(response.status_code, response.content))
|
||||||
|
raise Exception("Failed to cancel pending certificate {0}".format(pending_cert.name))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_authority(options):
|
def create_authority(options):
|
||||||
"""Create an authority.
|
"""Create an authority.
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import pytest
|
import pytest
|
||||||
import arrow
|
import arrow
|
||||||
import json
|
import json
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
from freezegun import freeze_time
|
from freezegun import freeze_time
|
||||||
|
|
||||||
from lemur.tests.vectors import CSR_STR
|
from lemur.tests.vectors import CSR_STR
|
||||||
|
@ -175,3 +177,22 @@ ghi
|
||||||
|
|
||||||
assert cert == "-----BEGIN CERTIFICATE-----\nabc\n-----END CERTIFICATE-----"
|
assert cert == "-----BEGIN CERTIFICATE-----\nabc\n-----END CERTIFICATE-----"
|
||||||
assert intermediate == "-----BEGIN CERTIFICATE-----\ndef\n-----END CERTIFICATE-----"
|
assert intermediate == "-----BEGIN CERTIFICATE-----\ndef\n-----END CERTIFICATE-----"
|
||||||
|
|
||||||
|
|
||||||
|
@patch("lemur.pending_certificates.models.PendingCertificate")
|
||||||
|
def test_cancel_ordered_certificate(mock_pending_cert):
|
||||||
|
import requests_mock
|
||||||
|
from lemur.plugins.lemur_digicert.plugin import DigiCertIssuerPlugin
|
||||||
|
|
||||||
|
mock_pending_cert.external_id = 1234
|
||||||
|
subject = DigiCertIssuerPlugin()
|
||||||
|
adapter = requests_mock.Adapter()
|
||||||
|
adapter.register_uri('PUT', 'mock://www.digicert.com/services/v2/order/certificate/1234/status', status_code=204)
|
||||||
|
adapter.register_uri('PUT', 'mock://www.digicert.com/services/v2/order/certificate/111/status', status_code=404)
|
||||||
|
subject.session.mount('mock', adapter)
|
||||||
|
data = {'note': 'Test'}
|
||||||
|
subject.cancel_ordered_certificate(mock_pending_cert, **data)
|
||||||
|
|
||||||
|
# A non-existing order id, does not raise exception because if it doesn't exist, then it doesn't matter
|
||||||
|
mock_pending_cert.external_id = 111
|
||||||
|
subject.cancel_ordered_certificate(mock_pending_cert, **data)
|
||||||
|
|
|
@ -14,7 +14,8 @@ from sqlalchemy import Boolean, Column, Integer, String, Text, ForeignKey
|
||||||
|
|
||||||
from lemur.database import db
|
from lemur.database import db
|
||||||
from lemur.utils import Vault
|
from lemur.utils import Vault
|
||||||
from lemur.models import roles_users, roles_authorities, roles_certificates
|
from lemur.models import roles_users, roles_authorities, roles_certificates, \
|
||||||
|
pending_cert_role_associations
|
||||||
|
|
||||||
|
|
||||||
class Role(db.Model):
|
class Role(db.Model):
|
||||||
|
@ -30,6 +31,7 @@ class Role(db.Model):
|
||||||
third_party = Column(Boolean)
|
third_party = Column(Boolean)
|
||||||
users = relationship("User", secondary=roles_users, passive_deletes=True, backref="role")
|
users = relationship("User", secondary=roles_users, passive_deletes=True, backref="role")
|
||||||
certificates = relationship("Certificate", secondary=roles_certificates, backref="role")
|
certificates = relationship("Certificate", secondary=roles_certificates, backref="role")
|
||||||
|
pending_certificates = relationship("PendingCertificate", secondary=pending_cert_role_associations, backref="role")
|
||||||
|
|
||||||
sensitive_fields = ('password',)
|
sensitive_fields = ('password',)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" ng-click="exit()" aria-label="Exit"><span artia-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
<h3 class="modal-title">Cancel <span class="text-muted"><small>{{ pendingCertificate.name }}</small></span></h3>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form name="cancelForm" class="form-horizontal" role="form" novalidate>
|
||||||
|
<div class="form-group"
|
||||||
|
ng-class="{'has-error': cancelForm.note.$invalid, 'has-success': !cancelForm.note.$invalid&&cancelForm.note.$dirty}">
|
||||||
|
<label class="control-label col-sm-2">
|
||||||
|
Note for Cancelling
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<textarea name="note" ng-model="cancelOptions.note" placeholder="Reason for cancelling"
|
||||||
|
class="form-control" required></textarea>
|
||||||
|
<p ng-show="cancelForm.note.$invalid && !cancelForm.note.$pristine" class="help-block">You must give a
|
||||||
|
reason for cancelling a pending certificate order.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="submit" ng-click="cancel(pendingCertificate, cancelOptions)" ng-disabled="cancelForm.$invalid" class="btn btn-success">Cancel Certificate
|
||||||
|
</button>
|
||||||
|
<button ng-click="exit()" class="btn btn-danger">Exit</button>
|
||||||
|
</div>
|
|
@ -0,0 +1,29 @@
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="control-label col-sm-2">
|
||||||
|
Destinations
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" ng-model="pendingCertificate.selectedDestination" placeholder="AWS..."
|
||||||
|
uib-typeahead="destination.label for destination in destinationService.findDestinationsByName($viewValue)" typeahead-loading="loadingDestinations"
|
||||||
|
class="form-control input-md" typeahead-on-select="pendingCertificate.attachDestination($item)"
|
||||||
|
uib-tooltip="Lemur can upload certificates to any pre-defined destination"
|
||||||
|
uib-tooltip-trigger="focus" uib-tooltip-placement="top"
|
||||||
|
typeahead-wait-ms="500">
|
||||||
|
<span class="input-group-btn">
|
||||||
|
<button ng-model="destinations.show" class="btn btn-md btn-default" uib-btn-checkbox btn-checkbox-true="1" btn-checkbox-false="0">
|
||||||
|
<span class="badge">{{ pendingCertificate.destinations.length || 0 }}</span>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<table class="table">
|
||||||
|
<tr ng-repeat="destination in pendingCertificate.destinations track by $index">
|
||||||
|
<td><a class="btn btn-sm btn-info" href="#/destinations/{{ destination.id }}/pendingCertificates">{{ destination.label }}</a></td>
|
||||||
|
<td><span class="text-muted">{{ destination.description }}</span></td>
|
||||||
|
<td>
|
||||||
|
<button type="button" ng-click="pendingCertificate.removeDestination($index)" class="btn btn-danger btn-sm pull-right">Remove</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,50 @@
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" ng-click="cancel()" aria-label="Close"><span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
<h3 class="modal-title">Edit <span class="text-muted"><small>{{ pendingCertificate.name }}</small></span></h3>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form name="editForm" class="form-horizontal" role="form" novalidate>
|
||||||
|
<div class="form-group"
|
||||||
|
ng-class="{'has-error': editForm.owner.$invalid, 'has-success': !editForm.owner.$invalid&&editForm.owner.$dirty}">
|
||||||
|
<label class="control-label col-sm-2">
|
||||||
|
Owner
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input type="email" name="owner" ng-model="pendingCertificate.owner" placeholder="owner@example.com"
|
||||||
|
class="form-control" required/>
|
||||||
|
|
||||||
|
<p ng-show="editForm.owner.$invalid && !editForm.owner.$pristine" class="help-block">Enter a valid
|
||||||
|
email.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group"
|
||||||
|
ng-class="{'has-error': editForm.description.$invalid, 'has-success': !editForm.$invalid&&editForm.description.$dirty}">
|
||||||
|
<label class="control-label col-sm-2">
|
||||||
|
Description
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<textarea name="description" ng-model="pendingCertificate.description" placeholder="Something elegant"
|
||||||
|
class="form-control" required></textarea>
|
||||||
|
<p ng-show="editForm.description.$invalid && !editForm.description.$pristine" class="help-block">You must give a
|
||||||
|
short description about this authority will be used for, this description should only include alphanumeric
|
||||||
|
characters</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="control-label col-sm-2">
|
||||||
|
Roles
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-10" ng-model="pendingCertificate" role-select></div>
|
||||||
|
</div>
|
||||||
|
<div ng-include="'angular/pending_certificates/pending_certificate/replaces.tpl.html'"></div>
|
||||||
|
<div ng-include="'angular/pending_certificates/pending_certificate/notifications.tpl.html'"></div>
|
||||||
|
|
||||||
|
<div ng-include="'angular/pending_certificates/pending_certificate/destinations.tpl.html'"></div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="submit" ng-click="save(pendingCertificate)" ng-disabled="editForm.$invalid" class="btn btn-success">Save
|
||||||
|
</button>
|
||||||
|
<button ng-click="cancel()" class="btn btn-danger">Cancel</button>
|
||||||
|
</div>
|
|
@ -0,0 +1,28 @@
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="control-label col-sm-2">
|
||||||
|
Notifications
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" ng-model="pendingCertificate.selectedNotification" placeholder="Email"
|
||||||
|
uib-typeahead="notification.label for notification in notificationService.findNotificationsByName($viewValue)" typeahead-loading="loadingDestinations"
|
||||||
|
class="form-control input-md" typeahead-on-select="pendingCertificate.attachNotification($item)"
|
||||||
|
uib-tooltip="By default Lemur will always notify you about this (pending) certificate through Email notifications."
|
||||||
|
uib-tooltip-trigger="focus" tooltip-placement="top" typeahead-wait-ms="500">
|
||||||
|
<span class="input-group-btn">
|
||||||
|
<button ng-model="notifications.show" class="btn btn-md btn-default" uib-btn-checkbox btn-checkbox-true="1" btn-checkbox-false="0">
|
||||||
|
<span class="badge">{{ pendingCertificate.notifications.length || 0 }}</span>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<table class="table">
|
||||||
|
<tr ng-repeat="notification in pendingCertificate.notifications track by $index">
|
||||||
|
<td><a class="btn btn-sm btn-info" href="#/notifications/{{ notification.id }}/pendingCertificates">{{ notification.label }}</a></td>
|
||||||
|
<td><span class="text-muted">{{ notification.description }}</span></td>
|
||||||
|
<td>
|
||||||
|
<button type="button" ng-click="pendingCertificate.removeNotification($index)" class="btn btn-danger btn-sm pull-right">Remove</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
72
lemur/static/app/angular/pending_certificates/pending_certificate/pending_certificate.js
vendored
Normal file
72
lemur/static/app/angular/pending_certificates/pending_certificate/pending_certificate.js
vendored
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
angular.module('lemur')
|
||||||
|
.controller('PendingCertificateEditController', function ($scope, $uibModalInstance, PendingCertificateApi, PendingCertificateService, CertificateService, DestinationService, NotificationService, toaster, editId) {
|
||||||
|
PendingCertificateApi.get(editId).then(function (pendingCertificate) {
|
||||||
|
$scope.pendingCertificate = pendingCertificate;
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.cancel = function () {
|
||||||
|
$uibModalInstance.dismiss('cancel');
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.save = function (pendingCertificate) {
|
||||||
|
PendingCertificateService.update(pendingCertificate).then(
|
||||||
|
function () {
|
||||||
|
toaster.pop({
|
||||||
|
type: 'success',
|
||||||
|
title: pendingCertificate.name,
|
||||||
|
body: 'Successfully updated!'
|
||||||
|
});
|
||||||
|
$uibModalInstance.close();
|
||||||
|
},
|
||||||
|
function (response) {
|
||||||
|
toaster.pop({
|
||||||
|
type: 'error',
|
||||||
|
title: pendingCertificate.name,
|
||||||
|
body: 'lemur-bad-request',
|
||||||
|
bodyOutputType: 'directive',
|
||||||
|
directiveData: response.data,
|
||||||
|
timeout: 100000
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.pendingCertificateService = PendingCertificateService;
|
||||||
|
$scope.certificateService = CertificateService;
|
||||||
|
$scope.destinationService = DestinationService;
|
||||||
|
$scope.notificationService = NotificationService;
|
||||||
|
})
|
||||||
|
.controller('PendingCertificateCancelController', function ($scope, $uibModalInstance, PendingCertificateApi, PendingCertificateService, toaster, cancelId) {
|
||||||
|
PendingCertificateApi.get(cancelId).then(function (pendingCertificate) {
|
||||||
|
$scope.pendingCertificate = pendingCertificate;
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.exit = function () {
|
||||||
|
$uibModalInstance.dismiss('cancel');
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.cancel = function (pendingCertificate, cancelOptions) {
|
||||||
|
PendingCertificateService.cancel(pendingCertificate, cancelOptions).then(
|
||||||
|
function () {
|
||||||
|
toaster.pop({
|
||||||
|
type: 'success',
|
||||||
|
title: pendingCertificate.name,
|
||||||
|
body: 'Successfully cancelled pending certificate!'
|
||||||
|
});
|
||||||
|
$uibModalInstance.close();
|
||||||
|
},
|
||||||
|
function (response) {
|
||||||
|
toaster.pop({
|
||||||
|
type: 'error',
|
||||||
|
title: pendingCertificate.name,
|
||||||
|
body: 'lemur-bad-request',
|
||||||
|
bodyOutputType: 'directive',
|
||||||
|
directiveData: response.data,
|
||||||
|
timeout: 100000
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
});
|
|
@ -0,0 +1,28 @@
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="control-label col-sm-2">
|
||||||
|
Replaces
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" ng-model="pendingCertificate.selectedReplaces" placeholder="Certificate123..."
|
||||||
|
uib-typeahead="certificate.name for certificate in certificateService.findCertificatesByName($viewValue)" typeahead-loading="loadingCertificates"
|
||||||
|
class="form-control input-md" typeahead-on-select="pendingCertificate.attachReplaces($item)"
|
||||||
|
uib-tooltip="Lemur will mark any certificates being replaced as 'inactive'"
|
||||||
|
uib-tooltip-trigger="focus" uib-tooltip-placement="top" typeahead-wait-ms="500">
|
||||||
|
<span class="input-group-btn">
|
||||||
|
<button ng-model="replaces.show" class="btn btn-md btn-default" uib-btn-checkbox btn-checkbox-true="1" btn-checkbox-false="0">
|
||||||
|
<span class="badge">{{ pendingCertificate.replaces.length || 0 }}</span>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<table class="table">
|
||||||
|
<tr ng-repeat="replaces in pendingCertificate.replaces track by $index">
|
||||||
|
<td><a class="btn btn-sm btn-info">{{ replaces.name }}</a></td>
|
||||||
|
<td><span class="text-muted">{{ replaces.description }}</span></td>
|
||||||
|
<td>
|
||||||
|
<button type="button" ng-click="pendingCertificate.removeReplaces($index)" class="btn btn-danger btn-sm pull-right">Remove</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -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;
|
||||||
|
});
|
|
@ -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();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
});
|
|
@ -0,0 +1,182 @@
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<h2 class="featurette-heading">Pending Certificates
|
||||||
|
<span class="text-muted"><small>Certificates that almost made it</small></span></h2>
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<div class="btn-group">
|
||||||
|
<button ng-model="showFilter" class="btn btn-default" uib-btn-checkbox
|
||||||
|
btn-checkbox-true="1"
|
||||||
|
btn-checkbox-false="0">Filter</button>
|
||||||
|
</div>
|
||||||
|
<!--<select class="form-control" ng-model="show" ng-options="item.value as item.title for item in fields"></select>-->
|
||||||
|
<div class="clearfix"></div>
|
||||||
|
</div>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table ng-table="pendingCertificateTable" class="table" show-filter="showFilter" template-pagination="angular/pager.html">
|
||||||
|
<tbody>
|
||||||
|
<tr ng-class="{'even-row': $even }" ng-repeat-start="pendingCertificate in $data track by $index">
|
||||||
|
<td data-title="'Id'" filter="{'id': 'text'}">
|
||||||
|
{{ pendingCertificate.id }}
|
||||||
|
</td>
|
||||||
|
<td data-title="'Name'" sortable="'name'" filter="{ 'name': 'text' }">
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<li>{{ pendingCertificate.name }}</li>
|
||||||
|
<li><span class="text-muted">{{ pendingCertificate.owner }}</span></li>
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
<td data-title="'Notify'" filter="{ 'notify': 'select' }" filter-data="getPendingCertificateStatus()">
|
||||||
|
<form>
|
||||||
|
<switch ng-change="updateNotify(pendingCertificate)" id="status" name="status"
|
||||||
|
ng-model="pendingCertificate.notify" class="green small"></switch>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
<td data-title="'Issuer'" sortable="'issuer'" filter="{ 'issuer': 'text' }">
|
||||||
|
{{ pendingCertificate.authority.name || pendingCertificate.issuer }}
|
||||||
|
</td>
|
||||||
|
<td data-title="'Common Name'" filter="{ 'cn': 'text'}">
|
||||||
|
{{ pendingCertificate.cn }}
|
||||||
|
</td>
|
||||||
|
<td data-title="''" style="text-align: center; vertical-align: middle;">
|
||||||
|
<div class="btn-group pull-right" role="group" aria-label="...">
|
||||||
|
<a class="btn btn-sm btn-primary" ui-sref="pending_certificate({name: pendingCertificate.name})">Permalink</a>
|
||||||
|
<button ng-model="pendingCertificate.toggle" class="btn btn-sm btn-info" uib-btn-checkbox btn-checkbox-true="1"
|
||||||
|
btn-checkbox-false="0">More
|
||||||
|
</button>
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<button type="button" class="btn btn-sm btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||||
|
Action
|
||||||
|
<span class="caret"></span>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a href ng-click="edit(pendingCertificate.id)">Edit</a></li>
|
||||||
|
<li><a href ng-click="cancel(pendingCertificate.id)">Cancel</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="warning" ng-if="pendingCertificate.toggle" ng-repeat-end>
|
||||||
|
<td colspan="12">
|
||||||
|
<uib-tabset justified="true" class="col-md-8">
|
||||||
|
<uib-tab>
|
||||||
|
<uib-tab-heading>Basic Info</uib-tab-heading>
|
||||||
|
<ul class="list-group">
|
||||||
|
<li class="list-group-item">
|
||||||
|
<strong>Creator</strong>
|
||||||
|
<span class="pull-right">
|
||||||
|
{{ pendingCertificate.user.email }}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item">
|
||||||
|
<strong>Owner</strong>
|
||||||
|
<span class="pull-right">
|
||||||
|
{{ pendingCertificate.owner }}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item">
|
||||||
|
<strong>Number of Attempts to Fetch</strong>
|
||||||
|
<span class="pull-right">
|
||||||
|
{{ pendingCertificate.numberAttempts }}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item">
|
||||||
|
<strong>Date Created</strong>
|
||||||
|
<span class="pull-right">
|
||||||
|
{{ pendingCertificate.dateCreated | date:'yyyy-MM-dd HH:mm:ss Z' }}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="list-group-item">
|
||||||
|
<strong>Description</strong>
|
||||||
|
<p>{{ pendingCertificate.description }}</p>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</uib-tab>
|
||||||
|
<uib-tab>
|
||||||
|
<uib-tab-heading>Endpoints</uib-tab-heading>
|
||||||
|
<ul class="list-group">
|
||||||
|
<li class="list-group-item" ng-repeat="endpoint in pendingCertificate.endpoints">
|
||||||
|
<span class="pull-right"><label class="label label-default">{{ endpoint.type }}</label></span>
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<li>{{ endpoint.name }}</li>
|
||||||
|
<li><span class="text-muted">{{ endpoint.dnsname }}</span></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</uib-tab>
|
||||||
|
<uib-tab>
|
||||||
|
<uib-tab-heading>Notifications</uib-tab-heading>
|
||||||
|
<ul class="list-group">
|
||||||
|
<li class="list-group-item" ng-repeat="notification in pendingCertificate.notifications">
|
||||||
|
<strong>{{ notification.label }}</strong>
|
||||||
|
<span class="pull-right">{{ notification.description}}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</uib-tab>
|
||||||
|
<uib-tab>
|
||||||
|
<uib-tab-heading>Roles</uib-tab-heading>
|
||||||
|
<ul class="list-group">
|
||||||
|
<li class="list-group-item" ng-repeat="role in pendingCertificate.roles">
|
||||||
|
<strong>{{ role.name }}</strong>
|
||||||
|
<span class="pull-right">{{ role.description}}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</uib-tab>
|
||||||
|
<uib-tab>
|
||||||
|
<uib-tab-heading>Destinations</uib-tab-heading>
|
||||||
|
<ul class="list-group">
|
||||||
|
<li class="list-group-item" ng-repeat="destination in pendingCertificate.destinations">
|
||||||
|
<strong>{{ destination.label }}</strong>
|
||||||
|
<span class="pull-right">{{ destination.description }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</uib-tab>
|
||||||
|
<uib-tab>
|
||||||
|
<uib-tab-heading>Domains</uib-tab-heading>
|
||||||
|
<div class="list-group">
|
||||||
|
<a href="#/domains/{{ domain.id }}" class="list-group-item"
|
||||||
|
ng-repeat="domain in pendingCertificate.domains">{{ domain.name }}</a>
|
||||||
|
</div>
|
||||||
|
</uib-tab>
|
||||||
|
<uib-tab>
|
||||||
|
<uib-tab-heading>Replaces</uib-tab-heading>
|
||||||
|
<ul class="list-group">
|
||||||
|
<li class="list-group-item" ng-repeat="replaces in pendingCertificate.replaces">
|
||||||
|
<strong>{{ replaces.name }}</strong>
|
||||||
|
<p>{{ replaces.description }}</p>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</uib-tab>
|
||||||
|
</uib-tabset>
|
||||||
|
<uib-tabset justified="true" class="col-md-4">
|
||||||
|
<uib-tab>
|
||||||
|
<uib-tab-heading>
|
||||||
|
Chain
|
||||||
|
<i class="glyphicon glyphicon-copy" style="cursor: pointer" clipboard text="pendingCertificate.chain"></i>
|
||||||
|
</uib-tab-heading>
|
||||||
|
<pre style="width: 100%">{{ pendingCertificate.chain }}</pre>
|
||||||
|
</uib-tab>
|
||||||
|
<uib-tab>
|
||||||
|
<uib-tab-heading>
|
||||||
|
Certificate Signing Request
|
||||||
|
<i class="glyphicon glyphicon-copy" style="cursor: pointer" clipboard text="pendingCertificate.csr"></i>
|
||||||
|
</uib-tab-heading>
|
||||||
|
<pre style="width: 100%">{{ pendingCertificate.csr }}</pre>
|
||||||
|
</uib-tab>
|
||||||
|
<uib-tab ng-click="loadPrivateKey(pendingCertificate)">
|
||||||
|
<uib-tab-heading>
|
||||||
|
Private Key
|
||||||
|
<i class="glyphicon glyphicon-copy" style="cursor: pointer" clipboard text="pendingCertificate.privateKey"></i>
|
||||||
|
</uib-tab-heading>
|
||||||
|
<pre style="width: 100%">{{ pendingCertificate.privateKey }}</pre>
|
||||||
|
</uib-tab>
|
||||||
|
</uib-tabset>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -50,6 +50,7 @@
|
||||||
<ul class="nav navbar-nav navbar-left">
|
<ul class="nav navbar-nav navbar-left">
|
||||||
<li><a ui-sref="dashboard">Dashboard</a></li>
|
<li><a ui-sref="dashboard">Dashboard</a></li>
|
||||||
<li><a ui-sref="certificates">Certificates</a></li>
|
<li><a ui-sref="certificates">Certificates</a></li>
|
||||||
|
<li><a ui-sref="pending_certificates">Pending Certificates</a></li>
|
||||||
<li><a ui-sref="authorities">Authorities</a></li>
|
<li><a ui-sref="authorities">Authorities</a></li>
|
||||||
<li><a ui-sref="endpoints">Endpoints</a></li>
|
<li><a ui-sref="endpoints">Endpoints</a></li>
|
||||||
<li><a ui-sref="notifications">Notifications</a></li>
|
<li><a ui-sref="notifications">Notifications</a></li>
|
||||||
|
|
|
@ -14,7 +14,8 @@ from lemur.auth.service import create_token
|
||||||
from lemur.tests.vectors import PRIVATE_KEY_STR
|
from lemur.tests.vectors import PRIVATE_KEY_STR
|
||||||
|
|
||||||
from .factories import ApiKeyFactory, AuthorityFactory, NotificationFactory, DestinationFactory, \
|
from .factories import ApiKeyFactory, AuthorityFactory, NotificationFactory, DestinationFactory, \
|
||||||
CertificateFactory, UserFactory, RoleFactory, SourceFactory, EndpointFactory, RotationPolicyFactory
|
CertificateFactory, UserFactory, RoleFactory, SourceFactory, EndpointFactory, \
|
||||||
|
RotationPolicyFactory, PendingCertificateFactory, AsyncAuthorityFactory
|
||||||
|
|
||||||
|
|
||||||
def pytest_runtest_setup(item):
|
def pytest_runtest_setup(item):
|
||||||
|
@ -90,6 +91,13 @@ def authority(session):
|
||||||
return a
|
return a
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def async_authority(session):
|
||||||
|
a = AsyncAuthorityFactory()
|
||||||
|
session.commit()
|
||||||
|
return a
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def destination(session):
|
def destination(session):
|
||||||
d = DestinationFactory()
|
d = DestinationFactory()
|
||||||
|
@ -144,6 +152,15 @@ def user(session):
|
||||||
return {'user': u, 'token': token}
|
return {'user': u, 'token': token}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def pending_certificate(session):
|
||||||
|
u = UserFactory()
|
||||||
|
a = AsyncAuthorityFactory()
|
||||||
|
p = PendingCertificateFactory(user=u, authority=a)
|
||||||
|
session.commit()
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def admin_user(session):
|
def admin_user(session):
|
||||||
u = UserFactory()
|
u = UserFactory()
|
||||||
|
@ -155,6 +172,14 @@ def admin_user(session):
|
||||||
return {'user': u, 'token': token}
|
return {'user': u, 'token': token}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def async_issuer_plugin():
|
||||||
|
from lemur.plugins.base import register
|
||||||
|
from .plugins.issuer_plugin import TestAsyncIssuerPlugin
|
||||||
|
register(TestAsyncIssuerPlugin)
|
||||||
|
return TestAsyncIssuerPlugin
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def issuer_plugin():
|
def issuer_plugin():
|
||||||
from lemur.plugins.base import register
|
from lemur.plugins.base import register
|
||||||
|
|
|
@ -12,13 +12,14 @@ from lemur.certificates.models import Certificate
|
||||||
from lemur.destinations.models import Destination
|
from lemur.destinations.models import Destination
|
||||||
from lemur.sources.models import Source
|
from lemur.sources.models import Source
|
||||||
from lemur.notifications.models import Notification
|
from lemur.notifications.models import Notification
|
||||||
|
from lemur.pending_certificates.models import PendingCertificate
|
||||||
from lemur.users.models import User
|
from lemur.users.models import User
|
||||||
from lemur.roles.models import Role
|
from lemur.roles.models import Role
|
||||||
from lemur.endpoints.models import Policy, Endpoint
|
from lemur.endpoints.models import Policy, Endpoint
|
||||||
from lemur.policies.models import RotationPolicy
|
from lemur.policies.models import RotationPolicy
|
||||||
from lemur.api_keys.models import ApiKey
|
from lemur.api_keys.models import ApiKey
|
||||||
|
|
||||||
from .vectors import INTERNAL_VALID_SAN_STR, PRIVATE_KEY_STR
|
from .vectors import INTERNAL_VALID_SAN_STR, PRIVATE_KEY_STR, CSR_STR
|
||||||
|
|
||||||
|
|
||||||
class BaseFactory(SQLAlchemyModelFactory):
|
class BaseFactory(SQLAlchemyModelFactory):
|
||||||
|
@ -140,6 +141,15 @@ class AuthorityFactory(BaseFactory):
|
||||||
self.roles.append(role)
|
self.roles.append(role)
|
||||||
|
|
||||||
|
|
||||||
|
class AsyncAuthorityFactory(AuthorityFactory):
|
||||||
|
"""Async Authority factory."""
|
||||||
|
name = Sequence(lambda n: 'authority{0}'.format(n))
|
||||||
|
owner = 'joe@example.com'
|
||||||
|
plugin = {'slug': 'test-issuer-async'}
|
||||||
|
description = FuzzyText(length=128)
|
||||||
|
authority_certificate = SubFactory(CertificateFactory)
|
||||||
|
|
||||||
|
|
||||||
class RotationPolicyFactory(BaseFactory):
|
class RotationPolicyFactory(BaseFactory):
|
||||||
"""Rotation Factory."""
|
"""Rotation Factory."""
|
||||||
name = Sequence(lambda n: 'policy{0}'.format(n))
|
name = Sequence(lambda n: 'policy{0}'.format(n))
|
||||||
|
@ -204,6 +214,7 @@ class UserFactory(BaseFactory):
|
||||||
email = Sequence(lambda n: 'user{0}@example.com'.format(n))
|
email = Sequence(lambda n: 'user{0}@example.com'.format(n))
|
||||||
active = True
|
active = True
|
||||||
password = FuzzyText(length=24)
|
password = FuzzyText(length=24)
|
||||||
|
certificates = []
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Factory Configuration."""
|
"""Factory Configuration."""
|
||||||
|
@ -281,3 +292,93 @@ class ApiKeyFactory(BaseFactory):
|
||||||
|
|
||||||
if extracted:
|
if extracted:
|
||||||
self.userId = extracted.id
|
self.userId = extracted.id
|
||||||
|
|
||||||
|
|
||||||
|
class PendingCertificateFactory(BaseFactory):
|
||||||
|
"""PendingCertificate factory."""
|
||||||
|
name = Sequence(lambda n: 'pending_certificate{0}'.format(n))
|
||||||
|
external_id = 12345
|
||||||
|
csr = CSR_STR
|
||||||
|
chain = INTERNAL_VALID_SAN_STR
|
||||||
|
private_key = PRIVATE_KEY_STR
|
||||||
|
owner = 'joe@example.com'
|
||||||
|
status = FuzzyChoice(['valid', 'revoked', 'unknown'])
|
||||||
|
deleted = False
|
||||||
|
description = FuzzyText(length=128)
|
||||||
|
date_created = FuzzyDate(date(2016, 1, 1), date(2020, 1, 1))
|
||||||
|
number_attempts = 0
|
||||||
|
rename = False
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""Factory Configuration."""
|
||||||
|
model = PendingCertificate
|
||||||
|
|
||||||
|
@post_generation
|
||||||
|
def user(self, create, extracted, **kwargs):
|
||||||
|
if not create:
|
||||||
|
return
|
||||||
|
|
||||||
|
if extracted:
|
||||||
|
self.user_id = extracted.id
|
||||||
|
|
||||||
|
@post_generation
|
||||||
|
def authority(self, create, extracted, **kwargs):
|
||||||
|
if not create:
|
||||||
|
return
|
||||||
|
|
||||||
|
if extracted:
|
||||||
|
self.authority_id = extracted.id
|
||||||
|
|
||||||
|
@post_generation
|
||||||
|
def notifications(self, create, extracted, **kwargs):
|
||||||
|
if not create:
|
||||||
|
return
|
||||||
|
|
||||||
|
if extracted:
|
||||||
|
for notification in extracted:
|
||||||
|
self.notifications.append(notification)
|
||||||
|
|
||||||
|
@post_generation
|
||||||
|
def destinations(self, create, extracted, **kwargs):
|
||||||
|
if not create:
|
||||||
|
return
|
||||||
|
|
||||||
|
if extracted:
|
||||||
|
for destination in extracted:
|
||||||
|
self.destintations.append(destination)
|
||||||
|
|
||||||
|
@post_generation
|
||||||
|
def replaces(self, create, extracted, **kwargs):
|
||||||
|
if not create:
|
||||||
|
return
|
||||||
|
|
||||||
|
if extracted:
|
||||||
|
for replace in extracted:
|
||||||
|
self.replaces.append(replace)
|
||||||
|
|
||||||
|
@post_generation
|
||||||
|
def sources(self, create, extracted, **kwargs):
|
||||||
|
if not create:
|
||||||
|
return
|
||||||
|
|
||||||
|
if extracted:
|
||||||
|
for source in extracted:
|
||||||
|
self.sources.append(source)
|
||||||
|
|
||||||
|
@post_generation
|
||||||
|
def domains(self, create, extracted, **kwargs):
|
||||||
|
if not create:
|
||||||
|
return
|
||||||
|
|
||||||
|
if extracted:
|
||||||
|
for domain in extracted:
|
||||||
|
self.domains.append(domain)
|
||||||
|
|
||||||
|
@post_generation
|
||||||
|
def roles(self, create, extracted, **kwargs):
|
||||||
|
if not create:
|
||||||
|
return
|
||||||
|
|
||||||
|
if extracted:
|
||||||
|
for domain in extracted:
|
||||||
|
self.roles.append(domain)
|
||||||
|
|
|
@ -21,3 +21,29 @@ class TestIssuerPlugin(IssuerPlugin):
|
||||||
def create_authority(options):
|
def create_authority(options):
|
||||||
role = {'username': '', 'password': '', 'name': 'test'}
|
role = {'username': '', 'password': '', 'name': 'test'}
|
||||||
return INTERNAL_VALID_SAN_STR, "", [role]
|
return INTERNAL_VALID_SAN_STR, "", [role]
|
||||||
|
|
||||||
|
|
||||||
|
class TestAsyncIssuerPlugin(IssuerPlugin):
|
||||||
|
title = 'Test Async'
|
||||||
|
slug = 'test-issuer-async'
|
||||||
|
description = 'Enables testing with pending certificates'
|
||||||
|
|
||||||
|
author = 'James Chuong'
|
||||||
|
author_url = 'https://github.com/jchuong'
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(TestAsyncIssuerPlugin, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def create_certificate(self, csr, issuer_options):
|
||||||
|
return "", "", 12345
|
||||||
|
|
||||||
|
def get_ordered_certificate(self, order_id):
|
||||||
|
return INTERNAL_VALID_LONG_STR, INTERNAL_VALID_SAN_STR, 54321
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_authority(options):
|
||||||
|
role = {'username': '', 'password': '', 'name': 'test'}
|
||||||
|
return INTERNAL_VALID_SAN_STR, "", [role]
|
||||||
|
|
||||||
|
def cancel_ordered_certificate(self, pending_certificate, **kwargs):
|
||||||
|
return True
|
||||||
|
|
|
@ -436,7 +436,7 @@ def test_get_account_number(client):
|
||||||
|
|
||||||
def test_mint_certificate(issuer_plugin, authority):
|
def test_mint_certificate(issuer_plugin, authority):
|
||||||
from lemur.certificates.service import mint
|
from lemur.certificates.service import mint
|
||||||
cert_body, private_key, chain, external_id = mint(authority=authority, csr=CSR_STR)
|
cert_body, private_key, chain, external_id, csr = mint(authority=authority, csr=CSR_STR)
|
||||||
assert cert_body == INTERNAL_VALID_LONG_STR, INTERNAL_VALID_SAN_STR
|
assert cert_body == INTERNAL_VALID_LONG_STR, INTERNAL_VALID_SAN_STR
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
import json
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from .vectors import CSR_STR, INTERNAL_VALID_LONG_STR, VALID_ADMIN_API_TOKEN, VALID_ADMIN_HEADER_TOKEN, VALID_USER_HEADER_TOKEN
|
||||||
|
|
||||||
|
from lemur.pending_certificates.views import * # noqa
|
||||||
|
|
||||||
|
|
||||||
|
def test_increment_attempt(pending_certificate):
|
||||||
|
from lemur.pending_certificates.service import increment_attempt
|
||||||
|
initial_attempt = pending_certificate.number_attempts
|
||||||
|
attempts = increment_attempt(pending_certificate)
|
||||||
|
assert attempts == initial_attempt + 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_pending_certificate(async_issuer_plugin, async_authority, user):
|
||||||
|
from lemur.certificates.service import create
|
||||||
|
pending_cert = create(authority=async_authority, csr=CSR_STR, owner='joe@example.com', creator=user['user'], common_name='ACommonName')
|
||||||
|
assert pending_cert.external_id == '12345'
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_pending(pending_certificate, user, session):
|
||||||
|
import copy
|
||||||
|
from lemur.pending_certificates.service import create_certificate, get
|
||||||
|
cert = {'body': INTERNAL_VALID_LONG_STR,
|
||||||
|
'chain': None,
|
||||||
|
'external_id': 54321}
|
||||||
|
|
||||||
|
# Weird copy because the session behavior. pending_certificate is a valid object but the
|
||||||
|
# return of vars(pending_certificate) is a sessionobject, and so nothing from the pending_cert
|
||||||
|
# is used to create the certificate. Maybe a bug due to using vars(), and should copy every
|
||||||
|
# field explicitly.
|
||||||
|
pending_certificate = copy.copy(get(pending_certificate.id))
|
||||||
|
real_cert = create_certificate(pending_certificate, cert, user['user'])
|
||||||
|
assert real_cert.owner == pending_certificate.owner
|
||||||
|
assert real_cert.notify == pending_certificate.notify
|
||||||
|
assert real_cert.private_key == pending_certificate.private_key
|
||||||
|
assert real_cert.external_id == '54321'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("token,status", [
|
||||||
|
(VALID_USER_HEADER_TOKEN, 403),
|
||||||
|
(VALID_ADMIN_HEADER_TOKEN, 204),
|
||||||
|
(VALID_ADMIN_API_TOKEN, 204),
|
||||||
|
('', 401)
|
||||||
|
])
|
||||||
|
def test_pending_cancel(client, pending_certificate, token, status):
|
||||||
|
assert client.delete(api.url_for(PendingCertificates, pending_certificate_id=pending_certificate.id),
|
||||||
|
data=json.dumps({'note': "unit test", 'send_email': False}),
|
||||||
|
headers=token).status_code == status
|
|
@ -43,6 +43,7 @@ class User(db.Model):
|
||||||
profile_picture = Column(String(255))
|
profile_picture = Column(String(255))
|
||||||
roles = relationship('Role', secondary=roles_users, passive_deletes=True, backref=db.backref('user'), lazy='dynamic')
|
roles = relationship('Role', secondary=roles_users, passive_deletes=True, backref=db.backref('user'), lazy='dynamic')
|
||||||
certificates = relationship('Certificate', backref=db.backref('user'), lazy='dynamic')
|
certificates = relationship('Certificate', backref=db.backref('user'), lazy='dynamic')
|
||||||
|
pending_certificates = relationship('PendingCertificate', backref=db.backref('user'), lazy='dynamic')
|
||||||
authorities = relationship('Authority', backref=db.backref('user'), lazy='dynamic')
|
authorities = relationship('Authority', backref=db.backref('user'), lazy='dynamic')
|
||||||
keys = relationship('ApiKey', backref=db.backref('user'), lazy='dynamic')
|
keys = relationship('ApiKey', backref=db.backref('user'), lazy='dynamic')
|
||||||
logs = relationship('Log', backref=db.backref('user'), lazy='dynamic')
|
logs = relationship('Log', backref=db.backref('user'), lazy='dynamic')
|
||||||
|
|
Loading…
Reference in New Issue