This commit is contained in:
kevgliss 2016-06-27 14:40:46 -07:00 committed by GitHub
parent b44a7c73d8
commit fe9703dd94
36 changed files with 1140 additions and 187 deletions

View File

@ -79,7 +79,7 @@ gulp.task('dev:styles', function () {
'bower_components/angular-loading-bar/src/loading-bar.css',
'bower_components/angular-ui-switch/angular-ui-switch.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/angular-ui-select/dist/select.css',
'lemur/static/app/styles/lemur.css'

View File

@ -24,6 +24,7 @@ from lemur.defaults.views import mod as defaults_bp
from lemur.plugins.views import mod as plugins_bp
from lemur.notifications.views import mod as notifications_bp
from lemur.sources.views import mod as sources_bp
from lemur.endpoints.views import mod as endpoints_bp
from lemur.__about__ import (
__author__, __copyright__, __email__, __license__, __summary__, __title__,
@ -47,7 +48,8 @@ LEMUR_BLUEPRINTS = (
defaults_bp,
plugins_bp,
notifications_bp,
sources_bp
sources_bp,
endpoints_bp
)

View File

@ -98,16 +98,6 @@ def create(**kwargs):
else:
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)
kwargs['authority_certificate'] = cert

View File

@ -9,8 +9,10 @@ import datetime
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.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.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
backref='replaced')
endpoints = relationship("Endpoint", backref='certificate')
def __init__(self, **kwargs):
cert = defaults.parse_certificate(kwargs['body'])
@ -104,22 +108,33 @@ class Certificate(db.Model):
for domain in defaults.domains(cert):
self.domains.append(Domain(name=domain))
@property
def is_expired(self):
if self.not_after < datetime.datetime.now():
@hybrid_property
def expired(self):
if self.not_after <= datetime.datetime.now():
return True
@property
def is_unused(self):
if self.elb_listeners.count() == 0:
@expired.expression
def expired(cls):
return case(
[
(cls.now_after <= datetime.datetime.now(), True)
],
else_=False
)
@hybrid_property
def revoked(self):
if 'revoked' == self.status:
return True
@property
def is_revoked(self):
# we might not yet know the condition of the cert
if self.status:
if 'revoked' in self.status:
return True
@revoked.expression
def revoked(cls):
return case(
[
(cls.status == 'revoked', True)
],
else_=False
)
def get_arn(self, account_number):
"""

View File

@ -10,7 +10,7 @@ from marshmallow import fields, validates_schema, post_load
from marshmallow.exceptions import ValidationError
from lemur.schemas import AssociatedAuthoritySchema, AssociatedDestinationSchema, AssociatedCertificateSchema, \
AssociatedNotificationSchema, PluginInputSchema, ExtensionSchema, AssociatedRoleSchema
AssociatedNotificationSchema, PluginInputSchema, ExtensionSchema, AssociatedRoleSchema, EndpointNestedOutputSchema
from lemur.authorities.schemas import AuthorityNestedOutputSchema
from lemur.destinations.schemas import DestinationNestedOutputSchema
@ -120,7 +120,7 @@ class CertificateOutputSchema(LemurOutputSchema):
replaces = fields.Nested(CertificateNestedOutputSchema, many=True)
authority = fields.Nested(AuthorityNestedOutputSchema)
roles = fields.Nested(RoleNestedOutputSchema, many=True)
endpoints = fields.List(fields.Dict(), missing=[])
endpoints = fields.Nested(EndpointNestedOutputSchema, many=True, missing=[])
class CertificateUploadInputSchema(CertificateSchema):

View File

@ -177,6 +177,7 @@ def upload(**kwargs):
"""
Allows for pre-made certificates to be imported into Lemur.
"""
from lemur.users import service as user_service
roles = create_certificate_roles(**kwargs)
if kwargs.get('roles'):
@ -187,10 +188,14 @@ def upload(**kwargs):
cert = Certificate(**kwargs)
cert = database.create(cert)
g.user.certificates.append(cert)
database.update(cert)
return cert
try:
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):

View File

@ -132,7 +132,10 @@ def bitstrength(cert):
:param cert:
: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):

View File

80
lemur/endpoints/models.py Normal file
View File

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

View File

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

144
lemur/endpoints/service.py Normal file
View File

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

106
lemur/endpoints/views.py Normal file
View File

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

View File

@ -14,7 +14,7 @@ import imp
import errno
import pkg_resources
from logging import Formatter
from logging import Formatter, StreamHandler
from logging.handlers import RotatingFileHandler
from flask import Flask
@ -144,6 +144,10 @@ def configure_logging(app):
app.logger.setLevel(app.config.get('LOG_LEVEL', 'DEBUG'))
app.logger.addHandler(handler)
stream_handler = StreamHandler()
stream_handler.setLevel(app.config.get('LOG_LEVEL'))
app.logger.addHandler(stream_handler)
def install_plugins(app):
"""

View File

@ -189,7 +189,8 @@ def generate_settings():
@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
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():
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))
labels = labels.split(",")
@ -220,7 +221,7 @@ def sync(labels):
if labels[0] == 'all':
source_sync()
else:
source_sync(labels=labels)
source_sync(labels=labels, type=type)
sys.stdout.write(
"[+] Finished syncing sources. Run Time: {time}\n".format(

View File

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

View File

@ -63,9 +63,9 @@ roles_authorities = db.Table('roles_authorities',
Index('roles_authorities_ix', roles_authorities.c.authority_id, roles_authorities.c.role_id)
roles_certificates = db.Table('roles_certificates',
Column('certificate_id', Integer, ForeignKey('certificates.id')),
Column('role_id', Integer, ForeignKey('roles.id'))
)
Column('certificate_id', Integer, ForeignKey('certificates.id')),
Column('role_id', Integer, ForeignKey('roles.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)
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)

View File

@ -45,6 +45,7 @@ def _get_message_data(cert):
cert_dict['owner'] = cert.owner
cert_dict['name'] = cert.name
cert_dict['body'] = cert.body
cert_dict['endpoints'] = [{'name': x.name, 'dnsname': x.dnsname} for x in cert.endpoints]
return cert_dict

View File

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

View File

@ -5,12 +5,10 @@
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
import boto.ec2
from flask import current_app
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):
@ -38,41 +36,34 @@ def is_valid(listener_tuple):
return listener_tuple
def get_all_regions():
"""
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):
@sts_client('elb')
def get_all_elbs(**kwargs):
"""
Fetches all elb objects for a given account and region.
:param account_number:
:param region:
"""
marker = None
elbs = []
return assume_service(account_number, 'elb', region).get_all_load_balancers()
# TODO create pull request for boto to include elb marker support
# while True:
# app.logger.debug(response.__dict__)
# raise Exception
# result = response['list_server_certificates_response']['list_server_certificates_result']
#
# for elb in result['server_certificate_metadata_list']:
# elbs.append(elb)
#
# if result['is_truncated'] == 'true':
# marker = result['marker']
# else:
# return elbs
return kwargs['client'].describe_load_balancers()
@sts_client('elb')
def describe_load_balancer_policies(load_balancer_name, policy_names, **kwargs):
"""
Fetching all policies currently associated with an ELB.
:param load_balancer_name:
:return:
"""
return kwargs['client'].describe_load_balancer_policies(LoadBalancerName=load_balancer_name, PolicyNames=policy_names)
@sts_client('elb')
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):
@ -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)
def create_new_listeners(account_number, region, name, listeners=None):
"""
Creates a new listener and attaches it to the ELB.
:param account_number:
:param region:
:param name:
:param listeners:
:return:
"""
listeners = [is_valid(x) for x in listeners]
return assume_service(account_number, 'elb', region).create_load_balancer_listeners(name, listeners=listeners)
def update_listeners(account_number, region, name, listeners, ports):
"""
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.
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
two listeners (one 80 and one 443)
:param account_number:
:param region:
:param name:
:param listeners:
:param ports:
"""
# you cannot update a listeners port/protocol instead we remove the only one and
# create a new one in it's place
listeners = [is_valid(x) for x in listeners]
assume_service(account_number, 'elb', region).delete_load_balancer_listeners(name, ports)
return create_new_listeners(account_number, region, name, listeners=listeners)
def delete_listeners(account_number, region, name, ports):
"""
Deletes a listener from an ELB.
:param account_number:
:param region:
:param name:
:param ports:
:return:
"""
return assume_service(account_number, 'elb', region).delete_load_balancer_listeners(name, ports)
def get_listeners(account_number, region, name):
"""
Gets the listeners configured on an elb and returns a array of tuples
:param account_number:
:param region:
:param name:
:return: list of tuples
"""
conn = assume_service(account_number, 'elb', region)
elbs = conn.get_all_load_balancers(load_balancer_names=[name])
if elbs:
return elbs[0].listeners
# def create_new_listeners(account_number, region, name, listeners=None):
# """
# Creates a new listener and attaches it to the ELB.
#
# :param account_number:
# :param region:
# :param name:
# :param listeners:
# :return:
# """
# listeners = [is_valid(x) for x in listeners]
# return assume_service(account_number, 'elb', region).create_load_balancer_listeners(name, listeners=listeners)
#
#
# def update_listeners(account_number, region, name, listeners, ports):
# """
# 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.
#
# 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
# two listeners (one 80 and one 443)
#
# :param account_number:
# :param region:
# :param name:
# :param listeners:
# :param ports:
# """
# # you cannot update a listeners port/protocol instead we remove the only one and
# # create a new one in it's place
# listeners = [is_valid(x) for x in listeners]
#
# assume_service(account_number, 'elb', region).delete_load_balancer_listeners(name, ports)
# return create_new_listeners(account_number, region, name, listeners=listeners)
#
#
# def delete_listeners(account_number, region, name, ports):
# """
# Deletes a listener from an ELB.
#
# :param account_number:
# :param region:
# :param name:
# :param ports:
# :return:
# """
# return assume_service(account_number, 'elb', region).delete_load_balancer_listeners(name, ports)
#
#
# def get_listeners(account_number, region, name):
# """
# Gets the listeners configured on an elb and returns a array of tuples
#
# :param account_number:
# :param region:
# :param name:
# :return: list of tuples
# """
#
# conn = assume_service(account_number, 'elb', region)
# elbs = conn.get_all_load_balancers(load_balancer_names=[name])
# if elbs:
# return elbs[0].listeners

View File

@ -6,18 +6,16 @@
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
from flask import current_app
from boto.exception import BotoServerError
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
def find_value(name, options):
for o in options:
if o['name'] == name:
return o['value']
class AWSDestinationPlugin(DestinationPlugin):
title = 'AWS'
slug = 'aws-destination'
@ -45,14 +43,14 @@ class AWSDestinationPlugin(DestinationPlugin):
def upload(self, name, body, private_key, cert_chain, options, **kwargs):
if private_key:
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:
if e.error_code != 'EntityAlreadyExists':
raise Exception(e)
e = find_value('elb', options)
e = self.get_option('elb', options)
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:
raise Exception("Unable to upload to AWS, private key is required")
@ -60,7 +58,7 @@ class AWSDestinationPlugin(DestinationPlugin):
class AWSSourcePlugin(SourcePlugin):
title = 'AWS'
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
author = 'Kevin Glisson'
@ -74,11 +72,16 @@ class AWSSourcePlugin(SourcePlugin):
'validation': '/^[0-9]{12,12}$/',
'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):
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:
cert_body, cert_chain = iam.get_cert_from_arn(arn)
cert_name = iam.get_name_from_arn(arn)
@ -89,3 +92,57 @@ class AWSSourcePlugin(SourcePlugin):
)
certs.append(cert)
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)

View File

@ -5,13 +5,16 @@
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
from functools import wraps
import boto
import boto.ec2.elb
import boto3
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()
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_secret_access_key=role.credentials.secret_key,
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

View File

@ -0,0 +1 @@
from lemur.tests.conftest import * # noqa

View File

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

View File

@ -23,7 +23,7 @@
<table width="100%" border="0" cellspacing="0" cellpadding="0">
<tbody>
<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
</td>
</tr>
@ -83,12 +83,15 @@
<tr valign="middle">
<td width="32px"></td>
<td width="16px"></td>
<td style="line-height:1.2"><span
style="font-family:Roboto-Regular,Helvetica,Arial,sans-serif;font-size:20px;color:#202020">{{ message.name }}</span><br><span
style="font-family:Roboto-Regular,Helvetica,Arial,sans-serif;font-size:13px;color:#727272">{{ message.owner }}
<td style="line-height:1.2">
<span style="font-family:Roboto-Regular,Helvetica,Arial,sans-serif;font-size:20px;color:#202020">{{ message.name }}</span>
<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 }}
<a href="https://{{ hostname }}/#/certificates/{{ message.name }}" target="_blank">Details</a>
</span>
</span>
</td>
</tr>
{% if not loop.last %}

