parent
b44a7c73d8
commit
fe9703dd94
|
@ -79,7 +79,7 @@ gulp.task('dev:styles', function () {
|
||||||
'bower_components/angular-loading-bar/src/loading-bar.css',
|
'bower_components/angular-loading-bar/src/loading-bar.css',
|
||||||
'bower_components/angular-ui-switch/angular-ui-switch.css',
|
'bower_components/angular-ui-switch/angular-ui-switch.css',
|
||||||
'bower_components/angular-wizard/dist/angular-wizard.css',
|
'bower_components/angular-wizard/dist/angular-wizard.css',
|
||||||
'bower_components/ng-table/ng-table.css',
|
'bower_components/ng-table/dist/ng-table.css',
|
||||||
'bower_components/angularjs-toaster/toaster.css',
|
'bower_components/angularjs-toaster/toaster.css',
|
||||||
'bower_components/angular-ui-select/dist/select.css',
|
'bower_components/angular-ui-select/dist/select.css',
|
||||||
'lemur/static/app/styles/lemur.css'
|
'lemur/static/app/styles/lemur.css'
|
||||||
|
|
|
@ -24,6 +24,7 @@ from lemur.defaults.views import mod as defaults_bp
|
||||||
from lemur.plugins.views import mod as plugins_bp
|
from lemur.plugins.views import mod as plugins_bp
|
||||||
from lemur.notifications.views import mod as notifications_bp
|
from lemur.notifications.views import mod as notifications_bp
|
||||||
from lemur.sources.views import mod as sources_bp
|
from lemur.sources.views import mod as sources_bp
|
||||||
|
from lemur.endpoints.views import mod as endpoints_bp
|
||||||
|
|
||||||
from lemur.__about__ import (
|
from lemur.__about__ import (
|
||||||
__author__, __copyright__, __email__, __license__, __summary__, __title__,
|
__author__, __copyright__, __email__, __license__, __summary__, __title__,
|
||||||
|
@ -47,7 +48,8 @@ LEMUR_BLUEPRINTS = (
|
||||||
defaults_bp,
|
defaults_bp,
|
||||||
plugins_bp,
|
plugins_bp,
|
||||||
notifications_bp,
|
notifications_bp,
|
||||||
sources_bp
|
sources_bp,
|
||||||
|
endpoints_bp
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -98,16 +98,6 @@ def create(**kwargs):
|
||||||
else:
|
else:
|
||||||
kwargs['roles'] = roles
|
kwargs['roles'] = roles
|
||||||
|
|
||||||
if kwargs['type'] == 'subca':
|
|
||||||
description = "This is the ROOT certificate for the {0} sub certificate authority the parent \
|
|
||||||
authority is {1}.".format(kwargs.get('name'), kwargs.get('parent'))
|
|
||||||
else:
|
|
||||||
description = "This is the ROOT certificate for the {0} certificate authority.".format(
|
|
||||||
kwargs.get('name')
|
|
||||||
)
|
|
||||||
|
|
||||||
kwargs['description'] = description
|
|
||||||
|
|
||||||
cert = upload(**kwargs)
|
cert = upload(**kwargs)
|
||||||
kwargs['authority_certificate'] = cert
|
kwargs['authority_certificate'] = cert
|
||||||
|
|
||||||
|
|
|
@ -9,8 +9,10 @@ import datetime
|
||||||
|
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
|
|
||||||
from sqlalchemy import event, Integer, ForeignKey, String, DateTime, PassiveDefault, func, Column, Text, Boolean
|
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
|
from sqlalchemy.sql.expression import case
|
||||||
|
from sqlalchemy.ext.hybrid import hybrid_property
|
||||||
|
from sqlalchemy import event, Integer, ForeignKey, String, DateTime, PassiveDefault, func, Column, Text, Boolean
|
||||||
|
|
||||||
from lemur.database import db
|
from lemur.database import db
|
||||||
from lemur.models import certificate_associations, certificate_source_associations, \
|
from lemur.models import certificate_associations, certificate_source_associations, \
|
||||||
|
@ -73,6 +75,8 @@ 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')
|
||||||
|
|
||||||
|
endpoints = relationship("Endpoint", backref='certificate')
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
cert = defaults.parse_certificate(kwargs['body'])
|
cert = defaults.parse_certificate(kwargs['body'])
|
||||||
|
|
||||||
|
@ -104,22 +108,33 @@ class Certificate(db.Model):
|
||||||
for domain in defaults.domains(cert):
|
for domain in defaults.domains(cert):
|
||||||
self.domains.append(Domain(name=domain))
|
self.domains.append(Domain(name=domain))
|
||||||
|
|
||||||
@property
|
@hybrid_property
|
||||||
def is_expired(self):
|
def expired(self):
|
||||||
if self.not_after < datetime.datetime.now():
|
if self.not_after <= datetime.datetime.now():
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@property
|
@expired.expression
|
||||||
def is_unused(self):
|
def expired(cls):
|
||||||
if self.elb_listeners.count() == 0:
|
return case(
|
||||||
|
[
|
||||||
|
(cls.now_after <= datetime.datetime.now(), True)
|
||||||
|
],
|
||||||
|
else_=False
|
||||||
|
)
|
||||||
|
|
||||||
|
@hybrid_property
|
||||||
|
def revoked(self):
|
||||||
|
if 'revoked' == self.status:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@property
|
@revoked.expression
|
||||||
def is_revoked(self):
|
def revoked(cls):
|
||||||
# we might not yet know the condition of the cert
|
return case(
|
||||||
if self.status:
|
[
|
||||||
if 'revoked' in self.status:
|
(cls.status == 'revoked', True)
|
||||||
return True
|
],
|
||||||
|
else_=False
|
||||||
|
)
|
||||||
|
|
||||||
def get_arn(self, account_number):
|
def get_arn(self, account_number):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -10,7 +10,7 @@ from marshmallow import fields, validates_schema, post_load
|
||||||
from marshmallow.exceptions import ValidationError
|
from marshmallow.exceptions import ValidationError
|
||||||
|
|
||||||
from lemur.schemas import AssociatedAuthoritySchema, AssociatedDestinationSchema, AssociatedCertificateSchema, \
|
from lemur.schemas import AssociatedAuthoritySchema, AssociatedDestinationSchema, AssociatedCertificateSchema, \
|
||||||
AssociatedNotificationSchema, PluginInputSchema, ExtensionSchema, AssociatedRoleSchema
|
AssociatedNotificationSchema, PluginInputSchema, ExtensionSchema, AssociatedRoleSchema, EndpointNestedOutputSchema
|
||||||
|
|
||||||
from lemur.authorities.schemas import AuthorityNestedOutputSchema
|
from lemur.authorities.schemas import AuthorityNestedOutputSchema
|
||||||
from lemur.destinations.schemas import DestinationNestedOutputSchema
|
from lemur.destinations.schemas import DestinationNestedOutputSchema
|
||||||
|
@ -120,7 +120,7 @@ class CertificateOutputSchema(LemurOutputSchema):
|
||||||
replaces = fields.Nested(CertificateNestedOutputSchema, many=True)
|
replaces = fields.Nested(CertificateNestedOutputSchema, many=True)
|
||||||
authority = fields.Nested(AuthorityNestedOutputSchema)
|
authority = fields.Nested(AuthorityNestedOutputSchema)
|
||||||
roles = fields.Nested(RoleNestedOutputSchema, many=True)
|
roles = fields.Nested(RoleNestedOutputSchema, many=True)
|
||||||
endpoints = fields.List(fields.Dict(), missing=[])
|
endpoints = fields.Nested(EndpointNestedOutputSchema, many=True, missing=[])
|
||||||
|
|
||||||
|
|
||||||
class CertificateUploadInputSchema(CertificateSchema):
|
class CertificateUploadInputSchema(CertificateSchema):
|
||||||
|
|
|
@ -177,6 +177,7 @@ def upload(**kwargs):
|
||||||
"""
|
"""
|
||||||
Allows for pre-made certificates to be imported into Lemur.
|
Allows for pre-made certificates to be imported into Lemur.
|
||||||
"""
|
"""
|
||||||
|
from lemur.users import service as user_service
|
||||||
roles = create_certificate_roles(**kwargs)
|
roles = create_certificate_roles(**kwargs)
|
||||||
|
|
||||||
if kwargs.get('roles'):
|
if kwargs.get('roles'):
|
||||||
|
@ -187,10 +188,14 @@ def upload(**kwargs):
|
||||||
cert = Certificate(**kwargs)
|
cert = Certificate(**kwargs)
|
||||||
|
|
||||||
cert = database.create(cert)
|
cert = database.create(cert)
|
||||||
g.user.certificates.append(cert)
|
|
||||||
|
|
||||||
database.update(cert)
|
try:
|
||||||
return cert
|
g.user.certificates.append(cert)
|
||||||
|
except AttributeError:
|
||||||
|
user = user_service.get_by_email('lemur@nobody')
|
||||||
|
user.certificates.append(cert)
|
||||||
|
|
||||||
|
return database.update(cert)
|
||||||
|
|
||||||
|
|
||||||
def create(**kwargs):
|
def create(**kwargs):
|
||||||
|
|
|
@ -132,7 +132,10 @@ def bitstrength(cert):
|
||||||
:param cert:
|
:param cert:
|
||||||
:return: Integer
|
:return: Integer
|
||||||
"""
|
"""
|
||||||
return cert.public_key().key_size
|
try:
|
||||||
|
return cert.public_key().key_size
|
||||||
|
except AttributeError:
|
||||||
|
current_app.logger.debug('Unable to get bitstrength.')
|
||||||
|
|
||||||
|
|
||||||
def issuer(cert):
|
def issuer(cert):
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
"""
|
||||||
|
.. module: lemur.endpoints.models
|
||||||
|
:platform: unix
|
||||||
|
:synopsis: This module contains all of the models need to create a authority within Lemur.
|
||||||
|
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||||
|
:license: Apache, see LICENSE for more details.
|
||||||
|
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||||
|
"""
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
from sqlalchemy import Column, Integer, String, func, DateTime, PassiveDefault, Boolean, ForeignKey
|
||||||
|
from sqlalchemy.ext.hybrid import hybrid_property
|
||||||
|
from sqlalchemy.sql.expression import case
|
||||||
|
|
||||||
|
from lemur.database import db
|
||||||
|
|
||||||
|
from lemur.models import policies_ciphers
|
||||||
|
|
||||||
|
|
||||||
|
BAD_CIPHERS = [
|
||||||
|
'Protocol-SSLv3',
|
||||||
|
'Protocol-SSLv2',
|
||||||
|
'Protocol-TLSv1'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class Cipher(db.Model):
|
||||||
|
__tablename__ = 'ciphers'
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
name = Column(String(128), nullable=False)
|
||||||
|
|
||||||
|
@hybrid_property
|
||||||
|
def deprecated(self):
|
||||||
|
return self.name in BAD_CIPHERS
|
||||||
|
|
||||||
|
@deprecated.expression
|
||||||
|
def deprecated(cls):
|
||||||
|
return case(
|
||||||
|
[
|
||||||
|
(cls.name in BAD_CIPHERS, True)
|
||||||
|
],
|
||||||
|
else_=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Policy(db.Model):
|
||||||
|
___tablename__ = 'policies'
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
name = Column(String(128), nullable=True)
|
||||||
|
ciphers = relationship('Cipher', secondary=policies_ciphers, backref='policy')
|
||||||
|
|
||||||
|
|
||||||
|
class Endpoint(db.Model):
|
||||||
|
__tablename__ = 'endpoints'
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
owner = Column(String(128))
|
||||||
|
name = Column(String(128))
|
||||||
|
dnsname = Column(String(256))
|
||||||
|
type = Column(String(128))
|
||||||
|
active = Column(Boolean, default=True)
|
||||||
|
port = Column(Integer)
|
||||||
|
date_created = Column(DateTime, PassiveDefault(func.now()), nullable=False)
|
||||||
|
policy_id = Column(Integer, ForeignKey('policy.id'))
|
||||||
|
policy = relationship('Policy', backref='endpoint')
|
||||||
|
certificate_id = Column(Integer, ForeignKey('certificates.id'))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def issues(self):
|
||||||
|
issues = []
|
||||||
|
|
||||||
|
for cipher in self.policy.ciphers:
|
||||||
|
if cipher.deprecated:
|
||||||
|
issues.append({'name': 'deprecated cipher', 'value': '{0} has been deprecated consider removing it.'.format(cipher.name)})
|
||||||
|
|
||||||
|
if self.certificate.expired:
|
||||||
|
issues.append({'name': 'expired certificate', 'value': 'There is an expired certificate attached to this endpoint consider replacing it.'})
|
||||||
|
|
||||||
|
if self.certificate.revoked:
|
||||||
|
issues.append({'name': 'revoked', 'value': 'There is a revoked certificate attached to this endpoint consider replacing it.'})
|
||||||
|
|
||||||
|
return issues
|
|
@ -0,0 +1,43 @@
|
||||||
|
"""
|
||||||
|
.. module: lemur.endpoints.schemas
|
||||||
|
:platform: unix
|
||||||
|
:copyright: (c) 2015 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
|
||||||
|
from lemur.certificates.schemas import CertificateNestedOutputSchema
|
||||||
|
|
||||||
|
|
||||||
|
class CipherNestedOutputSchema(LemurOutputSchema):
|
||||||
|
__envelope__ = False
|
||||||
|
id = fields.Integer()
|
||||||
|
deprecated = fields.Boolean()
|
||||||
|
name = fields.String()
|
||||||
|
|
||||||
|
|
||||||
|
class PolicyNestedOutputSchema(LemurOutputSchema):
|
||||||
|
__envelope__ = False
|
||||||
|
id = fields.Integer()
|
||||||
|
name = fields.String()
|
||||||
|
ciphers = fields.Nested(CipherNestedOutputSchema, many=True)
|
||||||
|
|
||||||
|
|
||||||
|
class EndpointOutputSchema(LemurOutputSchema):
|
||||||
|
id = fields.Integer()
|
||||||
|
description = fields.String()
|
||||||
|
name = fields.String()
|
||||||
|
dnsname = fields.String()
|
||||||
|
owner = fields.Email()
|
||||||
|
type = fields.String()
|
||||||
|
port = fields.Integer()
|
||||||
|
active = fields.Boolean()
|
||||||
|
certificate = fields.Nested(CertificateNestedOutputSchema)
|
||||||
|
policy = fields.Nested(PolicyNestedOutputSchema)
|
||||||
|
|
||||||
|
issues = fields.List(fields.Dict())
|
||||||
|
|
||||||
|
endpoint_output_schema = EndpointOutputSchema()
|
||||||
|
endpoints_output_schema = EndpointOutputSchema(many=True)
|
|
@ -0,0 +1,144 @@
|
||||||
|
"""
|
||||||
|
.. module: lemur.endpoints.service
|
||||||
|
:platform: Unix
|
||||||
|
:synopsis: This module contains all of the services level functions used to
|
||||||
|
administer endpoints in Lemur
|
||||||
|
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||||
|
:license: Apache, see LICENSE for more details.
|
||||||
|
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||||
|
|
||||||
|
"""
|
||||||
|
from flask import g
|
||||||
|
|
||||||
|
from lemur import database
|
||||||
|
from lemur.extensions import metrics
|
||||||
|
from lemur.endpoints.models import Endpoint, Policy, Cipher
|
||||||
|
|
||||||
|
from sqlalchemy import func
|
||||||
|
|
||||||
|
|
||||||
|
def get_all():
|
||||||
|
"""
|
||||||
|
Get all endpoints that are currently in Lemur.
|
||||||
|
|
||||||
|
:rtype : List
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
query = database.session_query(Endpoint)
|
||||||
|
return database.find_all(query, Endpoint, {}).all()
|
||||||
|
|
||||||
|
|
||||||
|
def get(endpoint_id):
|
||||||
|
"""
|
||||||
|
Retrieves an endpoint given it's ID
|
||||||
|
|
||||||
|
:param endpoint_id:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return database.get(Endpoint, endpoint_id)
|
||||||
|
|
||||||
|
|
||||||
|
def get_by_dnsname(endpoint_dnsname):
|
||||||
|
"""
|
||||||
|
Retrieves an endpoint given it's name.
|
||||||
|
|
||||||
|
:param endpoint_dnsname:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return database.get(Endpoint, endpoint_dnsname, field='dnsname')
|
||||||
|
|
||||||
|
|
||||||
|
def create(**kwargs):
|
||||||
|
"""
|
||||||
|
Creates a new endpoint.
|
||||||
|
:param kwargs:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
endpoint = Endpoint(**kwargs)
|
||||||
|
database.create(endpoint)
|
||||||
|
metrics.send('endpoint_added', 'counter', 1)
|
||||||
|
return endpoint
|
||||||
|
|
||||||
|
|
||||||
|
def get_or_create_policy(**kwargs):
|
||||||
|
policy = database.get(Policy, kwargs['name'], field='name')
|
||||||
|
|
||||||
|
if not policy:
|
||||||
|
policy = Policy(**kwargs)
|
||||||
|
database.create(policy)
|
||||||
|
|
||||||
|
return policy
|
||||||
|
|
||||||
|
|
||||||
|
def get_or_create_cipher(**kwargs):
|
||||||
|
cipher = database.get(Cipher, kwargs['name'], field='name')
|
||||||
|
|
||||||
|
if not cipher:
|
||||||
|
cipher = Cipher(**kwargs)
|
||||||
|
database.create(cipher)
|
||||||
|
|
||||||
|
return cipher
|
||||||
|
|
||||||
|
|
||||||
|
def update(endpoint_id, **kwargs):
|
||||||
|
endpoint = database.get(Endpoint, endpoint_id)
|
||||||
|
|
||||||
|
endpoint.policy = kwargs['policy']
|
||||||
|
endpoint.certificate = kwargs['certificate']
|
||||||
|
database.update(endpoint)
|
||||||
|
return endpoint
|
||||||
|
|
||||||
|
|
||||||
|
def render(args):
|
||||||
|
"""
|
||||||
|
Helper that helps us render the REST Api responses.
|
||||||
|
:param args:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
query = database.session_query(Endpoint)
|
||||||
|
filt = args.pop('filter')
|
||||||
|
|
||||||
|
if filt:
|
||||||
|
terms = filt.split(';')
|
||||||
|
if 'active' in filt: # this is really weird but strcmp seems to not work here??
|
||||||
|
query = query.filter(Endpoint.active == terms[1])
|
||||||
|
elif 'port' in filt:
|
||||||
|
if terms[1] != 'null': # ng-table adds 'null' if a number is removed
|
||||||
|
query = query.filter(Endpoint.port == terms[1])
|
||||||
|
elif 'ciphers' in filt:
|
||||||
|
query = query.filter(
|
||||||
|
Cipher.name == terms[1]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
query = database.filter(query, Endpoint, terms)
|
||||||
|
|
||||||
|
# we make sure that a user can only use an endpoint they either own are are a member of - admins can see all
|
||||||
|
if not g.current_user.is_admin:
|
||||||
|
endpoint_ids = []
|
||||||
|
for role in g.current_user.roles:
|
||||||
|
for endpoint in role.endpoints:
|
||||||
|
endpoint_ids.append(endpoint.id)
|
||||||
|
query = query.filter(Endpoint.id.in_(endpoint_ids))
|
||||||
|
|
||||||
|
return database.sort_and_page(query, Endpoint, args)
|
||||||
|
|
||||||
|
|
||||||
|
def stats(**kwargs):
|
||||||
|
"""
|
||||||
|
Helper that defines some useful statistics about endpoints.
|
||||||
|
|
||||||
|
:param kwargs:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
attr = getattr(Endpoint, kwargs.get('metric'))
|
||||||
|
query = database.db.session.query(attr, func.count(attr))
|
||||||
|
|
||||||
|
items = query.group_by(attr).all()
|
||||||
|
|
||||||
|
keys = []
|
||||||
|
values = []
|
||||||
|
for key, count in items:
|
||||||
|
keys.append(key)
|
||||||
|
values.append(count)
|
||||||
|
|
||||||
|
return {'labels': keys, 'values': values}
|
|
@ -0,0 +1,106 @@
|
||||||
|
"""
|
||||||
|
.. module: lemur.endpoints.views
|
||||||
|
:platform: Unix
|
||||||
|
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||||
|
:license: Apache, see LICENSE for more details.
|
||||||
|
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||||
|
"""
|
||||||
|
from flask import Blueprint
|
||||||
|
from flask.ext.restful import reqparse, Api
|
||||||
|
|
||||||
|
from lemur.common.utils import paginated_parser
|
||||||
|
from lemur.common.schema import validate_schema
|
||||||
|
from lemur.auth.service import AuthenticatedResource
|
||||||
|
|
||||||
|
from lemur.endpoints import service
|
||||||
|
from lemur.endpoints.schemas import endpoint_output_schema, endpoints_output_schema
|
||||||
|
|
||||||
|
|
||||||
|
mod = Blueprint('endpoints', __name__)
|
||||||
|
api = Api(mod)
|
||||||
|
|
||||||
|
|
||||||
|
class EndpointsList(AuthenticatedResource):
|
||||||
|
""" Defines the 'endpoints' endpoint """
|
||||||
|
def __init__(self):
|
||||||
|
self.reqparse = reqparse.RequestParser()
|
||||||
|
super(EndpointsList, self).__init__()
|
||||||
|
|
||||||
|
@validate_schema(None, endpoints_output_schema)
|
||||||
|
def get(self):
|
||||||
|
"""
|
||||||
|
.. http:get:: /endpoints
|
||||||
|
|
||||||
|
The current list of endpoints
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /endpoints 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
|
||||||
|
|
||||||
|
|
||||||
|
:query sortBy: field to sort on
|
||||||
|
:query sortDir: acs or desc
|
||||||
|
:query page: int default is 1
|
||||||
|
:query filter: key value pair. format is k;v
|
||||||
|
:query limit: limit number default is 10
|
||||||
|
:reqheader Authorization: OAuth token to authenticate
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 403: unauthenticated
|
||||||
|
|
||||||
|
:note: this will only show certificates that the current user is authorized to use
|
||||||
|
"""
|
||||||
|
parser = paginated_parser.copy()
|
||||||
|
args = parser.parse_args()
|
||||||
|
return service.render(args)
|
||||||
|
|
||||||
|
|
||||||
|
class Endpoints(AuthenticatedResource):
|
||||||
|
def __init__(self):
|
||||||
|
self.reqparse = reqparse.RequestParser()
|
||||||
|
super(Endpoints, self).__init__()
|
||||||
|
|
||||||
|
@validate_schema(None, endpoint_output_schema)
|
||||||
|
def get(self, endpoint_id):
|
||||||
|
"""
|
||||||
|
.. http:get:: /endpoints/1
|
||||||
|
|
||||||
|
One endpoint
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /endpoints/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
|
||||||
|
|
||||||
|
|
||||||
|
:reqheader Authorization: OAuth token to authenticate
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 403: unauthenticated
|
||||||
|
"""
|
||||||
|
return service.get(endpoint_id)
|
||||||
|
|
||||||
|
|
||||||
|
api.add_resource(EndpointsList, '/endpoints', endpoint='endpoints')
|
||||||
|
api.add_resource(Endpoints, '/endpoints/<int:endpoint_id>', endpoint='endpoint')
|
|
@ -14,7 +14,7 @@ import imp
|
||||||
import errno
|
import errno
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
|
|
||||||
from logging import Formatter
|
from logging import Formatter, StreamHandler
|
||||||
from logging.handlers import RotatingFileHandler
|
from logging.handlers import RotatingFileHandler
|
||||||
|
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
|
@ -144,6 +144,10 @@ def configure_logging(app):
|
||||||
app.logger.setLevel(app.config.get('LOG_LEVEL', 'DEBUG'))
|
app.logger.setLevel(app.config.get('LOG_LEVEL', 'DEBUG'))
|
||||||
app.logger.addHandler(handler)
|
app.logger.addHandler(handler)
|
||||||
|
|
||||||
|
stream_handler = StreamHandler()
|
||||||
|
stream_handler.setLevel(app.config.get('LOG_LEVEL'))
|
||||||
|
app.logger.addHandler(stream_handler)
|
||||||
|
|
||||||
|
|
||||||
def install_plugins(app):
|
def install_plugins(app):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -189,7 +189,8 @@ def generate_settings():
|
||||||
|
|
||||||
|
|
||||||
@manager.option('-s', '--sources', dest='labels')
|
@manager.option('-s', '--sources', dest='labels')
|
||||||
def sync(labels):
|
@manager.option('-t', '--type', dest='type')
|
||||||
|
def sync(labels, type):
|
||||||
"""
|
"""
|
||||||
Attempts to run several methods Certificate discovery. This is
|
Attempts to run several methods Certificate discovery. This is
|
||||||
run on a periodic basis and updates the Lemur datastore with the
|
run on a periodic basis and updates the Lemur datastore with the
|
||||||
|
@ -212,7 +213,7 @@ def sync(labels):
|
||||||
|
|
||||||
while not sync_lock.i_am_locking():
|
while not sync_lock.i_am_locking():
|
||||||
try:
|
try:
|
||||||
sync_lock.acquire(timeout=10) # wait up to 10 seconds
|
sync_lock.acquire(timeout=2) # wait up to 10 seconds
|
||||||
|
|
||||||
sys.stdout.write("[+] Staring to sync sources: {labels}!\n".format(labels=labels))
|
sys.stdout.write("[+] Staring to sync sources: {labels}!\n".format(labels=labels))
|
||||||
labels = labels.split(",")
|
labels = labels.split(",")
|
||||||
|
@ -220,7 +221,7 @@ def sync(labels):
|
||||||
if labels[0] == 'all':
|
if labels[0] == 'all':
|
||||||
source_sync()
|
source_sync()
|
||||||
else:
|
else:
|
||||||
source_sync(labels=labels)
|
source_sync(labels=labels, type=type)
|
||||||
|
|
||||||
sys.stdout.write(
|
sys.stdout.write(
|
||||||
"[+] Finished syncing sources. Run Time: {time}\n".format(
|
"[+] Finished syncing sources. Run Time: {time}\n".format(
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 368320d26c6c
|
||||||
|
Revises: 3307381f3b88
|
||||||
|
Create Date: 2016-05-27 13:41:47.413694
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '368320d26c6c'
|
||||||
|
down_revision = '3307381f3b88'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
import sqlalchemy_utils
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('endpoints',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('owner', sa.String(length=128), nullable=True),
|
||||||
|
sa.Column('name', sa.String(length=128), nullable=True),
|
||||||
|
sa.Column('dnsname', sa.String(length=256), nullable=True),
|
||||||
|
sa.Column('type', sa.String(length=128), nullable=True),
|
||||||
|
sa.Column('active', sa.Boolean(), nullable=True),
|
||||||
|
sa.Column('port', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('date_created', sa.DateTime(), server_default=sa.text(u'now()'), nullable=False),
|
||||||
|
sa.Column('certificate_id', sa.Integer(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['certificate_id'], ['certificates.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('policy',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('endpoint_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('name', sa.String(length=32), nullable=True),
|
||||||
|
sa.Column('ciphers', sqlalchemy_utils.types.json.JSONType(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['endpoint_id'], ['endpoints.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table('policy')
|
||||||
|
op.drop_table('endpoints')
|
||||||
|
### end Alembic commands ###
|
|
@ -63,9 +63,9 @@ roles_authorities = db.Table('roles_authorities',
|
||||||
Index('roles_authorities_ix', roles_authorities.c.authority_id, roles_authorities.c.role_id)
|
Index('roles_authorities_ix', roles_authorities.c.authority_id, roles_authorities.c.role_id)
|
||||||
|
|
||||||
roles_certificates = db.Table('roles_certificates',
|
roles_certificates = db.Table('roles_certificates',
|
||||||
Column('certificate_id', Integer, ForeignKey('certificates.id')),
|
Column('certificate_id', Integer, ForeignKey('certificates.id')),
|
||||||
Column('role_id', Integer, ForeignKey('roles.id'))
|
Column('role_id', Integer, ForeignKey('roles.id'))
|
||||||
)
|
)
|
||||||
|
|
||||||
Index('roles_certificates_ix', roles_certificates.c.certificate_id, roles_certificates.c.role_id)
|
Index('roles_certificates_ix', roles_certificates.c.certificate_id, roles_certificates.c.role_id)
|
||||||
|
|
||||||
|
@ -76,3 +76,10 @@ roles_users = db.Table('roles_users',
|
||||||
)
|
)
|
||||||
|
|
||||||
Index('roles_users_ix', roles_users.c.user_id, roles_users.c.role_id)
|
Index('roles_users_ix', roles_users.c.user_id, roles_users.c.role_id)
|
||||||
|
|
||||||
|
|
||||||
|
policies_ciphers = db.Table('policies_ciphers',
|
||||||
|
Column('cipher_id', Integer, ForeignKey('ciphers.id')),
|
||||||
|
Column('policy_id', Integer, ForeignKey('policy.id')))
|
||||||
|
|
||||||
|
Index('policies_ciphers_ix', policies_ciphers.c.cipher_id, policies_ciphers.c.policy_id)
|
||||||
|
|
|
@ -45,6 +45,7 @@ def _get_message_data(cert):
|
||||||
cert_dict['owner'] = cert.owner
|
cert_dict['owner'] = cert.owner
|
||||||
cert_dict['name'] = cert.name
|
cert_dict['name'] = cert.name
|
||||||
cert_dict['body'] = cert.body
|
cert_dict['body'] = cert.body
|
||||||
|
cert_dict['endpoints'] = [{'name': x.name, 'dnsname': x.dnsname} for x in cert.endpoints]
|
||||||
|
|
||||||
return cert_dict
|
return cert_dict
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
"""
|
||||||
|
.. module: lemur.plugins.lemur_aws.elb
|
||||||
|
:synopsis: Module contains some often used and helpful classes that
|
||||||
|
are used to deal with ELBs
|
||||||
|
|
||||||
|
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||||
|
"""
|
||||||
|
from lemur.plugins.lemur_aws.sts import sts_client
|
||||||
|
|
||||||
|
|
||||||
|
@sts_client('ec2')
|
||||||
|
def get_regions(**kwargs):
|
||||||
|
regions = kwargs['client'].describe_regions()
|
||||||
|
return [x['RegionName'] for x in regions['Regions']]
|
||||||
|
|
||||||
|
|
||||||
|
@sts_client('ec2')
|
||||||
|
def get_all_instances(**kwargs):
|
||||||
|
"""
|
||||||
|
Fetches all instance objects for a given account and region.
|
||||||
|
"""
|
||||||
|
paginator = kwargs['client'].get_paginator('describe_instances')
|
||||||
|
return paginator.paginate()
|
|
@ -5,12 +5,10 @@
|
||||||
|
|
||||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||||
"""
|
"""
|
||||||
import boto.ec2
|
|
||||||
|
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
|
|
||||||
from lemur.exceptions import InvalidListener
|
from lemur.exceptions import InvalidListener
|
||||||
from lemur.plugins.lemur_aws.sts import assume_service
|
from lemur.plugins.lemur_aws.sts import sts_client, assume_service
|
||||||
|
|
||||||
|
|
||||||
def is_valid(listener_tuple):
|
def is_valid(listener_tuple):
|
||||||
|
@ -38,41 +36,34 @@ def is_valid(listener_tuple):
|
||||||
return listener_tuple
|
return listener_tuple
|
||||||
|
|
||||||
|
|
||||||
def get_all_regions():
|
@sts_client('elb')
|
||||||
"""
|
def get_all_elbs(**kwargs):
|
||||||
Retrieves all current EC2 regions.
|
|
||||||
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
regions = []
|
|
||||||
for r in boto.ec2.regions():
|
|
||||||
regions.append(r.name)
|
|
||||||
return regions
|
|
||||||
|
|
||||||
|
|
||||||
def get_all_elbs(account_number, region):
|
|
||||||
"""
|
"""
|
||||||
Fetches all elb objects for a given account and region.
|
Fetches all elb objects for a given account and region.
|
||||||
|
|
||||||
:param account_number:
|
|
||||||
:param region:
|
|
||||||
"""
|
"""
|
||||||
marker = None
|
return kwargs['client'].describe_load_balancers()
|
||||||
elbs = []
|
|
||||||
return assume_service(account_number, 'elb', region).get_all_load_balancers()
|
|
||||||
# TODO create pull request for boto to include elb marker support
|
@sts_client('elb')
|
||||||
# while True:
|
def describe_load_balancer_policies(load_balancer_name, policy_names, **kwargs):
|
||||||
# app.logger.debug(response.__dict__)
|
"""
|
||||||
# raise Exception
|
Fetching all policies currently associated with an ELB.
|
||||||
# result = response['list_server_certificates_response']['list_server_certificates_result']
|
|
||||||
#
|
:param load_balancer_name:
|
||||||
# for elb in result['server_certificate_metadata_list']:
|
:return:
|
||||||
# elbs.append(elb)
|
"""
|
||||||
#
|
return kwargs['client'].describe_load_balancer_policies(LoadBalancerName=load_balancer_name, PolicyNames=policy_names)
|
||||||
# if result['is_truncated'] == 'true':
|
|
||||||
# marker = result['marker']
|
|
||||||
# else:
|
@sts_client('elb')
|
||||||
# return elbs
|
def describe_load_balancer_types(policies, **kwargs):
|
||||||
|
"""
|
||||||
|
Describe the policies with policy details.
|
||||||
|
|
||||||
|
:param policies:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return kwargs['client'].describe_load_balancer_policy_types(PolicyTypeNames=policies)
|
||||||
|
|
||||||
|
|
||||||
def attach_certificate(account_number, region, name, port, certificate_id):
|
def attach_certificate(account_number, region, name, port, certificate_id):
|
||||||
|
@ -89,67 +80,67 @@ def attach_certificate(account_number, region, name, port, certificate_id):
|
||||||
return assume_service(account_number, 'elb', region).set_lb_listener_SSL_certificate(name, port, certificate_id)
|
return assume_service(account_number, 'elb', region).set_lb_listener_SSL_certificate(name, port, certificate_id)
|
||||||
|
|
||||||
|
|
||||||
def create_new_listeners(account_number, region, name, listeners=None):
|
# def create_new_listeners(account_number, region, name, listeners=None):
|
||||||
"""
|
# """
|
||||||
Creates a new listener and attaches it to the ELB.
|
# Creates a new listener and attaches it to the ELB.
|
||||||
|
#
|
||||||
:param account_number:
|
# :param account_number:
|
||||||
:param region:
|
# :param region:
|
||||||
:param name:
|
# :param name:
|
||||||
:param listeners:
|
# :param listeners:
|
||||||
:return:
|
# :return:
|
||||||
"""
|
# """
|
||||||
listeners = [is_valid(x) for x in listeners]
|
# listeners = [is_valid(x) for x in listeners]
|
||||||
return assume_service(account_number, 'elb', region).create_load_balancer_listeners(name, listeners=listeners)
|
# return assume_service(account_number, 'elb', region).create_load_balancer_listeners(name, listeners=listeners)
|
||||||
|
#
|
||||||
|
#
|
||||||
def update_listeners(account_number, region, name, listeners, ports):
|
# def update_listeners(account_number, region, name, listeners, ports):
|
||||||
"""
|
# """
|
||||||
We assume that a listener with a specified port already exists. We can then
|
# We assume that a listener with a specified port already exists. We can then
|
||||||
delete the old listener on the port and create a new one in it's place.
|
# delete the old listener on the port and create a new one in it's place.
|
||||||
|
#
|
||||||
If however we are replacing a listener e.g. changing a port from 80 to 443 we need
|
# If however we are replacing a listener e.g. changing a port from 80 to 443 we need
|
||||||
to make sure we kept track of which ports we needed to delete so that we don't create
|
# to make sure we kept track of which ports we needed to delete so that we don't create
|
||||||
two listeners (one 80 and one 443)
|
# two listeners (one 80 and one 443)
|
||||||
|
#
|
||||||
:param account_number:
|
# :param account_number:
|
||||||
:param region:
|
# :param region:
|
||||||
:param name:
|
# :param name:
|
||||||
:param listeners:
|
# :param listeners:
|
||||||
:param ports:
|
# :param ports:
|
||||||
"""
|
# """
|
||||||
# you cannot update a listeners port/protocol instead we remove the only one and
|
# # you cannot update a listeners port/protocol instead we remove the only one and
|
||||||
# create a new one in it's place
|
# # create a new one in it's place
|
||||||
listeners = [is_valid(x) for x in listeners]
|
# listeners = [is_valid(x) for x in listeners]
|
||||||
|
#
|
||||||
assume_service(account_number, 'elb', region).delete_load_balancer_listeners(name, ports)
|
# assume_service(account_number, 'elb', region).delete_load_balancer_listeners(name, ports)
|
||||||
return create_new_listeners(account_number, region, name, listeners=listeners)
|
# return create_new_listeners(account_number, region, name, listeners=listeners)
|
||||||
|
#
|
||||||
|
#
|
||||||
def delete_listeners(account_number, region, name, ports):
|
# def delete_listeners(account_number, region, name, ports):
|
||||||
"""
|
# """
|
||||||
Deletes a listener from an ELB.
|
# Deletes a listener from an ELB.
|
||||||
|
#
|
||||||
:param account_number:
|
# :param account_number:
|
||||||
:param region:
|
# :param region:
|
||||||
:param name:
|
# :param name:
|
||||||
:param ports:
|
# :param ports:
|
||||||
:return:
|
# :return:
|
||||||
"""
|
# """
|
||||||
return assume_service(account_number, 'elb', region).delete_load_balancer_listeners(name, ports)
|
# return assume_service(account_number, 'elb', region).delete_load_balancer_listeners(name, ports)
|
||||||
|
#
|
||||||
|
#
|
||||||
def get_listeners(account_number, region, name):
|
# def get_listeners(account_number, region, name):
|
||||||
"""
|
# """
|
||||||
Gets the listeners configured on an elb and returns a array of tuples
|
# Gets the listeners configured on an elb and returns a array of tuples
|
||||||
|
#
|
||||||
:param account_number:
|
# :param account_number:
|
||||||
:param region:
|
# :param region:
|
||||||
:param name:
|
# :param name:
|
||||||
:return: list of tuples
|
# :return: list of tuples
|
||||||
"""
|
# """
|
||||||
|
#
|
||||||
conn = assume_service(account_number, 'elb', region)
|
# conn = assume_service(account_number, 'elb', region)
|
||||||
elbs = conn.get_all_load_balancers(load_balancer_names=[name])
|
# elbs = conn.get_all_load_balancers(load_balancer_names=[name])
|
||||||
if elbs:
|
# if elbs:
|
||||||
return elbs[0].listeners
|
# return elbs[0].listeners
|
||||||
|
|
|
@ -6,18 +6,16 @@
|
||||||
|
|
||||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||||
"""
|
"""
|
||||||
|
from flask import current_app
|
||||||
from boto.exception import BotoServerError
|
from boto.exception import BotoServerError
|
||||||
|
|
||||||
from lemur.plugins.bases import DestinationPlugin, SourcePlugin
|
from lemur.plugins.bases import DestinationPlugin, SourcePlugin
|
||||||
from lemur.plugins.lemur_aws import iam, elb
|
from lemur.plugins.lemur_aws import iam
|
||||||
|
from lemur.plugins.lemur_aws.ec2 import get_regions
|
||||||
|
from lemur.plugins.lemur_aws.elb import get_all_elbs, describe_load_balancer_policies, attach_certificate
|
||||||
from lemur.plugins import lemur_aws as aws
|
from lemur.plugins import lemur_aws as aws
|
||||||
|
|
||||||
|
|
||||||
def find_value(name, options):
|
|
||||||
for o in options:
|
|
||||||
if o['name'] == name:
|
|
||||||
return o['value']
|
|
||||||
|
|
||||||
|
|
||||||
class AWSDestinationPlugin(DestinationPlugin):
|
class AWSDestinationPlugin(DestinationPlugin):
|
||||||
title = 'AWS'
|
title = 'AWS'
|
||||||
slug = 'aws-destination'
|
slug = 'aws-destination'
|
||||||
|
@ -45,14 +43,14 @@ class AWSDestinationPlugin(DestinationPlugin):
|
||||||
def upload(self, name, body, private_key, cert_chain, options, **kwargs):
|
def upload(self, name, body, private_key, cert_chain, options, **kwargs):
|
||||||
if private_key:
|
if private_key:
|
||||||
try:
|
try:
|
||||||
iam.upload_cert(find_value('accountNumber', options), name, body, private_key, cert_chain=cert_chain)
|
iam.upload_cert(self.get_option('accountNumber', options), name, body, private_key, cert_chain=cert_chain)
|
||||||
except BotoServerError as e:
|
except BotoServerError as e:
|
||||||
if e.error_code != 'EntityAlreadyExists':
|
if e.error_code != 'EntityAlreadyExists':
|
||||||
raise Exception(e)
|
raise Exception(e)
|
||||||
|
|
||||||
e = find_value('elb', options)
|
e = self.get_option('elb', options)
|
||||||
if e:
|
if e:
|
||||||
elb.attach_certificate(kwargs['accountNumber'], ['region'], e['name'], e['port'], e['certificateId'])
|
attach_certificate(kwargs['accountNumber'], ['region'], e['name'], e['port'], e['certificateId'])
|
||||||
else:
|
else:
|
||||||
raise Exception("Unable to upload to AWS, private key is required")
|
raise Exception("Unable to upload to AWS, private key is required")
|
||||||
|
|
||||||
|
@ -60,7 +58,7 @@ class AWSDestinationPlugin(DestinationPlugin):
|
||||||
class AWSSourcePlugin(SourcePlugin):
|
class AWSSourcePlugin(SourcePlugin):
|
||||||
title = 'AWS'
|
title = 'AWS'
|
||||||
slug = 'aws-source'
|
slug = 'aws-source'
|
||||||
description = 'Discovers all SSL certificates in an AWS account'
|
description = 'Discovers all SSL certificates and ELB endpoints in an AWS account'
|
||||||
version = aws.VERSION
|
version = aws.VERSION
|
||||||
|
|
||||||
author = 'Kevin Glisson'
|
author = 'Kevin Glisson'
|
||||||
|
@ -74,11 +72,16 @@ class AWSSourcePlugin(SourcePlugin):
|
||||||
'validation': '/^[0-9]{12,12}$/',
|
'validation': '/^[0-9]{12,12}$/',
|
||||||
'helpMessage': 'Must be a valid AWS account number!',
|
'helpMessage': 'Must be a valid AWS account number!',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'name': 'regions',
|
||||||
|
'type': 'str',
|
||||||
|
'helpMessage': 'Comma separated list of regions to search in, if no region is specified we look in all regions.'
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_certificates(self, options, **kwargs):
|
def get_certificates(self, options, **kwargs):
|
||||||
certs = []
|
certs = []
|
||||||
arns = iam.get_all_server_certs(find_value('accountNumber', options))
|
arns = iam.get_all_server_certs(self.get_option('accountNumber', options))
|
||||||
for arn in arns:
|
for arn in arns:
|
||||||
cert_body, cert_chain = iam.get_cert_from_arn(arn)
|
cert_body, cert_chain = iam.get_cert_from_arn(arn)
|
||||||
cert_name = iam.get_name_from_arn(arn)
|
cert_name = iam.get_name_from_arn(arn)
|
||||||
|
@ -89,3 +92,57 @@ class AWSSourcePlugin(SourcePlugin):
|
||||||
)
|
)
|
||||||
certs.append(cert)
|
certs.append(cert)
|
||||||
return certs
|
return certs
|
||||||
|
|
||||||
|
def get_endpoints(self, options, **kwargs):
|
||||||
|
endpoints = []
|
||||||
|
account_number = self.get_option('accountNumber', options)
|
||||||
|
regions = self.get_option('regions', options)
|
||||||
|
|
||||||
|
if not regions:
|
||||||
|
regions = get_regions(account_number=account_number)
|
||||||
|
else:
|
||||||
|
regions = regions.split(',')
|
||||||
|
|
||||||
|
for region in regions:
|
||||||
|
elbs = get_all_elbs(account_number=account_number, region=region)
|
||||||
|
current_app.logger.info("Describing load balancers in {0}-{1}".format(account_number, region))
|
||||||
|
for elb in elbs['LoadBalancerDescriptions']:
|
||||||
|
for listener in elb['ListenerDescriptions']:
|
||||||
|
if not listener['Listener'].get('SSLCertificateId'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
endpoint = dict(
|
||||||
|
name=elb['LoadBalancerName'],
|
||||||
|
dnsname=elb['DNSName'],
|
||||||
|
type='elb',
|
||||||
|
port=listener['Listener']['LoadBalancerPort'],
|
||||||
|
certificate_name=iam.get_name_from_arn(listener['Listener']['SSLCertificateId'])
|
||||||
|
)
|
||||||
|
|
||||||
|
if listener['PolicyNames']:
|
||||||
|
policy = describe_load_balancer_policies(elb['LoadBalancerName'], listener['PolicyNames'], account_number=account_number, region=region)
|
||||||
|
endpoint['policy'] = format_elb_cipher_policy(policy)
|
||||||
|
|
||||||
|
endpoints.append(endpoint)
|
||||||
|
|
||||||
|
return endpoints
|
||||||
|
|
||||||
|
|
||||||
|
def format_elb_cipher_policy(policy):
|
||||||
|
"""
|
||||||
|
Attempts to format cipher policy information into a common format.
|
||||||
|
:param policy:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
ciphers = []
|
||||||
|
name = None
|
||||||
|
for descr in policy['PolicyDescriptions']:
|
||||||
|
for attr in descr['PolicyAttributeDescriptions']:
|
||||||
|
if attr['AttributeName'] == 'Reference-Security-Policy':
|
||||||
|
name = attr['AttributeValue']
|
||||||
|
continue
|
||||||
|
|
||||||
|
if attr['AttributeValue'] == 'true':
|
||||||
|
ciphers.append(attr['AttributeName'])
|
||||||
|
|
||||||
|
return dict(name=name, ciphers=ciphers)
|
||||||
|
|
|
@ -5,13 +5,16 @@
|
||||||
:license: Apache, see LICENSE for more details.
|
:license: Apache, see LICENSE for more details.
|
||||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||||
"""
|
"""
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
import boto
|
import boto
|
||||||
import boto.ec2.elb
|
import boto.ec2.elb
|
||||||
|
import boto3
|
||||||
|
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
|
|
||||||
|
|
||||||
def assume_service(account_number, service, region=None):
|
def assume_service(account_number, service, region='us-east-1'):
|
||||||
conn = boto.connect_sts()
|
conn = boto.connect_sts()
|
||||||
|
|
||||||
role = conn.assume_role('arn:aws:iam::{0}:role/{1}'.format(
|
role = conn.assume_role('arn:aws:iam::{0}:role/{1}'.format(
|
||||||
|
@ -35,3 +38,40 @@ def assume_service(account_number, service, region=None):
|
||||||
aws_access_key_id=role.credentials.access_key,
|
aws_access_key_id=role.credentials.access_key,
|
||||||
aws_secret_access_key=role.credentials.secret_key,
|
aws_secret_access_key=role.credentials.secret_key,
|
||||||
security_token=role.credentials.session_token)
|
security_token=role.credentials.session_token)
|
||||||
|
|
||||||
|
|
||||||
|
def sts_client(service, service_type='client'):
|
||||||
|
def decorator(f):
|
||||||
|
@wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
sts = boto3.client('sts')
|
||||||
|
arn = 'arn:aws:iam::{0}:role/{1}'.format(
|
||||||
|
kwargs.pop('account_number'),
|
||||||
|
current_app.config.get('LEMUR_INSTANCE_PROFILE', 'Lemur')
|
||||||
|
)
|
||||||
|
# TODO add user specific information to RoleSessionName
|
||||||
|
role = sts.assume_role(RoleArn=arn, RoleSessionName='lemur')
|
||||||
|
|
||||||
|
if service_type == 'client':
|
||||||
|
client = boto3.client(
|
||||||
|
service,
|
||||||
|
region_name=kwargs.pop('region', 'us-east-1'),
|
||||||
|
aws_access_key_id=role['Credentials']['AccessKeyId'],
|
||||||
|
aws_secret_access_key=role['Credentials']['SecretAccessKey'],
|
||||||
|
aws_session_token=role['Credentials']['SessionToken']
|
||||||
|
)
|
||||||
|
kwargs['client'] = client
|
||||||
|
elif service_type == 'resource':
|
||||||
|
resource = boto3.resource(
|
||||||
|
service,
|
||||||
|
region_name=kwargs.pop('region', 'us-east-1'),
|
||||||
|
aws_access_key_id=role['Credentials']['AccessKeyId'],
|
||||||
|
aws_secret_access_key=role['Credentials']['SecretAccessKey'],
|
||||||
|
aws_session_token=role['Credentials']['SessionToken']
|
||||||
|
)
|
||||||
|
kwargs['resource'] = resource
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
return decorated_function
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
from lemur.tests.conftest import * # noqa
|
|
@ -0,0 +1,14 @@
|
||||||
|
import boto
|
||||||
|
from moto import mock_sts, mock_elb
|
||||||
|
|
||||||
|
|
||||||
|
@mock_sts()
|
||||||
|
@mock_elb()
|
||||||
|
def test_get_all_elbs(app):
|
||||||
|
from lemur.plugins.lemur_aws.elb import get_all_elbs
|
||||||
|
conn = boto.ec2.elb.connect_to_region('us-east-1')
|
||||||
|
elbs = get_all_elbs(account_number='123456789012', region='us-east-1')
|
||||||
|
assert not elbs['LoadBalancerDescriptions']
|
||||||
|
conn.create_load_balancer('example-lb', ['us-east-1a', 'us-east-1b'], [(443, 5443, 'tcp')])
|
||||||
|
elbs = get_all_elbs(account_number='123456789012', region='us-east-1')
|
||||||
|
assert elbs['LoadBalancerDescriptions']
|
|
@ -23,7 +23,7 @@
|
||||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="left" style="font-family:Roboto-Regular,Helvetica,Arial,sans-serif;font-size:35px;color:#727272;" line-height:1.5">
|
<td align="left" style="font-family:Roboto-Regular,Helvetica,Arial,sans-serif;font-size:35px;color:#727272; line-height:1.5">
|
||||||
Lemur
|
Lemur
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -83,12 +83,15 @@
|
||||||
<tr valign="middle">
|
<tr valign="middle">
|
||||||
<td width="32px"></td>
|
<td width="32px"></td>
|
||||||
<td width="16px"></td>
|
<td width="16px"></td>
|
||||||
<td style="line-height:1.2"><span
|
<td style="line-height:1.2">
|
||||||
style="font-family:Roboto-Regular,Helvetica,Arial,sans-serif;font-size:20px;color:#202020">{{ message.name }}</span><br><span
|
<span style="font-family:Roboto-Regular,Helvetica,Arial,sans-serif;font-size:20px;color:#202020">{{ message.name }}</span>
|
||||||
style="font-family:Roboto-Regular,Helvetica,Arial,sans-serif;font-size:13px;color:#727272">{{ message.owner }}
|
<br>
|
||||||
|
<span style="font-family:Roboto-Regular,Helvetica,Arial,sans-serif;font-size:13px;color:#727272">
|
||||||
|
{{ message.endpoints | length }} Endpoints
|
||||||
|
<br>{{ message.owner }}
|
||||||
<br>{{ message.not_after | time }}
|
<br>{{ message.not_after | time }}
|
||||||
<a href="https://{{ hostname }}/#/certificates/{{ message.name }}" target="_blank">Details</a>
|
<a href="https://{{ hostname }}/#/certificates/{{ message.name }}" target="_blank">Details</a>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% if not loop.last %}
|
{% if not loop.last %}
|
||||||
|
|
|
@ -210,3 +210,14 @@ class ExtensionSchema(BaseExtensionSchema):
|
||||||
authority_key_identifier = fields.Nested(AuthorityKeyIdentifierSchema)
|
authority_key_identifier = fields.Nested(AuthorityKeyIdentifierSchema)
|
||||||
certificate_info_access = fields.Nested(CertificateInfoAccessSchema)
|
certificate_info_access = fields.Nested(CertificateInfoAccessSchema)
|
||||||
custom = fields.List(fields.Nested(CustomOIDSchema))
|
custom = fields.List(fields.Nested(CustomOIDSchema))
|
||||||
|
|
||||||
|
|
||||||
|
class EndpointNestedOutputSchema(LemurOutputSchema):
|
||||||
|
__envelope__ = False
|
||||||
|
id = fields.Integer()
|
||||||
|
description = fields.String()
|
||||||
|
name = fields.String()
|
||||||
|
dnsname = fields.String()
|
||||||
|
owner = fields.Email()
|
||||||
|
type = fields.String()
|
||||||
|
active = fields.Boolean()
|
||||||
|
|
|
@ -5,12 +5,15 @@
|
||||||
:license: Apache, see LICENSE for more details.
|
:license: Apache, see LICENSE for more details.
|
||||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||||
"""
|
"""
|
||||||
|
import datetime
|
||||||
|
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
|
|
||||||
from lemur import database
|
from lemur import database
|
||||||
from lemur.sources.models import Source
|
from lemur.sources.models import Source
|
||||||
from lemur.certificates.models import Certificate
|
from lemur.certificates.models import Certificate
|
||||||
from lemur.certificates import service as cert_service
|
from lemur.certificates import service as cert_service
|
||||||
|
from lemur.endpoints import service as endpoint_service
|
||||||
from lemur.destinations import service as destination_service
|
from lemur.destinations import service as destination_service
|
||||||
|
|
||||||
from lemur.plugins.base import plugins
|
from lemur.plugins.base import plugins
|
||||||
|
@ -37,7 +40,7 @@ def _disassociate_certs_from_source(current_certificates, found_certificates, so
|
||||||
c.sources.delete(s)
|
c.sources.delete(s)
|
||||||
|
|
||||||
|
|
||||||
def sync_create(certificate, source):
|
def certificate_create(certificate, source):
|
||||||
cert = cert_service.import_certificate(**certificate)
|
cert = cert_service.import_certificate(**certificate)
|
||||||
cert.description = "This certificate was automatically discovered by Lemur"
|
cert.description = "This certificate was automatically discovered by Lemur"
|
||||||
cert.sources.append(source)
|
cert.sources.append(source)
|
||||||
|
@ -45,7 +48,7 @@ def sync_create(certificate, source):
|
||||||
database.update(cert)
|
database.update(cert)
|
||||||
|
|
||||||
|
|
||||||
def sync_update(certificate, source):
|
def certificate_update(certificate, source):
|
||||||
for s in certificate.sources:
|
for s in certificate.sources:
|
||||||
if s.label == source.label:
|
if s.label == source.label:
|
||||||
break
|
break
|
||||||
|
@ -66,40 +69,102 @@ def sync_update_destination(certificate, source):
|
||||||
certificate.destinations.append(dest)
|
certificate.destinations.append(dest)
|
||||||
|
|
||||||
|
|
||||||
def sync(labels=None):
|
def sync_endpoints(source):
|
||||||
|
new, updated = 0, 0
|
||||||
|
current_app.logger.debug("Retrieving endpoints from {0}".format(source.label))
|
||||||
|
s = plugins.get(source.plugin_name)
|
||||||
|
|
||||||
|
try:
|
||||||
|
endpoints = s.get_endpoints(source.options)
|
||||||
|
except NotImplementedError:
|
||||||
|
current_app.logger.warning("Unable to sync endpoints for source {0} plugin has not implemented 'get_endpoints'".format(source.label))
|
||||||
|
return
|
||||||
|
|
||||||
|
for endpoint in endpoints:
|
||||||
|
exists = endpoint_service.get_by_dnsname(endpoint['dnsname'])
|
||||||
|
|
||||||
|
certificate_name = endpoint.pop('certificate_name', None)
|
||||||
|
certificate = endpoint.pop('certificate', None)
|
||||||
|
|
||||||
|
if certificate_name:
|
||||||
|
cert = cert_service.get_by_name(certificate_name)
|
||||||
|
|
||||||
|
elif certificate:
|
||||||
|
cert = cert_service.get_by_body(certificate['body'])
|
||||||
|
if not cert:
|
||||||
|
cert = cert_service.import_certificate(**certificate)
|
||||||
|
|
||||||
|
if not cert:
|
||||||
|
current_app.logger.error("Unable to find associated certificate, be sure that certificates are sync'ed before endpoints")
|
||||||
|
continue
|
||||||
|
|
||||||
|
endpoint['certificate'] = cert
|
||||||
|
|
||||||
|
policy = endpoint.pop('policy')
|
||||||
|
|
||||||
|
policy_ciphers = []
|
||||||
|
for nc in policy['ciphers']:
|
||||||
|
policy_ciphers.append(endpoint_service.get_or_create_cipher(name=nc))
|
||||||
|
|
||||||
|
policy['ciphers'] = policy_ciphers
|
||||||
|
endpoint['policy'] = endpoint_service.get_or_create_policy(**policy)
|
||||||
|
|
||||||
|
if not exists:
|
||||||
|
endpoint_service.create(**endpoint)
|
||||||
|
new += 1
|
||||||
|
|
||||||
|
else:
|
||||||
|
endpoint_service.update(exists.id, **endpoint)
|
||||||
|
updated += 1
|
||||||
|
|
||||||
|
|
||||||
|
def sync_certificates(source):
|
||||||
new, updated = 0, 0
|
new, updated = 0, 0
|
||||||
c_certificates = cert_service.get_all_certs()
|
c_certificates = cert_service.get_all_certs()
|
||||||
|
|
||||||
|
current_app.logger.debug("Retrieving certificates from {0}".format(source.label))
|
||||||
|
s = plugins.get(source.plugin_name)
|
||||||
|
certificates = s.get_certificates(source.options)
|
||||||
|
|
||||||
|
for certificate in certificates:
|
||||||
|
exists = cert_service.find_duplicates(certificate['body'])
|
||||||
|
|
||||||
|
if not exists:
|
||||||
|
certificate_create(certificate, source)
|
||||||
|
new += 1
|
||||||
|
|
||||||
|
# check to make sure that existing certificates have the current source associated with it
|
||||||
|
elif len(exists) == 1:
|
||||||
|
certificate_update(exists[0], source)
|
||||||
|
updated += 1
|
||||||
|
else:
|
||||||
|
current_app.logger.warning(
|
||||||
|
"Multiple certificates found, attempt to deduplicate the following certificates: {0}".format(
|
||||||
|
",".join([x.name for x in exists])
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# we need to try and find the absent of certificates so we can properly disassociate them when they are deleted
|
||||||
|
_disassociate_certs_from_source(c_certificates, certificates, source)
|
||||||
|
|
||||||
|
|
||||||
|
def sync(labels=None, type=None):
|
||||||
for source in database.get_all(Source, True, field='active'):
|
for source in database.get_all(Source, True, field='active'):
|
||||||
# we should be able to specify, individual sources to sync
|
# we should be able to specify, individual sources to sync
|
||||||
if labels:
|
if labels:
|
||||||
if source.label not in labels:
|
if source.label not in labels:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
current_app.logger.debug("Retrieving certificates from {0}".format(source.label))
|
if type == 'endpoints':
|
||||||
s = plugins.get(source.plugin_name)
|
sync_endpoints(source)
|
||||||
certificates = s.get_certificates(source.options)
|
elif type == 'certificates':
|
||||||
|
sync_certificates(source)
|
||||||
|
else:
|
||||||
|
sync_certificates(source)
|
||||||
|
sync_endpoints(source)
|
||||||
|
|
||||||
for certificate in certificates:
|
source.last_run = datetime.datetime.utcnow()
|
||||||
exists = cert_service.find_duplicates(certificate['body'])
|
database.update(source)
|
||||||
|
|
||||||
if not exists:
|
|
||||||
sync_create(certificate, source)
|
|
||||||
new += 1
|
|
||||||
|
|
||||||
# check to make sure that existing certificates have the current source associated with it
|
|
||||||
elif len(exists) == 1:
|
|
||||||
sync_update(exists[0], source)
|
|
||||||
updated += 1
|
|
||||||
else:
|
|
||||||
current_app.logger.warning(
|
|
||||||
"Multiple certificates found, attempt to deduplicate the following certificates: {0}".format(
|
|
||||||
",".join([x.name for x in exists])
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# we need to try and find the absent of certificates so we can properly disassociate them when they are deleted
|
|
||||||
_disassociate_certs_from_source(c_certificates, certificates, source)
|
|
||||||
|
|
||||||
|
|
||||||
def create(label, plugin_name, options, description=None):
|
def create(label, plugin_name, options, description=None):
|
||||||
|
|
|
@ -57,7 +57,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="warning" ng-if="certificate.toggle" ng-repeat-end>
|
<tr class="warning" ng-if="certificate.toggle" ng-repeat-end>
|
||||||
<td colspan="12">
|
<td colspan="12">
|
||||||
<uib-tabset justified="true" class="col-md-6">
|
<uib-tabset justified="true" class="col-md-8">
|
||||||
<uib-tab>
|
<uib-tab>
|
||||||
<uib-tab-heading>Basic Info</uib-tab-heading>
|
<uib-tab-heading>Basic Info</uib-tab-heading>
|
||||||
<ul class="list-group">
|
<ul class="list-group">
|
||||||
|
@ -114,6 +114,18 @@
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</uib-tab>
|
</uib-tab>
|
||||||
|
<uib-tab>
|
||||||
|
<uib-tab-heading>Endpoints</uib-tab-heading>
|
||||||
|
<ul class="list-group">
|
||||||
|
<li class="list-group-item" ng-repeat="endpoint in certificate.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>
|
||||||
<uib-tab-heading>Notifications</uib-tab-heading>
|
<uib-tab-heading>Notifications</uib-tab-heading>
|
||||||
<ul class="list-group">
|
<ul class="list-group">
|
||||||
|
@ -158,7 +170,7 @@
|
||||||
</ul>
|
</ul>
|
||||||
</uib-tab>
|
</uib-tab>
|
||||||
</uib-tabset>
|
</uib-tabset>
|
||||||
<uib-tabset justified="true" class="col-md-6">
|
<uib-tabset justified="true" class="col-md-4">
|
||||||
<uib-tab>
|
<uib-tab>
|
||||||
<uib-tab-heading>
|
<uib-tab-heading>
|
||||||
Chain
|
Chain
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
'use strict';
|
||||||
|
angular.module('lemur')
|
||||||
|
.service('EndpointApi', function (LemurRestangular) {
|
||||||
|
return LemurRestangular.all('endpoints');
|
||||||
|
})
|
||||||
|
.service('EndpointService', function ($location, EndpointApi) {
|
||||||
|
var EndpointService = this;
|
||||||
|
EndpointService.findEndpointsByName = function (filterValue) {
|
||||||
|
return EndpointApi.getList({'filter[label]': filterValue})
|
||||||
|
.then(function (endpoints) {
|
||||||
|
return endpoints;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
EndpointService.getCertificates = function (endpoint) {
|
||||||
|
endpoint.getList('certificates').then(function (certificates) {
|
||||||
|
endpoint.certificates = certificates;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return EndpointService;
|
||||||
|
});
|
|
@ -0,0 +1,47 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
angular.module('lemur')
|
||||||
|
|
||||||
|
.config(function config($stateProvider) {
|
||||||
|
$stateProvider.state('endpoints', {
|
||||||
|
url: '/endpoints',
|
||||||
|
templateUrl: '/angular/endpoints/view/view.tpl.html',
|
||||||
|
controller: 'EndpointsViewController'
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
.controller('EndpointsViewController', function ($q, $scope, $uibModal, EndpointApi, EndpointService, MomentService, ngTableParams) {
|
||||||
|
$scope.filter = {};
|
||||||
|
$scope.endpointsTable = 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) {
|
||||||
|
EndpointApi.getList(params.url()).then(
|
||||||
|
function (data) {
|
||||||
|
params.total(data.total);
|
||||||
|
$defer.resolve(data);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
$scope.ciphers = [
|
||||||
|
{'title': 'Protocol-SSLv2', 'id': 'Protocol-SSLv2'},
|
||||||
|
{'title': 'Protocol-SSLv3', 'id': 'Protocol-SSLv3'},
|
||||||
|
{'title': 'Protocol-TLSv1', 'id': 'Protocol-TLSv1'},
|
||||||
|
{'title': 'Protocol-TLSv1.1', 'id': 'Protocol-TLSv1.1'},
|
||||||
|
{'title': 'Protocol-TLSv1.1', 'id': 'Protocol-TLSv1.2'},
|
||||||
|
];
|
||||||
|
|
||||||
|
$scope.momentService = MomentService;
|
||||||
|
|
||||||
|
$scope.endpointService = EndpointService;
|
||||||
|
|
||||||
|
});
|
|
@ -0,0 +1,108 @@
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<h2 class="featurette-heading">Endpoints
|
||||||
|
<span class="text-muted"><small>443 or bust</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>
|
||||||
|
<div class="clearfix"></div>
|
||||||
|
</div>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table ng-table="endpointsTable" class="table table-striped" show-filter="showFilter" template-pagination="angular/pager.html" >
|
||||||
|
<tbody>
|
||||||
|
<tr ng-repeat-start="endpoint in $data track by $index">
|
||||||
|
<td data-title="''" class="centered-cell">
|
||||||
|
<i ng-if="endpoint.issues.length" class="fa fa-exclamation-triangle" uib-tooltip="Issues Found" tooltip-placement="right" style="color: #FF5154;" aria-hidden="true"></i>
|
||||||
|
<i ng-if="!endpoint.issues.length" class="fa fa-check-circle" aria-hidden="true" uib-tooltip="No issues" tooltip-placement="right" style="color: #93c54b;"></i>
|
||||||
|
</td>
|
||||||
|
<td data-title="'Name'" sortable="'name'" filter="{ 'name': 'text' }">
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<li>{{ endpoint.name }}</li>
|
||||||
|
<li><span class="text-muted">{{ endpoint.dnsname }}</span></li>
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
<td data-title="'Port'" sortable="'port'" filter="{ 'port': 'number' }" class="centered-cell">
|
||||||
|
{{ endpoint.port }}
|
||||||
|
</td>
|
||||||
|
<td data-title="'Type'" sortable="'type'" filter="{ 'type': 'text'}" class="centered-cell">
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<li><label class="label label-default text-uppercase">{{ endpoint.type }}</label></li>
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
<td data-title="''" filter="{ 'ciphers' : 'select'}" filter-data="ciphers" class="centered-cell">
|
||||||
|
<div class="btn-group-vertical pull-right">
|
||||||
|
<button ng-model="endpoint.toggle" class="btn btn-sm btn-info" uib-btn-checkbox
|
||||||
|
btn-checkbox-true="1"
|
||||||
|
btn-checkbox-false="0">More
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="warning" ng-if="endpoint.toggle" ng-repeat-end>
|
||||||
|
<td colspan="12">
|
||||||
|
<uib-tabset justified="true" class="col-md-6">
|
||||||
|
<uib-tab>
|
||||||
|
<uib-tab-heading>Certificate</uib-tab-heading>
|
||||||
|
<ul class="list-group">
|
||||||
|
<li class="list-group-item">
|
||||||
|
<strong>Name</strong>
|
||||||
|
<span class="pull-right">
|
||||||
|
{{ endpoint.certificate.name }}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item">
|
||||||
|
<strong>Not Before</strong>
|
||||||
|
<span class="pull-right" uib-tooltip="{{ endpoint.certificate.notBefore }}">
|
||||||
|
{{ momentService.createMoment(endpoint.certificate.notBefore) }}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item">
|
||||||
|
<strong>Not After</strong>
|
||||||
|
<span class="pull-right" uib-tooltip="{{ endpoint.certificate.notAfter }}">
|
||||||
|
{{ momentService.createMoment(endpoint.certificate.notAfter) }}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item">
|
||||||
|
<strong>Description</strong>
|
||||||
|
<p>{{ endpoint.certificate.description }}</p>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</uib-tab>
|
||||||
|
<uib-tab>
|
||||||
|
<uib-tab-heading>
|
||||||
|
Issues
|
||||||
|
</uib-tab-heading>
|
||||||
|
<ul class="list-group">
|
||||||
|
<li class="list-group-item" ng-repeat="issue in endpoint.issues">
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<li>{{ issue.name | titleCase }}</li>
|
||||||
|
<li><span class="text-muted">{{ issue.value }}</span></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</uib-tab>
|
||||||
|
</uib-tabset>
|
||||||
|
<uib-tabset justified="true" class="col-md-6">
|
||||||
|
<uib-tab>
|
||||||
|
<uib-tab-heading>
|
||||||
|
Ciphers/Protocols
|
||||||
|
</uib-tab-heading>
|
||||||
|
<ul class="list-group">
|
||||||
|
<li class="list-group-item" ng-repeat="cipher in endpoint.policy.ciphers">
|
||||||
|
<strong>{{ cipher.name }}</strong>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</uib-tab>
|
||||||
|
</uib-tabset>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -51,13 +51,14 @@
|
||||||
<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="authorities">Authorities</a></li>
|
<li><a ui-sref="authorities">Authorities</a></li>
|
||||||
|
<li><a ui-sref="endpoints">Endpoints</a></li>
|
||||||
<li><a ui-sref="notifications">Notifications</a></li>
|
<li><a ui-sref="notifications">Notifications</a></li>
|
||||||
<li><a ui-sref="destinations">Destinations</a></li>
|
|
||||||
<li><a ui-sref="sources">Sources</a></li>
|
|
||||||
<li></li>
|
<li></li>
|
||||||
<li class="dropdown" uib-dropdown on-toggle="toggled(open)">
|
<li class="dropdown" uib-dropdown on-toggle="toggled(open)">
|
||||||
<a href class="dropdown-toggle" uib-dropdown-toggle>Settings <span class="caret"></span></a>
|
<a href class="dropdown-toggle" uib-dropdown-toggle>Settings <span class="caret"></span></a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
|
<li><a ui-sref="destinations">Destinations</a></li>
|
||||||
|
<li><a ui-sref="sources">Sources</a></li>
|
||||||
<li><a ui-sref="roles">Roles</a></li>
|
<li><a ui-sref="roles">Roles</a></li>
|
||||||
<li><a ui-sref="users">Users</a></li>
|
<li><a ui-sref="users">Users</a></li>
|
||||||
<li><a ui-sref="domains">Domains</a></li>
|
<li><a ui-sref="domains">Domains</a></li>
|
||||||
|
|
|
@ -198,3 +198,8 @@ a {
|
||||||
stroke-width: 1.5px;
|
stroke-width: 1.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.centered-cell {
|
||||||
|
vertical-align:middle;
|
||||||
|
text-align:center;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ from datetime import date
|
||||||
|
|
||||||
from factory import Sequence, post_generation, SubFactory
|
from factory import Sequence, post_generation, SubFactory
|
||||||
from factory.alchemy import SQLAlchemyModelFactory
|
from factory.alchemy import SQLAlchemyModelFactory
|
||||||
from factory.fuzzy import FuzzyChoice, FuzzyText, FuzzyDate
|
from factory.fuzzy import FuzzyChoice, FuzzyText, FuzzyDate, FuzzyInteger
|
||||||
|
|
||||||
|
|
||||||
from lemur.database import db
|
from lemur.database import db
|
||||||
|
@ -210,3 +210,20 @@ class UserFactory(BaseFactory):
|
||||||
if extracted:
|
if extracted:
|
||||||
for authority in extracted:
|
for authority in extracted:
|
||||||
self.authorities.append(authority)
|
self.authorities.append(authority)
|
||||||
|
|
||||||
|
|
||||||
|
class PolicyFactory(BaseFactory):
|
||||||
|
"""Policy Factory."""
|
||||||
|
name = Sequence(lambda n: 'endpoint{0}'.format(n))
|
||||||
|
|
||||||
|
|
||||||
|
class EndpointFactory(BaseFactory):
|
||||||
|
"""Endpoint Factory."""
|
||||||
|
owner = 'joe@example.com'
|
||||||
|
name = Sequence(lambda n: 'endpoint{0}'.format(n))
|
||||||
|
type = FuzzyChoice(['elb'])
|
||||||
|
active = True
|
||||||
|
port = FuzzyInteger(0, high=65535)
|
||||||
|
policy = SubFactory(PolicyFactory)
|
||||||
|
certificate = SubFactory(CertificateFactory)
|
||||||
|
destination = SubFactory(DestinationFactory)
|
||||||
|
|
|
@ -0,0 +1,87 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from lemur.endpoints.views import * # noqa
|
||||||
|
|
||||||
|
|
||||||
|
from .vectors import VALID_ADMIN_HEADER_TOKEN, VALID_USER_HEADER_TOKEN
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("token,status", [
|
||||||
|
(VALID_USER_HEADER_TOKEN, 404),
|
||||||
|
(VALID_ADMIN_HEADER_TOKEN, 404),
|
||||||
|
('', 401)
|
||||||
|
])
|
||||||
|
def test_endpoint_get(client, token, status):
|
||||||
|
assert client.get(api.url_for(Endpoints, endpoint_id=1), headers=token).status_code == status
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("token,status", [
|
||||||
|
(VALID_USER_HEADER_TOKEN, 405),
|
||||||
|
(VALID_ADMIN_HEADER_TOKEN, 405),
|
||||||
|
('', 405)
|
||||||
|
])
|
||||||
|
def test_endpoint_post_(client, token, status):
|
||||||
|
assert client.post(api.url_for(Endpoints, endpoint_id=1), data={}, headers=token).status_code == status
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("token,status", [
|
||||||
|
(VALID_USER_HEADER_TOKEN, 405),
|
||||||
|
(VALID_ADMIN_HEADER_TOKEN, 405),
|
||||||
|
('', 405)
|
||||||
|
])
|
||||||
|
def test_endpoint_put(client, token, status):
|
||||||
|
assert client.put(api.url_for(Endpoints, endpoint_id=1), data={}, headers=token).status_code == status
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("token,status", [
|
||||||
|
(VALID_USER_HEADER_TOKEN, 405),
|
||||||
|
(VALID_ADMIN_HEADER_TOKEN, 405),
|
||||||
|
('', 405)
|
||||||
|
])
|
||||||
|
def test_endpoint_delete(client, token, status):
|
||||||
|
assert client.delete(api.url_for(Endpoints, endpoint_id=1), headers=token).status_code == status
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("token,status", [
|
||||||
|
(VALID_USER_HEADER_TOKEN, 405),
|
||||||
|
(VALID_ADMIN_HEADER_TOKEN, 405),
|
||||||
|
('', 405)
|
||||||
|
])
|
||||||
|
def test_endpoint_patch(client, token, status):
|
||||||
|
assert client.patch(api.url_for(Endpoints, endpoint_id=1), data={}, headers=token).status_code == status
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("token,status", [
|
||||||
|
(VALID_USER_HEADER_TOKEN, 405),
|
||||||
|
(VALID_ADMIN_HEADER_TOKEN, 405),
|
||||||
|
('', 405)
|
||||||
|
])
|
||||||
|
def test_endpoint_list_post_(client, token, status):
|
||||||
|
assert client.post(api.url_for(EndpointsList), data={}, headers=token).status_code == status
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("token,status", [
|
||||||
|
(VALID_USER_HEADER_TOKEN, 200),
|
||||||
|
(VALID_ADMIN_HEADER_TOKEN, 200),
|
||||||
|
('', 401)
|
||||||
|
])
|
||||||
|
def test_endpoint_list_get(client, token, status):
|
||||||
|
assert client.get(api.url_for(EndpointsList), headers=token).status_code == status
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("token,status", [
|
||||||
|
(VALID_USER_HEADER_TOKEN, 405),
|
||||||
|
(VALID_ADMIN_HEADER_TOKEN, 405),
|
||||||
|
('', 405)
|
||||||
|
])
|
||||||
|
def test_endpoint_list_delete(client, token, status):
|
||||||
|
assert client.delete(api.url_for(EndpointsList), headers=token).status_code == status
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("token,status", [
|
||||||
|
(VALID_USER_HEADER_TOKEN, 405),
|
||||||
|
(VALID_ADMIN_HEADER_TOKEN, 405),
|
||||||
|
('', 405)
|
||||||
|
])
|
||||||
|
def test_endpoint_list_patch(client, token, status):
|
||||||
|
assert client.patch(api.url_for(EndpointsList), data={}, headers=token).status_code == status
|
3
setup.py
3
setup.py
|
@ -47,7 +47,6 @@ install_requires = [
|
||||||
'requests==2.9.1',
|
'requests==2.9.1',
|
||||||
'psycopg2==2.6.1',
|
'psycopg2==2.6.1',
|
||||||
'arrow==0.7.0',
|
'arrow==0.7.0',
|
||||||
'boto==2.38.0', # we might make this optional
|
|
||||||
'six==1.10.0',
|
'six==1.10.0',
|
||||||
'gunicorn==19.4.1',
|
'gunicorn==19.4.1',
|
||||||
'marshmallow-sqlalchemy==0.8.0',
|
'marshmallow-sqlalchemy==0.8.0',
|
||||||
|
@ -60,6 +59,8 @@ install_requires = [
|
||||||
'lockfile==0.12.2',
|
'lockfile==0.12.2',
|
||||||
'inflection==0.3.1',
|
'inflection==0.3.1',
|
||||||
'future==0.15.2',
|
'future==0.15.2',
|
||||||
|
'boto==2.38.0', # we might make this optional
|
||||||
|
'boto3==1.3.0'
|
||||||
]
|
]
|
||||||
|
|
||||||
tests_require = [
|
tests_require = [
|
||||||
|
|
Loading…
Reference in New Issue