Adding the ability to specify a per-certificate rotation policy. (#851)
This commit is contained in:
parent
560bd5a872
commit
443eb43d1f
|
@ -1,10 +1,10 @@
|
||||||
- repo: git://github.com/pre-commit/pre-commit-hooks
|
- repo: git://github.com/pre-commit/pre-commit-hooks
|
||||||
sha: 18d7035de5388cc7775be57f529c154bf541aab9
|
sha: v0.9.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
- id: flake8
|
- id: flake8
|
||||||
- id: check-merge-conflict
|
- id: check-merge-conflict
|
||||||
- repo: git://github.com/pre-commit/mirrors-jshint
|
- repo: git://github.com/pre-commit/mirrors-jshint
|
||||||
sha: e72140112bdd29b18b0c8257956c896c4c3cebcb
|
sha: v2.9.5
|
||||||
hooks:
|
hooks:
|
||||||
- id: jshint
|
- id: jshint
|
||||||
|
|
|
@ -4,6 +4,10 @@ Changelog
|
||||||
0.6 - `master`
|
0.6 - `master`
|
||||||
~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|
||||||
|
Adds per-certificate rotation policies, requires a database migration. The default rotation policy for all certificates
|
||||||
|
is 30 days. Every certificate will gain a policy regardless is auto-rotation is used.
|
||||||
|
|
||||||
.. note:: This version is not yet released and is under active development
|
.. note:: This version is not yet released and is under active development
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||||
"""
|
"""
|
||||||
import arrow
|
import arrow
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
|
|
||||||
|
@ -15,7 +16,7 @@ from cryptography.hazmat.primitives.asymmetric import rsa
|
||||||
from idna.core import InvalidCodepoint
|
from idna.core import InvalidCodepoint
|
||||||
|
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
from sqlalchemy.sql.expression import case
|
from sqlalchemy.sql.expression import case, extract
|
||||||
from sqlalchemy.ext.hybrid import hybrid_property
|
from sqlalchemy.ext.hybrid import hybrid_property
|
||||||
from sqlalchemy import event, Integer, ForeignKey, String, PassiveDefault, func, Column, Text, Boolean
|
from sqlalchemy import event, Integer, ForeignKey, String, PassiveDefault, func, Column, Text, Boolean
|
||||||
|
|
||||||
|
@ -37,6 +38,7 @@ from lemur.models import certificate_associations, certificate_source_associatio
|
||||||
certificate_replacement_associations, roles_certificates
|
certificate_replacement_associations, roles_certificates
|
||||||
|
|
||||||
from lemur.domains.models import Domain
|
from lemur.domains.models import Domain
|
||||||
|
from lemur.policies.models import RotationPolicy
|
||||||
|
|
||||||
|
|
||||||
def get_sequence(name):
|
def get_sequence(name):
|
||||||
|
@ -102,10 +104,10 @@ class Certificate(db.Model):
|
||||||
san = Column(String(1024)) # TODO this should be migrated to boolean
|
san = Column(String(1024)) # TODO this should be migrated to boolean
|
||||||
|
|
||||||
rotation = Column(Boolean, default=False)
|
rotation = Column(Boolean, default=False)
|
||||||
|
|
||||||
user_id = Column(Integer, ForeignKey('users.id'))
|
user_id = Column(Integer, ForeignKey('users.id'))
|
||||||
authority_id = Column(Integer, ForeignKey('authorities.id', ondelete="CASCADE"))
|
authority_id = Column(Integer, ForeignKey('authorities.id', ondelete="CASCADE"))
|
||||||
root_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=certificate_notification_associations, backref='certificate')
|
notifications = relationship('Notification', secondary=certificate_notification_associations, backref='certificate')
|
||||||
destinations = relationship('Destination', secondary=certificate_destination_associations, backref='certificate')
|
destinations = relationship('Destination', secondary=certificate_destination_associations, backref='certificate')
|
||||||
|
@ -120,6 +122,7 @@ class Certificate(db.Model):
|
||||||
|
|
||||||
logs = relationship('Log', backref='certificate')
|
logs = relationship('Log', backref='certificate')
|
||||||
endpoints = relationship('Endpoint', backref='certificate')
|
endpoints = relationship('Endpoint', backref='certificate')
|
||||||
|
rotation_policy = relationship("RotationPolicy")
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
cert = lemur.common.utils.parse_certificate(kwargs['body'])
|
cert = lemur.common.utils.parse_certificate(kwargs['body'])
|
||||||
|
@ -134,7 +137,8 @@ class Certificate(db.Model):
|
||||||
if kwargs.get('name'):
|
if kwargs.get('name'):
|
||||||
self.name = get_or_increase_name(kwargs['name'])
|
self.name = get_or_increase_name(kwargs['name'])
|
||||||
else:
|
else:
|
||||||
self.name = get_or_increase_name(defaults.certificate_name(self.cn, self.issuer, self.not_before, self.not_after, self.san))
|
self.name = get_or_increase_name(
|
||||||
|
defaults.certificate_name(self.cn, self.issuer, self.not_before, self.not_after, self.san))
|
||||||
|
|
||||||
self.owner = kwargs['owner']
|
self.owner = kwargs['owner']
|
||||||
self.body = kwargs['body'].strip()
|
self.body = kwargs['body'].strip()
|
||||||
|
@ -152,6 +156,7 @@ class Certificate(db.Model):
|
||||||
self.roles = list(set(kwargs.get('roles', [])))
|
self.roles = list(set(kwargs.get('roles', [])))
|
||||||
self.replaces = kwargs.get('replaces', [])
|
self.replaces = kwargs.get('replaces', [])
|
||||||
self.rotation = kwargs.get('rotation')
|
self.rotation = kwargs.get('rotation')
|
||||||
|
self.rotation_policy = kwargs.get('rotation_policy')
|
||||||
self.signing_algorithm = defaults.signing_algorithm(cert)
|
self.signing_algorithm = defaults.signing_algorithm(cert)
|
||||||
self.bits = defaults.bitstrength(cert)
|
self.bits = defaults.bitstrength(cert)
|
||||||
self.serial = defaults.serial(cert)
|
self.serial = defaults.serial(cert)
|
||||||
|
@ -240,6 +245,33 @@ class Certificate(db.Model):
|
||||||
else_=False
|
else_=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@hybrid_property
|
||||||
|
def in_rotation_window(self):
|
||||||
|
"""
|
||||||
|
Determines if a certificate is available for rotation based
|
||||||
|
on the rotation policy associated.
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
now = arrow.utcnow()
|
||||||
|
end = now + timedelta(days=self.rotation_policy.days)
|
||||||
|
|
||||||
|
if self.not_after <= end:
|
||||||
|
return True
|
||||||
|
|
||||||
|
@in_rotation_window.expression
|
||||||
|
def in_rotation_window(cls):
|
||||||
|
"""
|
||||||
|
Determines if a certificate is available for rotation based
|
||||||
|
on the rotation policy associated.
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return case(
|
||||||
|
[
|
||||||
|
(extract('day', cls.not_after - func.now()) <= RotationPolicy.days, True)
|
||||||
|
],
|
||||||
|
else_=False
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def extensions(self):
|
def extensions(self):
|
||||||
# setup default values
|
# setup default values
|
||||||
|
@ -298,16 +330,6 @@ class Certificate(db.Model):
|
||||||
|
|
||||||
return return_extensions
|
return return_extensions
|
||||||
|
|
||||||
def get_arn(self, account_number):
|
|
||||||
"""
|
|
||||||
Generate a valid AWS IAM arn
|
|
||||||
|
|
||||||
:rtype : str
|
|
||||||
:param account_number:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
return "arn:aws:iam::{}:server-certificate/{}".format(account_number, self.name)
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "Certificate(name={name})".format(name=self.name)
|
return "Certificate(name={name})".format(name=self.name)
|
||||||
|
|
||||||
|
@ -329,7 +351,8 @@ def update_destinations(target, value, initiator):
|
||||||
destination_plugin.upload(target.name, target.body, target.private_key, target.chain, value.options)
|
destination_plugin.upload(target.name, target.body, target.private_key, target.chain, value.options)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
current_app.logger.exception(e)
|
current_app.logger.exception(e)
|
||||||
metrics.send('destination_upload_failure', 'counter', 1, metric_tags={'certificate': target.name, 'destination': value.label})
|
metrics.send('destination_upload_failure', 'counter', 1,
|
||||||
|
metric_tags={'certificate': target.name, 'destination': value.label})
|
||||||
|
|
||||||
|
|
||||||
@event.listens_for(Certificate.replaces, 'append')
|
@event.listens_for(Certificate.replaces, 'append')
|
||||||
|
|
|
@ -9,8 +9,17 @@ from flask import current_app
|
||||||
from marshmallow import fields, validate, validates_schema, post_load, pre_load
|
from marshmallow import fields, validate, validates_schema, post_load, pre_load
|
||||||
from marshmallow.exceptions import ValidationError
|
from marshmallow.exceptions import ValidationError
|
||||||
|
|
||||||
from lemur.schemas import AssociatedAuthoritySchema, AssociatedDestinationSchema, AssociatedCertificateSchema, \
|
from lemur.schemas import (
|
||||||
AssociatedNotificationSchema, PluginInputSchema, ExtensionSchema, AssociatedRoleSchema, EndpointNestedOutputSchema
|
AssociatedAuthoritySchema,
|
||||||
|
AssociatedDestinationSchema,
|
||||||
|
AssociatedCertificateSchema,
|
||||||
|
AssociatedNotificationSchema,
|
||||||
|
PluginInputSchema,
|
||||||
|
ExtensionSchema,
|
||||||
|
AssociatedRoleSchema,
|
||||||
|
EndpointNestedOutputSchema,
|
||||||
|
AssociatedRotationPolicySchema
|
||||||
|
)
|
||||||
|
|
||||||
from lemur.authorities.schemas import AuthorityNestedOutputSchema
|
from lemur.authorities.schemas import AuthorityNestedOutputSchema
|
||||||
from lemur.destinations.schemas import DestinationNestedOutputSchema
|
from lemur.destinations.schemas import DestinationNestedOutputSchema
|
||||||
|
@ -18,6 +27,7 @@ from lemur.notifications.schemas import NotificationNestedOutputSchema
|
||||||
from lemur.roles.schemas import RoleNestedOutputSchema
|
from lemur.roles.schemas import RoleNestedOutputSchema
|
||||||
from lemur.domains.schemas import DomainNestedOutputSchema
|
from lemur.domains.schemas import DomainNestedOutputSchema
|
||||||
from lemur.users.schemas import UserNestedOutputSchema
|
from lemur.users.schemas import UserNestedOutputSchema
|
||||||
|
from lemur.policies.schemas import RotationPolicyNestedOutputSchema
|
||||||
|
|
||||||
from lemur.common.schema import LemurInputSchema, LemurOutputSchema
|
from lemur.common.schema import LemurInputSchema, LemurOutputSchema
|
||||||
from lemur.common import validators, missing
|
from lemur.common import validators, missing
|
||||||
|
@ -63,6 +73,7 @@ class CertificateInputSchema(CertificateCreationSchema):
|
||||||
|
|
||||||
notify = fields.Boolean(default=True)
|
notify = fields.Boolean(default=True)
|
||||||
rotation = fields.Boolean()
|
rotation = fields.Boolean()
|
||||||
|
rotation_policy = fields.Nested(AssociatedRotationPolicySchema, missing={'name': 'default'}, default={'name': 'default'})
|
||||||
|
|
||||||
# certificate body fields
|
# certificate body fields
|
||||||
organizational_unit = fields.String(missing=lambda: current_app.config.get('LEMUR_DEFAULT_ORGANIZATIONAL_UNIT'))
|
organizational_unit = fields.String(missing=lambda: current_app.config.get('LEMUR_DEFAULT_ORGANIZATIONAL_UNIT'))
|
||||||
|
@ -133,6 +144,7 @@ class CertificateNestedOutputSchema(LemurOutputSchema):
|
||||||
|
|
||||||
rotation = fields.Boolean()
|
rotation = fields.Boolean()
|
||||||
notify = fields.Boolean()
|
notify = fields.Boolean()
|
||||||
|
rotation_policy = fields.Nested(RotationPolicyNestedOutputSchema)
|
||||||
|
|
||||||
# Note aliasing is the first step in deprecating these fields.
|
# Note aliasing is the first step in deprecating these fields.
|
||||||
cn = fields.String() # deprecated
|
cn = fields.String() # deprecated
|
||||||
|
@ -198,6 +210,7 @@ class CertificateOutputSchema(LemurOutputSchema):
|
||||||
roles = fields.Nested(RoleNestedOutputSchema, many=True)
|
roles = fields.Nested(RoleNestedOutputSchema, many=True)
|
||||||
endpoints = fields.Nested(EndpointNestedOutputSchema, many=True, missing=[])
|
endpoints = fields.Nested(EndpointNestedOutputSchema, many=True, missing=[])
|
||||||
replaced_by = fields.Nested(CertificateNestedOutputSchema, many=True, attribute='replaced')
|
replaced_by = fields.Nested(CertificateNestedOutputSchema, many=True, attribute='replaced')
|
||||||
|
rotation_policy = fields.Nested(RotationPolicyNestedOutputSchema)
|
||||||
|
|
||||||
|
|
||||||
class CertificateUploadInputSchema(CertificateCreationSchema):
|
class CertificateUploadInputSchema(CertificateCreationSchema):
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||||
"""
|
"""
|
||||||
import arrow
|
import arrow
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
from sqlalchemy import func, or_, not_, cast, Boolean, Integer
|
from sqlalchemy import func, or_, not_, cast, Boolean, Integer
|
||||||
|
@ -85,20 +84,15 @@ def get_all_pending_reissue():
|
||||||
"""
|
"""
|
||||||
Retrieves all certificates that need to be rotated.
|
Retrieves all certificates that need to be rotated.
|
||||||
|
|
||||||
Must be X days from expiration, uses `LEMUR_DEFAULT_ROTATION_INTERVAL`
|
Must be X days from expiration, uses the certificates rotation
|
||||||
to determine how many days from expiration the certificate must be
|
policy to determine how many days from expiration the certificate must be
|
||||||
for rotation to be pending.
|
for rotation to be pending.
|
||||||
|
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
now = arrow.utcnow()
|
|
||||||
interval = current_app.config.get('LEMUR_DEFAULT_ROTATION_INTERVAL', 30)
|
|
||||||
end = now + timedelta(days=interval)
|
|
||||||
|
|
||||||
return Certificate.query.filter(Certificate.rotation == True)\
|
return Certificate.query.filter(Certificate.rotation == True)\
|
||||||
.filter(Certificate.endpoints.any())\
|
|
||||||
.filter(not_(Certificate.replaced.any()))\
|
.filter(not_(Certificate.replaced.any()))\
|
||||||
.filter(Certificate.not_after <= end.format('YYYY-MM-DD')).all() # noqa
|
.filter(Certificate.in_rotation_window == True).all() # noqa
|
||||||
|
|
||||||
|
|
||||||
def find_duplicates(cert):
|
def find_duplicates(cert):
|
||||||
|
|
|
@ -232,6 +232,8 @@ class CertificatesList(AuthenticatedResource):
|
||||||
"replaces": [{
|
"replaces": [{
|
||||||
"id": 1
|
"id": 1
|
||||||
}],
|
}],
|
||||||
|
"rotation": True,
|
||||||
|
"rotationPolicy": {"name": "default"},
|
||||||
"name": "WILDCARD.test.example.net-SymantecCorporation-20160603-20180112",
|
"name": "WILDCARD.test.example.net-SymantecCorporation-20160603-20180112",
|
||||||
"roles": [{
|
"roles": [{
|
||||||
"id": 464,
|
"id": 464,
|
||||||
|
@ -241,18 +243,6 @@ class CertificatesList(AuthenticatedResource):
|
||||||
"san": null
|
"san": null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
:arg extensions: extensions to be used in the certificate
|
|
||||||
:arg description: description for new certificate
|
|
||||||
:arg owner: owner email
|
|
||||||
:arg validityStart: when the certificate should start being valid
|
|
||||||
:arg validityEnd: when the certificate should expire
|
|
||||||
:arg authority: authority that should issue the certificate
|
|
||||||
:arg country: country for the CSR
|
|
||||||
:arg state: state for the CSR
|
|
||||||
:arg location: location for the CSR
|
|
||||||
:arg organization: organization for CSR
|
|
||||||
:arg commonName: certificate common name
|
|
||||||
:reqheader Authorization: OAuth token to authenticate
|
:reqheader Authorization: OAuth token to authenticate
|
||||||
:statuscode 200: no error
|
:statuscode 200: no error
|
||||||
:statuscode 403: unauthenticated
|
:statuscode 403: unauthenticated
|
||||||
|
@ -356,6 +346,8 @@ class CertificatesUpload(AuthenticatedResource):
|
||||||
"name": "*.test.example.net"
|
"name": "*.test.example.net"
|
||||||
}],
|
}],
|
||||||
"replaces": [],
|
"replaces": [],
|
||||||
|
"rotation": True,
|
||||||
|
"rotationPolicy": {"name": "default"},
|
||||||
"name": "WILDCARD.test.example.net-SymantecCorporation-20160603-20180112",
|
"name": "WILDCARD.test.example.net-SymantecCorporation-20160603-20180112",
|
||||||
"roles": [{
|
"roles": [{
|
||||||
"id": 464,
|
"id": 464,
|
||||||
|
@ -365,11 +357,6 @@ class CertificatesUpload(AuthenticatedResource):
|
||||||
"san": null
|
"san": null
|
||||||
}
|
}
|
||||||
|
|
||||||
:arg owner: owner email for certificate
|
|
||||||
:arg publicCert: valid PEM public key for certificate
|
|
||||||
:arg intermediateCert valid PEM intermediate key for certificate
|
|
||||||
:arg privateKey: valid PEM private key for certificate
|
|
||||||
:arg destinations: list of aws destinations to upload the certificate to
|
|
||||||
:reqheader Authorization: OAuth token to authenticate
|
:reqheader Authorization: OAuth token to authenticate
|
||||||
:statuscode 403: unauthenticated
|
:statuscode 403: unauthenticated
|
||||||
:statuscode 200: no error
|
:statuscode 200: no error
|
||||||
|
@ -522,6 +509,8 @@ class Certificates(AuthenticatedResource):
|
||||||
"id": 1090,
|
"id": 1090,
|
||||||
"name": "*.test.example.net"
|
"name": "*.test.example.net"
|
||||||
}],
|
}],
|
||||||
|
"rotation": True,
|
||||||
|
"rotationPolicy": {"name": "default"},
|
||||||
"replaces": [],
|
"replaces": [],
|
||||||
"replaced": [],
|
"replaced": [],
|
||||||
"name": "WILDCARD.test.example.net-SymantecCorporation-20160603-20180112",
|
"name": "WILDCARD.test.example.net-SymantecCorporation-20160603-20180112",
|
||||||
|
@ -616,6 +605,8 @@ class Certificates(AuthenticatedResource):
|
||||||
"description": "This is a google group based role created by Lemur",
|
"description": "This is a google group based role created by Lemur",
|
||||||
"name": "joe@example.com"
|
"name": "joe@example.com"
|
||||||
}],
|
}],
|
||||||
|
"rotation": True,
|
||||||
|
"rotationPolicy": {"name": "default"},
|
||||||
"san": null
|
"san": null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -722,6 +713,8 @@ class NotificationCertificatesList(AuthenticatedResource):
|
||||||
}],
|
}],
|
||||||
"replaces": [],
|
"replaces": [],
|
||||||
"replaced": [],
|
"replaced": [],
|
||||||
|
"rotation": True,
|
||||||
|
"rotationPolicy": {"name": "default"},
|
||||||
"name": "WILDCARD.test.example.net-SymantecCorporation-20160603-20180112",
|
"name": "WILDCARD.test.example.net-SymantecCorporation-20160603-20180112",
|
||||||
"roles": [{
|
"roles": [{
|
||||||
"id": 464,
|
"id": 464,
|
||||||
|
@ -827,6 +820,8 @@ class CertificatesReplacementsList(AuthenticatedResource):
|
||||||
}],
|
}],
|
||||||
"replaces": [],
|
"replaces": [],
|
||||||
"replaced": [],
|
"replaced": [],
|
||||||
|
"rotation": True,
|
||||||
|
"rotationPolicy": {"name": "default"},
|
||||||
"name": "WILDCARD.test.example.net-SymantecCorporation-20160603-20180112",
|
"name": "WILDCARD.test.example.net-SymantecCorporation-20160603-20180112",
|
||||||
"roles": [{
|
"roles": [{
|
||||||
"id": 464,
|
"id": 464,
|
||||||
|
|
|
@ -10,7 +10,6 @@ def rotate_certificate(endpoint, new_cert):
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
# ensure that certificate is available for rotation
|
# ensure that certificate is available for rotation
|
||||||
|
|
||||||
endpoint.source.plugin.update_endpoint(endpoint, new_cert)
|
endpoint.source.plugin.update_endpoint(endpoint, new_cert)
|
||||||
endpoint.certificate = new_cert
|
endpoint.certificate = new_cert
|
||||||
database.update(endpoint)
|
database.update(endpoint)
|
||||||
|
|
|
@ -16,16 +16,18 @@ from flask_migrate import Migrate, MigrateCommand, stamp
|
||||||
from flask_script.commands import ShowUrls, Clean, Server
|
from flask_script.commands import ShowUrls, Clean, Server
|
||||||
|
|
||||||
from lemur.sources.cli import manager as source_manager
|
from lemur.sources.cli import manager as source_manager
|
||||||
|
from lemur.policies.cli import manager as policy_manager
|
||||||
|
from lemur.reporting.cli import manager as report_manager
|
||||||
|
from lemur.endpoints.cli import manager as endpoint_manager
|
||||||
from lemur.certificates.cli import manager as certificate_manager
|
from lemur.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.endpoints.cli import manager as endpoint_manager
|
|
||||||
from lemur.reporting.cli import manager as report_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
|
||||||
from lemur.roles import service as role_service
|
from lemur.roles import service as role_service
|
||||||
|
from lemur.policies import service as policy_service
|
||||||
from lemur.notifications import service as notification_service
|
from lemur.notifications import service as notification_service
|
||||||
|
|
||||||
|
|
||||||
from lemur.common.utils import validate_conf
|
from lemur.common.utils import validate_conf
|
||||||
|
|
||||||
from lemur import create_app
|
from lemur import create_app
|
||||||
|
@ -40,6 +42,8 @@ from lemur.domains.models import Domain # noqa
|
||||||
from lemur.notifications.models import Notification # noqa
|
from lemur.notifications.models import Notification # noqa
|
||||||
from lemur.sources.models import Source # noqa
|
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.policies.models import RotationPolicy # noqa
|
||||||
|
|
||||||
|
|
||||||
manager = Manager(create_app)
|
manager = Manager(create_app)
|
||||||
|
@ -242,6 +246,12 @@ class InitializeApp(Command):
|
||||||
recipients = current_app.config.get('LEMUR_SECURITY_TEAM_EMAIL')
|
recipients = current_app.config.get('LEMUR_SECURITY_TEAM_EMAIL')
|
||||||
notification_service.create_default_expiration_notifications("DEFAULT_SECURITY", recipients=recipients)
|
notification_service.create_default_expiration_notifications("DEFAULT_SECURITY", recipients=recipients)
|
||||||
|
|
||||||
|
days = current_app.config.get("LEMUR_DEFAULT_ROTATION_INTERVAL", 30)
|
||||||
|
sys.stdout.write("[+] Creating default certificate rotation policy of {days} days before issuance.\n".format(
|
||||||
|
days=days
|
||||||
|
))
|
||||||
|
|
||||||
|
policy_service.create(days=days)
|
||||||
sys.stdout.write("[/] Done!\n")
|
sys.stdout.write("[/] Done!\n")
|
||||||
|
|
||||||
|
|
||||||
|
@ -531,6 +541,7 @@ def main():
|
||||||
manager.add_command("notify", notification_manager)
|
manager.add_command("notify", notification_manager)
|
||||||
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.run()
|
manager.run()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
"""Adds support for rotation policies.
|
||||||
|
|
||||||
|
Creates a default rotation policy (30 days) with the name
|
||||||
|
'default' ensures that all existing certificates use the default
|
||||||
|
policy.
|
||||||
|
|
||||||
|
Revision ID: a02a678ddc25
|
||||||
|
Revises: 8ae67285ff14
|
||||||
|
Create Date: 2017-07-12 11:45:49.257927
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'a02a678ddc25'
|
||||||
|
down_revision = '8ae67285ff14'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.sql import text
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('rotation_policies',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('name', sa.String(), nullable=True),
|
||||||
|
sa.Column('days', sa.Integer(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.add_column('certificates', sa.Column('rotation_policy_id', sa.Integer(), nullable=True))
|
||||||
|
op.create_foreign_key(None, 'certificates', 'rotation_policies', ['rotation_policy_id'], ['id'])
|
||||||
|
|
||||||
|
conn = op.get_bind()
|
||||||
|
stmt = text('insert into rotation_policies (days, name) values (:days, :name)')
|
||||||
|
stmt = stmt.bindparams(days=30, name='default')
|
||||||
|
conn.execute(stmt)
|
||||||
|
|
||||||
|
stmt = text('select id from rotation_policies where name=:name')
|
||||||
|
stmt = stmt.bindparams(name='default')
|
||||||
|
rotation_policy_id = conn.execute(stmt).fetchone()[0]
|
||||||
|
|
||||||
|
stmt = text('update certificates set rotation_policy_id=:rotation_policy_id')
|
||||||
|
stmt = stmt.bindparams(rotation_policy_id=rotation_policy_id)
|
||||||
|
conn.execute(stmt)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_constraint(None, 'certificates', type_='foreignkey')
|
||||||
|
op.drop_column('certificates', 'rotation_policy_id')
|
||||||
|
op.drop_index('certificate_replacement_associations_ix', table_name='certificate_replacement_associations')
|
||||||
|
op.create_index('certificate_replacement_associations_ix', 'certificate_replacement_associations', ['replaced_certificate_id', 'certificate_id'], unique=True)
|
||||||
|
op.drop_table('rotation_policies')
|
||||||
|
# ### end Alembic commands ###
|
|
@ -0,0 +1,24 @@
|
||||||
|
"""
|
||||||
|
.. module: lemur.policies.cli
|
||||||
|
:platform: Unix
|
||||||
|
:copyright: (c) 2017 by Netflix Inc., see AUTHORS for more
|
||||||
|
:license: Apache, see LICENSE for more details.
|
||||||
|
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||||
|
"""
|
||||||
|
from flask_script import Manager
|
||||||
|
from lemur.policies import service as policy_service
|
||||||
|
|
||||||
|
|
||||||
|
manager = Manager(usage="Handles all policy related tasks.")
|
||||||
|
|
||||||
|
|
||||||
|
@manager.option('-d', '--days', dest='days', help='Number of days before expiration.')
|
||||||
|
@manager.option('-n', '--name', dest='name', help='Policy name.')
|
||||||
|
def create(days, name):
|
||||||
|
"""
|
||||||
|
Create a new certificate rotation policy
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
print("[+] Creating a new certificate rotation policy.")
|
||||||
|
policy_service.create(days=days, name=name)
|
||||||
|
print("[+] Successfully created a new certificate rotation policy")
|
|
@ -0,0 +1,21 @@
|
||||||
|
"""
|
||||||
|
.. module: lemur.policies.models
|
||||||
|
:platform: unix
|
||||||
|
:synopsis: This module contains all of the models need to create a certificate policy within Lemur.
|
||||||
|
:copyright: (c) 2017 by Netflix Inc., see AUTHORS for more
|
||||||
|
:license: Apache, see LICENSE for more details.
|
||||||
|
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||||
|
"""
|
||||||
|
from sqlalchemy import Column, Integer, String
|
||||||
|
|
||||||
|
from lemur.database import db
|
||||||
|
|
||||||
|
|
||||||
|
class RotationPolicy(db.Model):
|
||||||
|
__tablename__ = 'rotation_policies'
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
name = Column(String)
|
||||||
|
days = Column(Integer)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "RotationPolicy(days={days}, name={name})".format(days=self.days, name=self.name)
|
|
@ -0,0 +1,19 @@
|
||||||
|
"""
|
||||||
|
.. module: lemur.policies.schemas
|
||||||
|
:platform: unix
|
||||||
|
:copyright: (c) 2017 by Netflix Inc., see AUTHORS for more
|
||||||
|
:license: Apache, see LICENSE for more details.
|
||||||
|
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||||
|
"""
|
||||||
|
from marshmallow import fields
|
||||||
|
|
||||||
|
from lemur.common.schema import LemurOutputSchema
|
||||||
|
|
||||||
|
|
||||||
|
class RotationPolicyOutputSchema(LemurOutputSchema):
|
||||||
|
id = fields.Integer()
|
||||||
|
days = fields.Integer()
|
||||||
|
|
||||||
|
|
||||||
|
class RotationPolicyNestedOutputSchema(RotationPolicyOutputSchema):
|
||||||
|
pass
|
|
@ -0,0 +1,62 @@
|
||||||
|
"""
|
||||||
|
.. module: lemur.policies.service
|
||||||
|
:platform: Unix
|
||||||
|
:copyright: (c) 2017 by Netflix Inc., see AUTHORS for more
|
||||||
|
:license: Apache, see LICENSE for more details.
|
||||||
|
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||||
|
"""
|
||||||
|
from lemur import database
|
||||||
|
from lemur.policies.models import RotationPolicy
|
||||||
|
|
||||||
|
|
||||||
|
def get(policy_id):
|
||||||
|
"""
|
||||||
|
Retrieves policy by its ID.
|
||||||
|
:param policy_id:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return database.get(RotationPolicy, policy_id)
|
||||||
|
|
||||||
|
|
||||||
|
def delete(policy_id):
|
||||||
|
"""
|
||||||
|
Delete a rotation policy.
|
||||||
|
:param policy_id:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
database.delete(get(policy_id))
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_policies():
|
||||||
|
"""
|
||||||
|
Retrieves all rotation policies.
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return RotationPolicy.query.all()
|
||||||
|
|
||||||
|
|
||||||
|
def create(**kwargs):
|
||||||
|
"""
|
||||||
|
Creates a new rotation policy.
|
||||||
|
|
||||||
|
:param kwargs:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
policy = RotationPolicy(**kwargs)
|
||||||
|
database.create(policy)
|
||||||
|
return policy
|
||||||
|
|
||||||
|
|
||||||
|
def update(policy_id, **kwargs):
|
||||||
|
"""
|
||||||
|
Updates a policy.
|
||||||
|
:param policy_id:
|
||||||
|
:param kwargs:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
policy = get(policy_id)
|
||||||
|
|
||||||
|
for key, value in kwargs.items():
|
||||||
|
setattr(policy, key, value)
|
||||||
|
|
||||||
|
return database.update(policy)
|
|
@ -21,6 +21,7 @@ from lemur.plugins.utils import get_plugin_option
|
||||||
from lemur.roles.models import Role
|
from lemur.roles.models import Role
|
||||||
from lemur.users.models import User
|
from lemur.users.models import User
|
||||||
from lemur.authorities.models import Authority
|
from lemur.authorities.models import Authority
|
||||||
|
from lemur.policies.models import RotationPolicy
|
||||||
from lemur.certificates.models import Certificate
|
from lemur.certificates.models import Certificate
|
||||||
from lemur.destinations.models import Destination
|
from lemur.destinations.models import Destination
|
||||||
from lemur.notifications.models import Notification
|
from lemur.notifications.models import Notification
|
||||||
|
@ -149,6 +150,15 @@ class AssociatedUserSchema(LemurInputSchema):
|
||||||
return fetch_objects(User, data, many=many)
|
return fetch_objects(User, data, many=many)
|
||||||
|
|
||||||
|
|
||||||
|
class AssociatedRotationPolicySchema(LemurInputSchema):
|
||||||
|
id = fields.Int()
|
||||||
|
name = fields.String()
|
||||||
|
|
||||||
|
@post_load
|
||||||
|
def get_object(self, data, many=False):
|
||||||
|
return fetch_objects(RotationPolicy, data, many=many)
|
||||||
|
|
||||||
|
|
||||||
class PluginInputSchema(LemurInputSchema):
|
class PluginInputSchema(LemurInputSchema):
|
||||||
plugin_options = fields.List(fields.Dict(), validate=validate_options)
|
plugin_options = fields.List(fields.Dict(), validate=validate_options)
|
||||||
slug = fields.String(required=True)
|
slug = fields.String(required=True)
|
||||||
|
|
|
@ -6,7 +6,7 @@ from lemur.database import db as _db
|
||||||
from lemur.auth.service import create_token
|
from lemur.auth.service import create_token
|
||||||
|
|
||||||
from .factories import AuthorityFactory, NotificationFactory, DestinationFactory, \
|
from .factories import AuthorityFactory, NotificationFactory, DestinationFactory, \
|
||||||
CertificateFactory, UserFactory, RoleFactory, SourceFactory, EndpointFactory
|
CertificateFactory, UserFactory, RoleFactory, SourceFactory, EndpointFactory, RotationPolicyFactory
|
||||||
|
|
||||||
|
|
||||||
def pytest_runtest_setup(item):
|
def pytest_runtest_setup(item):
|
||||||
|
@ -51,6 +51,7 @@ def db(app, request):
|
||||||
UserFactory()
|
UserFactory()
|
||||||
r = RoleFactory(name='admin')
|
r = RoleFactory(name='admin')
|
||||||
UserFactory(roles=[r])
|
UserFactory(roles=[r])
|
||||||
|
rp = RotationPolicyFactory(name='default')
|
||||||
|
|
||||||
_db.session.commit()
|
_db.session.commit()
|
||||||
yield _db
|
yield _db
|
||||||
|
|
|
@ -15,6 +15,7 @@ from lemur.notifications.models import Notification
|
||||||
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 .vectors import INTERNAL_VALID_SAN_STR, PRIVATE_KEY_STR
|
from .vectors import INTERNAL_VALID_SAN_STR, PRIVATE_KEY_STR
|
||||||
|
|
||||||
|
@ -137,6 +138,16 @@ class AuthorityFactory(BaseFactory):
|
||||||
self.roles.append(role)
|
self.roles.append(role)
|
||||||
|
|
||||||
|
|
||||||
|
class RotationPolicyFactory(BaseFactory):
|
||||||
|
"""Rotation Factory."""
|
||||||
|
name = Sequence(lambda n: 'policy{0}'.format(n))
|
||||||
|
days = 30
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""Factory configuration."""
|
||||||
|
model = RotationPolicy
|
||||||
|
|
||||||
|
|
||||||
class DestinationFactory(BaseFactory):
|
class DestinationFactory(BaseFactory):
|
||||||
"""Destination factory."""
|
"""Destination factory."""
|
||||||
plugin_name = 'test-destination'
|
plugin_name = 'test-destination'
|
||||||
|
|
|
@ -155,7 +155,7 @@ def test_certificate_input_schema(client, authority):
|
||||||
assert data['country'] == 'US'
|
assert data['country'] == 'US'
|
||||||
assert data['location'] == 'Los Gatos'
|
assert data['location'] == 'Los Gatos'
|
||||||
|
|
||||||
assert len(data.keys()) == 17
|
assert len(data.keys()) == 18
|
||||||
|
|
||||||
|
|
||||||
def test_certificate_input_with_extensions(client, authority):
|
def test_certificate_input_with_extensions(client, authority):
|
||||||
|
|
Loading…
Reference in New Issue