View File

@ -210,3 +210,14 @@ class ExtensionSchema(BaseExtensionSchema):
authority_key_identifier = fields.Nested(AuthorityKeyIdentifierSchema)
certificate_info_access = fields.Nested(CertificateInfoAccessSchema)
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()

View File

@ -5,12 +5,15 @@
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
import datetime
from flask import current_app
from lemur import database
from lemur.sources.models import Source
from lemur.certificates.models import Certificate
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.plugins.base import plugins
@ -37,7 +40,7 @@ def _disassociate_certs_from_source(current_certificates, found_certificates, so
c.sources.delete(s)
def sync_create(certificate, source):
def certificate_create(certificate, source):
cert = cert_service.import_certificate(**certificate)
cert.description = "This certificate was automatically discovered by Lemur"
cert.sources.append(source)
@ -45,7 +48,7 @@ def sync_create(certificate, source):
database.update(cert)
def sync_update(certificate, source):
def certificate_update(certificate, source):
for s in certificate.sources:
if s.label == source.label:
break
@ -66,40 +69,102 @@ def sync_update_destination(certificate, source):
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
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'):
# we should be able to specify, individual sources to sync
if labels:
if source.label not in labels:
continue
current_app.logger.debug("Retrieving certificates from {0}".format(source.label))
s = plugins.get(source.plugin_name)
certificates = s.get_certificates(source.options)
if type == 'endpoints':
sync_endpoints(source)
elif type == 'certificates':
sync_certificates(source)
else:
sync_certificates(source)
sync_endpoints(source)
for certificate in certificates:
exists = cert_service.find_duplicates(certificate['body'])
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)
source.last_run = datetime.datetime.utcnow()
database.update(source)
def create(label, plugin_name, options, description=None):

View File

@ -57,7 +57,7 @@
</tr>
<tr class="warning" ng-if="certificate.toggle" ng-repeat-end>
<td colspan="12">
<uib-tabset justified="true" class="col-md-6">
<uib-tabset justified="true" class="col-md-8">
<uib-tab>
<uib-tab-heading>Basic Info</uib-tab-heading>
<ul class="list-group">
@ -114,6 +114,18 @@
</li>
</ul>
</uib-tab>
<uib-tab>
<uib-tab-heading>Endpoints</uib-tab-heading>
<ul class="list-group">
<li class="list-group-item" ng-repeat="endpoint in 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-heading>Notifications</uib-tab-heading>
<ul class="list-group">
@ -158,7 +170,7 @@
</ul>
</uib-tab>
</uib-tabset>
<uib-tabset justified="true" class="col-md-6">
<uib-tabset justified="true" class="col-md-4">
<uib-tab>
<uib-tab-heading>
Chain

View File

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

View File

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

View File

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

View File

@ -51,13 +51,14 @@
<li><a ui-sref="dashboard">Dashboard</a></li>
<li><a ui-sref="certificates">Certificates</a></li>
<li><a ui-sref="authorities">Authorities</a></li>
<li><a ui-sref="endpoints">Endpoints</a></li>
<li><a ui-sref="notifications">Notifications</a></li>
<li><a ui-sref="destinations">Destinations</a></li>
<li><a ui-sref="sources">Sources</a></li>
<li></li>
<li class="dropdown" uib-dropdown on-toggle="toggled(open)">
<a href class="dropdown-toggle" uib-dropdown-toggle>Settings <span class="caret"></span></a>
<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="users">Users</a></li>
<li><a ui-sref="domains">Domains</a></li>

View File

@ -198,3 +198,8 @@ a {
stroke-width: 1.5px;
}
.centered-cell {
vertical-align:middle;
text-align:center;
}

View File

@ -3,7 +3,7 @@ from datetime import date
from factory import Sequence, post_generation, SubFactory
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
@ -210,3 +210,20 @@ class UserFactory(BaseFactory):
if extracted:
for authority in extracted:
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)

View File

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

View File

@ -47,7 +47,6 @@ install_requires = [
'requests==2.9.1',
'psycopg2==2.6.1',
'arrow==0.7.0',
'boto==2.38.0', # we might make this optional
'six==1.10.0',
'gunicorn==19.4.1',
'marshmallow-sqlalchemy==0.8.0',
@ -60,6 +59,8 @@ install_requires = [
'lockfile==0.12.2',
'inflection==0.3.1',
'future==0.15.2',
'boto==2.38.0', # we might make this optional
'boto3==1.3.0'
]
tests_require = [