Refactored 'accounts' to be more general with 'destinations'

This commit is contained in:
kevgliss 2015-07-10 17:06:57 -07:00
parent b26de2b000
commit 0c7204cdb9
29 changed files with 421 additions and 708 deletions

View File

@ -15,11 +15,12 @@ from lemur.roles.views import mod as roles_bp
from lemur.auth.views import mod as auth_bp from lemur.auth.views import mod as auth_bp
from lemur.domains.views import mod as domains_bp from lemur.domains.views import mod as domains_bp
from lemur.elbs.views import mod as elbs_bp from lemur.elbs.views import mod as elbs_bp
from lemur.accounts.views import mod as accounts_bp from lemur.destinations.views import mod as destinations_bp
from lemur.authorities.views import mod as authorities_bp from lemur.authorities.views import mod as authorities_bp
from lemur.listeners.views import mod as listeners_bp from lemur.listeners.views import mod as listeners_bp
from lemur.certificates.views import mod as certificates_bp from lemur.certificates.views import mod as certificates_bp
from lemur.status.views import mod as status_bp from lemur.status.views import mod as status_bp
from lemur.plugins.views import mod as plugins_bp
LEMUR_BLUEPRINTS = ( LEMUR_BLUEPRINTS = (
users_bp, users_bp,
@ -27,11 +28,12 @@ LEMUR_BLUEPRINTS = (
auth_bp, auth_bp,
domains_bp, domains_bp,
elbs_bp, elbs_bp,
accounts_bp, destinations_bp,
authorities_bp, authorities_bp,
listeners_bp, listeners_bp,
certificates_bp, certificates_bp,
status_bp status_bp,
plugins_bp,
) )
def create_app(config=None): def create_app(config=None):

View File

@ -1,29 +0,0 @@
"""
.. module: lemur.accounts.models
: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 sqlalchemy import Column, Integer, String, Text
from sqlalchemy.orm import relationship
from lemur.database import db
class Account(db.Model):
__tablename__ = 'accounts'
id = Column(Integer, primary_key=True)
account_number = Column(String(32), unique=True)
label = Column(String(32))
notes = Column(Text())
elbs = relationship("ELB", backref='account', cascade="all, delete, delete-orphan")
def as_dict(self):
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
def serialize(self):
blob = self.as_dict()
blob['elbs'] = [x.id for x in self.elbs]
return blob

View File

@ -1,112 +0,0 @@
"""
.. module: lemur.accounts.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 lemur import database
from lemur.accounts.models import Account
from lemur.certificates.models import Certificate
def create(account_number, label=None, comments=None):
"""
Creates a new account, that can then be used as a destination for certificates.
:param account_number: AWS assigned ID
:param label: Account common name
:param comments:
:rtype : Account
:return: New account
"""
acct = Account(account_number=account_number, label=label, notes=comments)
return database.create(acct)
def update(account_id, account_number, label, comments=None):
"""
Updates an existing account.
:param account_id: Lemur assigned ID
:param account_number: AWS assigned ID
:param label: Account common name
:param comments:
:rtype : Account
:return:
"""
account = get(account_id)
account.account_number = account_number
account.label = label
account.notes = comments
return database.update(account)
def delete(account_id):
"""
Deletes an account.
:param account_id: Lemur assigned ID
"""
database.delete(get(account_id))
def get(account_id):
"""
Retrieves an account by it's lemur assigned ID.
:param account_id: Lemur assigned ID
:rtype : Account
:return:
"""
return database.get(Account, account_id)
def get_by_account_number(account_number):
"""
Retrieves an account by it's amazon assigned ID.
:rtype : Account
:param account_number: AWS assigned ID
:return:
"""
return database.get(Account, account_number, field='account_number')
def get_all():
"""
Retrieves all account currently known by Lemur.
:return:
"""
query = database.session_query(Account)
return database.find_all(query, Account, {}).all()
def render(args):
sort_by = args.pop('sort_by')
sort_dir = args.pop('sort_dir')
page = args.pop('page')
count = args.pop('count')
filt = args.pop('filter')
certificate_id = args.pop('certificate_id', None)
if certificate_id:
query = database.session_query(Account).join(Certificate, Account.certificate)
query = query.filter(Certificate.id == certificate_id)
else:
query = database.session_query(Account)
if filt:
terms = filt.split(';')
query = database.filter(query, Account, terms)
query = database.find_all(query, Account, args)
if sort_by and sort_dir:
query = database.sort(query, Account, sort_by, sort_dir)
return database.paginate(query, page, count)

View File

@ -22,7 +22,7 @@ from lemur.database import db
from lemur.domains.models import Domain from lemur.domains.models import Domain
from lemur.constants import SAN_NAMING_TEMPLATE, DEFAULT_NAMING_TEMPLATE, NONSTANDARD_NAMING_TEMPLATE from lemur.constants import SAN_NAMING_TEMPLATE, DEFAULT_NAMING_TEMPLATE, NONSTANDARD_NAMING_TEMPLATE
from lemur.models import certificate_associations, certificate_account_associations from lemur.models import certificate_associations, certificate_destination_associations
def create_name(issuer, not_before, not_after, subject, san): def create_name(issuer, not_before, not_after, subject, san):
@ -215,7 +215,7 @@ class Certificate(db.Model):
date_created = Column(DateTime, PassiveDefault(func.now()), nullable=False) date_created = Column(DateTime, PassiveDefault(func.now()), nullable=False)
user_id = Column(Integer, ForeignKey('users.id')) user_id = Column(Integer, ForeignKey('users.id'))
authority_id = Column(Integer, ForeignKey('authorities.id')) authority_id = Column(Integer, ForeignKey('authorities.id'))
accounts = relationship("Account", secondary=certificate_account_associations, backref='certificate') accounts = relationship("Destination", secondary=certificate_destination_associations, backref='certificate')
domains = relationship("Domain", secondary=certificate_associations, backref="certificate") domains = relationship("Domain", secondary=certificate_associations, backref="certificate")
elb_listeners = relationship("Listener", lazy='dynamic', backref='certificate') elb_listeners = relationship("Listener", lazy='dynamic', backref='certificate')

View File

@ -13,12 +13,10 @@ from sqlalchemy import func, or_
from flask import g, current_app from flask import g, current_app
from lemur import database from lemur import database
from lemur.common.services.aws import iam
from lemur.plugins.base import plugins from lemur.plugins.base import plugins
from lemur.certificates.models import Certificate from lemur.certificates.models import Certificate
from lemur.accounts.models import Account from lemur.destinations.models import Destination
from lemur.accounts import service as account_service
from lemur.authorities.models import Authority from lemur.authorities.models import Authority
from lemur.roles.models import Role from lemur.roles.models import Role
@ -59,28 +57,6 @@ def delete(cert_id):
database.delete(get(cert_id)) database.delete(get(cert_id))
def disassociate_aws_account(certs, account):
"""
Removes the account association from a certificate. We treat AWS as a completely
external service. Certificates are added and removed from this service but a record
of that certificate is always kept and tracked by Lemur. This allows us to migrate
certificates to different accounts with ease.
:param certs:
:param account:
"""
account_certs = Certificate.query.filter(Certificate.accounts.any(Account.id == 1)).\
filter(~Certificate.body.in_(certs)).all()
for a_cert in account_certs:
try:
a_cert.accounts.remove(account)
except Exception as e:
current_app.logger.debug("Skipping {0} account {1} is already disassociated".format(a_cert.name, account.label))
continue
database.update(a_cert)
def get_all_certs(): def get_all_certs():
""" """
Retrieves all certificates within Lemur. Retrieves all certificates within Lemur.
@ -134,7 +110,7 @@ def mint(issuer_options):
issuer_options['creator'] = g.user.email issuer_options['creator'] = g.user.email
cert_body, cert_chain = issuer.create_certificate(csr, issuer_options) cert_body, cert_chain = issuer.create_certificate(csr, issuer_options)
cert = save_cert(cert_body, private_key, cert_chain, issuer_options.get('accounts')) cert = save_cert(cert_body, private_key, cert_chain, issuer_options.get('destinations'))
cert.user = g.user cert.user = g.user
cert.authority = authority cert.authority = authority
database.update(cert) database.update(cert)
@ -154,9 +130,10 @@ def import_certificate(**kwargs):
:param kwargs: :param kwargs:
""" """
from lemur.users import service as user_service
cert = Certificate(kwargs['public_certificate']) cert = Certificate(kwargs['public_certificate'])
cert.owner = kwargs.get('owner', ) cert.owner = kwargs.get('owner', current_app.config.get('LEMUR_SECURITY_TEAM_EMAIL'))
cert.creator = kwargs.get('creator', 'Lemur') cert.creator = kwargs.get('creator', user_service.get_by_email('lemur@nobody'))
# NOTE existing certs may not follow our naming standard we will # NOTE existing certs may not follow our naming standard we will
# overwrite the generated name with the actual cert name # overwrite the generated name with the actual cert name
@ -166,31 +143,29 @@ def import_certificate(**kwargs):
if kwargs.get('user'): if kwargs.get('user'):
cert.user = kwargs.get('user') cert.user = kwargs.get('user')
if kwargs.get('account'): if kwargs.get('destination'):
cert.accounts.append(kwargs.get('account')) cert.destinations.append(kwargs.get('destination'))
cert = database.create(cert) cert = database.create(cert)
return cert return cert
def save_cert(cert_body, private_key, cert_chain, accounts): def save_cert(cert_body, private_key, cert_chain, destinations):
""" """
Determines if the certificate needs to be uploaded to AWS or other services. Determines if the certificate needs to be uploaded to AWS or other services.
:param cert_body: :param cert_body:
:param private_key: :param private_key:
:param cert_chain: :param cert_chain:
:param challenge: :param destinations:
:param csr_config:
:param accounts:
""" """
cert = Certificate(cert_body, private_key, cert_chain) cert = Certificate(cert_body, private_key, cert_chain)
# if we have an AWS accounts lets upload them
if accounts: # we should save them to any destination that is requested
for account in accounts: for destination in destinations:
account = account_service.get(account['id']) destination_plugin = plugins.get(destination['plugin']['slug'])
iam.upload_cert(account.account_number, cert, private_key, cert_chain) destination_plugin.upload(cert, private_key, cert_chain, destination['plugin']['pluginOptions'])
cert.accounts.append(account)
return cert return cert
@ -198,13 +173,11 @@ def upload(**kwargs):
""" """
Allows for pre-made certificates to be imported into Lemur. Allows for pre-made certificates to be imported into Lemur.
""" """
# save this cert the same way we save all of our certs, including uploading
# to aws if necessary
cert = save_cert( cert = save_cert(
kwargs.get('public_cert'), kwargs.get('public_cert'),
kwargs.get('private_key'), kwargs.get('private_key'),
kwargs.get('intermediate_cert'), kwargs.get('intermediate_cert'),
kwargs.get('accounts') kwargs.get('destinations')
) )
cert.owner = kwargs['owner'] cert.owner = kwargs['owner']
@ -237,7 +210,7 @@ def render(args):
query = database.session_query(Certificate) query = database.session_query(Certificate)
time_range = args.pop('time_range') time_range = args.pop('time_range')
account_id = args.pop('account_id') destination_id = args.pop('destination_id')
show = args.pop('show') show = args.pop('show')
owner = args.pop('owner') owner = args.pop('owner')
creator = args.pop('creator') # TODO we should enabling filtering by owner creator = args.pop('creator') # TODO we should enabling filtering by owner
@ -260,8 +233,8 @@ def render(args):
) )
return database.sort_and_page(query, Certificate, args) return database.sort_and_page(query, Certificate, args)
if 'account' in terms: if 'destination' in terms:
query = query.filter(Certificate.accounts.any(Account.id == terms[1])) query = query.filter(Certificate.destinations.any(Destination.id == terms[1]))
elif 'active' in filt: # this is really weird but strcmp seems to not work here?? elif 'active' in filt: # this is really weird but strcmp seems to not work here??
query = query.filter(Certificate.active == terms[1]) query = query.filter(Certificate.active == terms[1])
else: else:
@ -276,8 +249,8 @@ def render(args):
) )
) )
if account_id: if destination_id:
query = query.filter(Certificate.accounts.any(Account.id == account_id)) query = query.filter(Certificate.destinations.any(Destination.id == destination_id))
if time_range: if time_range:
to = arrow.now().replace(weeks=+time_range).format('YYYY-MM-DD') to = arrow.now().replace(weeks=+time_range).format('YYYY-MM-DD')
@ -404,8 +377,8 @@ def stats(**kwargs):
if kwargs.get('active') == 'true': if kwargs.get('active') == 'true':
query = query.filter(Certificate.elb_listeners.any()) query = query.filter(Certificate.elb_listeners.any())
if kwargs.get('account_id'): if kwargs.get('destination_id'):
query = query.filter(Certificate.accounts.any(Account.id == kwargs.get('account_id'))) query = query.filter(Certificate.destinations.any(Destination.id == kwargs.get('destination_id')))
if kwargs.get('metric') == 'not_after': if kwargs.get('metric') == 'not_after':
start = arrow.utcnow() start = arrow.utcnow()

View File

@ -7,10 +7,6 @@
to 'sync' with as many different datasources as possible to try and track to 'sync' with as many different datasources as possible to try and track
any certificate that may be in use. any certificate that may be in use.
This include querying AWS for certificates attached to ELBs, querying our own
internal CA for certificates issued. As well as some rudimentary source code
scraping that attempts to find certificates checked into source code.
These operations are typically run on a periodic basis from either the command These operations are typically run on a periodic basis from either the command
line or a cron job. line or a cron job.
@ -18,151 +14,33 @@
: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 requests
from bs4 import BeautifulSoup
from flask import current_app from flask import current_app
from lemur.users import service as user_service
from lemur.accounts import service as account_service
from lemur.certificates import service as cert_service from lemur.certificates import service as cert_service
from lemur.certificates.models import Certificate, get_name_from_arn
from lemur.common.services.aws.iam import get_all_server_certs
from lemur.common.services.aws.iam import get_cert_from_arn
from lemur.plugins.base import plugins from lemur.plugins.base import plugins
from lemur.plugins.bases.source import SourcePlugin
def aws(): def sync():
""" for plugin in plugins:
Attempts to retrieve all certificates located in known AWS accounts
:raise e:
"""
new = 0 new = 0
updated = 0 updated = 0
if isinstance(plugin, SourcePlugin):
if plugin.is_enabled():
current_app.logger.error("Retrieving certificates from {0}".format(plugin.title))
certificates = plugin.get_certificates()
# all certificates 'discovered' by lemur are tracked by the lemur for certificate in certificates:
# user exists = cert_service.find_duplicates(certificate)
user = user_service.get_by_email('lemur@nobody')
# we don't need to check regions as IAM is a global service if not exists:
for account in account_service.get_all(): cert_service.import_certificate(**certificate)
certificate_bodies = []
try:
cert_arns = get_all_server_certs(account.account_number)
except Exception as e:
current_app.logger.error("Failed to to get Certificates from '{}/{}' reason {}".format(
account.label, account.account_number, e.message)
)
raise e
current_app.logger.info("found {} certs from '{}/{}' ... ".format(
len(cert_arns), account.account_number, account.label)
)
for cert in cert_arns:
cert_body = get_cert_from_arn(cert.arn)[0]
certificate_bodies.append(cert_body)
existing = cert_service.find_duplicates(cert_body)
if not existing:
cert_service.import_certificate(
**{'owner': 'secops@netflix.com',
'creator': 'Lemur',
'name': get_name_from_arn(cert.arn),
'account': account,
'user': user,
'public_certificate': cert_body
}
)
new += 1 new += 1
elif len(existing) == 1: # we check to make sure we know about the current account for this certificate if len(exists) == 1:
for e_account in existing[0].accounts:
if e_account.account_number == account.account_number:
break
else: # we have a new account
existing[0].accounts.append(account)
updated += 1 updated += 1
else: # TODO associated cert with source
current_app.logger.error( # TODO update cert if found from different source
"Multiple certificates with the same body found, unable to correctly determine which entry to update" # TODO dissassociate source if missing
)
# make sure we remove any certs that have been removed from AWS
cert_service.disassociate_aws_account(certificate_bodies, account)
current_app.logger.info("found {} new certificates in aws {}".format(new, account.label))
def cloudca():
"""
Attempts to retrieve all certificates that are stored in CloudCA
"""
user = user_service.get_by_email('lemur@nobody')
# sync all new certificates/authorities not created through lemur
issuer = plugins.get('cloudca')
authorities = issuer.get_authorities()
total = 0
new = 1
for authority in authorities:
certs = issuer.get_cert(ca_name=authority)
for cert in certs:
total += 1
cert['user'] = user
existing = cert_service.find_duplicates(cert['public_certificate'])
if not existing:
new += 1
try:
cert_service.import_certificate(**cert)
except NameError as e:
current_app.logger.error("Cannot import certificate {0}".format(cert))
current_app.logger.debug("Found {0} total certificates in cloudca".format(total))
current_app.logger.debug("Found {0} new certificates in cloudca".format(new))
def source():
"""
Attempts to track certificates that are stored in Source Code
"""
new = 0
keywords = ['"--- Begin Certificate ---"']
endpoint = current_app.config.get('LEMUR_SOURCE_SEARCH')
maxresults = 25000
current_app.logger.info("Searching {0} for new certificates".format(endpoint))
for keyword in keywords:
current_app.logger.info("Looking for keyword: {0}".format(keyword))
url = "{}/source/s?n={}&start=1&sort=relevancy&q={}&project=github%2Cperforce%2Cstash".format(endpoint, maxresults, keyword)
current_app.logger.debug("Request url: {0}".format(url))
r = requests.get(url, timeout=20)
if r.status_code != 200:
current_app.logger.error("Unable to retrieve: {0} Status Code: {1}".format(url, r.status_code))
continue
soup = BeautifulSoup(r.text, "lxml")
results = soup.find_all(title='Download')
for result in results:
parts = result['href'].split('/')
path = "/".join(parts[:-1])
filename = parts[-1:][0]
r = requests.get("{0}{1}/{2}".format(endpoint, path, filename))
if r.status_code != 200:
current_app.logger.error("Unable to retrieve: {0} Status Code: {1}".format(url, r.status_code))
continue
try:
# validate we have a real certificate
cert = Certificate(r.content)
# do a lookup to see if we know about this certificate
existing = cert_service.find_duplicates(r.content)
if not existing:
current_app.logger.debug(cert.name)
cert_service.import_certificate()
new += 1
except Exception as e:
current_app.logger.debug("Could not parse the following 'certificate': {0} Reason: {1}".format(r.content, e))

View File

@ -164,7 +164,7 @@ class CertificatesList(AuthenticatedResource):
parser.add_argument('owner', type=bool, location='args') parser.add_argument('owner', type=bool, location='args')
parser.add_argument('id', type=str, location='args') parser.add_argument('id', type=str, location='args')
parser.add_argument('active', type=bool, location='args') parser.add_argument('active', type=bool, location='args')
parser.add_argument('accountId', type=int, dest="account_id", location='args') parser.add_argument('destinationId', type=int, dest="destination_id", location='args')
parser.add_argument('creator', type=str, location='args') parser.add_argument('creator', type=str, location='args')
parser.add_argument('show', type=str, location='args') parser.add_argument('show', type=str, location='args')
@ -271,7 +271,7 @@ class CertificatesList(AuthenticatedResource):
:statuscode 403: unauthenticated :statuscode 403: unauthenticated
""" """
self.reqparse.add_argument('extensions', type=dict, location='json') self.reqparse.add_argument('extensions', type=dict, location='json')
self.reqparse.add_argument('accounts', type=list, location='json') self.reqparse.add_argument('destinations', type=list, default=[], location='json')
self.reqparse.add_argument('elbs', type=list, location='json') self.reqparse.add_argument('elbs', type=list, location='json')
self.reqparse.add_argument('owner', type=str, location='json') self.reqparse.add_argument('owner', type=str, location='json')
self.reqparse.add_argument('validityStart', type=str, location='json') # parse date self.reqparse.add_argument('validityStart', type=str, location='json') # parse date
@ -330,7 +330,7 @@ class CertificatesUpload(AuthenticatedResource):
"publicCert": "---Begin Public...", "publicCert": "---Begin Public...",
"intermediateCert": "---Begin Public...", "intermediateCert": "---Begin Public...",
"privateKey": "---Begin Private..." "privateKey": "---Begin Private..."
"accounts": [] "destinations": []
} }
**Example response**: **Example response**:
@ -364,19 +364,19 @@ class CertificatesUpload(AuthenticatedResource):
:arg publicCert: valid PEM public key for certificate :arg publicCert: valid PEM public key for certificate
:arg intermediateCert valid PEM intermediate key for certificate :arg intermediateCert valid PEM intermediate key for certificate
:arg privateKey: valid PEM private key for certificate :arg privateKey: valid PEM private key for certificate
:arg accounts: list of aws accounts to upload the certificate to :arg destinations: list of aws destinations to upload the certificate to
:reqheader Authorization: OAuth token to authenticate :reqheader Authorization: OAuth token to authenticate
:statuscode 403: unauthenticated :statuscode 403: unauthenticated
:statuscode 200: no error :statuscode 200: no error
""" """
self.reqparse.add_argument('owner', type=str, required=True, location='json') self.reqparse.add_argument('owner', type=str, required=True, location='json')
self.reqparse.add_argument('publicCert', type=pem_str, required=True, dest='public_cert', location='json') self.reqparse.add_argument('publicCert', type=pem_str, required=True, dest='public_cert', location='json')
self.reqparse.add_argument('accounts', type=list, dest='accounts', location='json') self.reqparse.add_argument('destinations', type=list, default=[], dest='destinations', location='json')
self.reqparse.add_argument('intermediateCert', type=pem_str, dest='intermediate_cert', location='json') self.reqparse.add_argument('intermediateCert', type=pem_str, dest='intermediate_cert', location='json')
self.reqparse.add_argument('privateKey', type=private_key_str, dest='private_key', location='json') self.reqparse.add_argument('privateKey', type=private_key_str, dest='private_key', location='json')
args = self.reqparse.parse_args() args = self.reqparse.parse_args()
if args.get('accounts'): if args.get('destinations'):
if args.get('private_key'): if args.get('private_key'):
return service.upload(**args) return service.upload(**args)
else: else:
@ -393,7 +393,7 @@ class CertificatesStats(AuthenticatedResource):
def get(self): def get(self):
self.reqparse.add_argument('metric', type=str, location='args') self.reqparse.add_argument('metric', type=str, location='args')
self.reqparse.add_argument('range', default=32, type=int, location='args') self.reqparse.add_argument('range', default=32, type=int, location='args')
self.reqparse.add_argument('accountId', dest='account_id', location='args') self.reqparse.add_argument('destinationId', dest='destination_id', location='args')
self.reqparse.add_argument('active', type=str, default='true', location='args') self.reqparse.add_argument('active', type=str, default='true', location='args')
args = self.reqparse.parse_args() args = self.reqparse.parse_args()

View File

@ -0,0 +1,28 @@
"""
.. module: lemur.destinations.models
: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>
"""
import copy
from sqlalchemy import Column, Integer, String, Text
from sqlalchemy_utils import JSONType
from lemur.database import db
from lemur.plugins.base import plugins
class Destination(db.Model):
__tablename__ = 'destinations'
id = Column(Integer, primary_key=True)
label = Column(String(32))
options = Column(JSONType)
description = Column(Text())
plugin_name = Column(String(32))
@property
def plugin(self):
p = plugins.get(self.plugin_name)
c = copy.deepcopy(p)
c.options = self.options
return c

View File

@ -0,0 +1,110 @@
"""
.. module: lemur.destinations.service
: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 lemur import database
from lemur.destinations.models import Destination
from lemur.certificates.models import Certificate
def create(label, plugin_name, options, description=None):
"""
Creates a new destination, that can then be used as a destination for certificates.
:param label: Destination common name
:param description:
:rtype : Destination
:return: New destination
"""
destination = Destination(label=label, options=options, plugin_name=plugin_name, description=description)
return database.create(destination)
def update(destination_id, label, options, description):
"""
Updates an existing destination.
:param destination_id: Lemur assigned ID
:param destination_number: AWS assigned ID
:param label: Destination common name
:param comments:
:rtype : Destination
:return:
"""
destination = get(destination_id)
destination.label = label
description.options = options
destination.description = description
return database.update(destination)
def delete(destination_id):
"""
Deletes an destination.
:param destination_id: Lemur assigned ID
"""
database.delete(get(destination_id))
def get(destination_id):
"""
Retrieves an destination by it's lemur assigned ID.
:param destination_id: Lemur assigned ID
:rtype : Destination
:return:
"""
return database.get(Destination, destination_id)
def get_by_label(label):
"""
Retrieves a destination by it's label
:param label:
:return:
"""
return database.get(Destination, label, field='label')
def get_all():
"""
Retrieves all destination currently known by Lemur.
:return:
"""
query = database.session_query(Destination)
return database.find_all(query, Destination, {}).all()
def render(args):
sort_by = args.pop('sort_by')
sort_dir = args.pop('sort_dir')
page = args.pop('page')
count = args.pop('count')
filt = args.pop('filter')
certificate_id = args.pop('certificate_id', None)
if certificate_id:
query = database.session_query(Destination).join(Certificate, Destination.certificate)
query = query.filter(Certificate.id == certificate_id)
else:
query = database.session_query(Destination)
if filt:
terms = filt.split(';')
query = database.filter(query, Destination, terms)
query = database.find_all(query, Destination, args)
if sort_by and sort_dir:
query = database.sort(query, Destination, sort_by, sort_dir)
return database.paginate(query, page, count)

View File

@ -1,5 +1,5 @@
""" """
.. module: lemur.accounts.views .. module: lemur.destinations.views
:platform: Unix :platform: Unix
:synopsis: This module contains all of the accounts view code. :synopsis: This module contains all of the accounts view code.
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
@ -8,35 +8,36 @@
""" """
from flask import Blueprint from flask import Blueprint
from flask.ext.restful import Api, reqparse, fields from flask.ext.restful import Api, reqparse, fields
from lemur.accounts import service from lemur.destinations import service
from lemur.auth.service import AuthenticatedResource from lemur.auth.service import AuthenticatedResource
from lemur.auth.permissions import admin_permission from lemur.auth.permissions import admin_permission
from lemur.common.utils import paginated_parser, marshal_items from lemur.common.utils import paginated_parser, marshal_items
from lemur.plugins.views import FIELDS as PLUGIN_FIELDS
mod = Blueprint('accounts', __name__) mod = Blueprint('destinations', __name__)
api = Api(mod) api = Api(mod)
FIELDS = { FIELDS = {
'accountNumber': fields.Integer(attribute='account_number'), 'description': fields.String,
'plugin': fields.Nested(PLUGIN_FIELDS, attribute='plugin'),
'label': fields.String, 'label': fields.String,
'comments': fields.String(attribute='notes'),
'id': fields.Integer, 'id': fields.Integer,
} }
class AccountsList(AuthenticatedResource): class DestinationsList(AuthenticatedResource):
""" Defines the 'accounts' endpoint """ """ Defines the 'destinations' endpoint """
def __init__(self): def __init__(self):
self.reqparse = reqparse.RequestParser() self.reqparse = reqparse.RequestParser()
super(AccountsList, self).__init__() super(DestinationsList, self).__init__()
@marshal_items(FIELDS) @marshal_items(FIELDS)
def get(self): def get(self):
""" """
.. http:get:: /accounts .. http:get:: /destinations
The current account list The current account list
@ -44,7 +45,7 @@ class AccountsList(AuthenticatedResource):
.. sourcecode:: http .. sourcecode:: http
GET /accounts HTTP/1.1 GET /destinations HTTP/1.1
Host: example.com Host: example.com
Accept: application/json, text/javascript Accept: application/json, text/javascript
@ -90,7 +91,7 @@ class AccountsList(AuthenticatedResource):
@marshal_items(FIELDS) @marshal_items(FIELDS)
def post(self): def post(self):
""" """
.. http:post:: /accounts .. http:post:: /destinations
Creates a new account Creates a new account
@ -98,7 +99,7 @@ class AccountsList(AuthenticatedResource):
.. sourcecode:: http .. sourcecode:: http
POST /accounts HTTP/1.1 POST /destinations HTTP/1.1
Host: example.com Host: example.com
Accept: application/json, text/javascript Accept: application/json, text/javascript
@ -129,23 +130,23 @@ class AccountsList(AuthenticatedResource):
:reqheader Authorization: OAuth token to authenticate :reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error :statuscode 200: no error
""" """
self.reqparse.add_argument('accountNumber', type=int, dest="account_number", location='json', required=True)
self.reqparse.add_argument('label', type=str, location='json', required=True) self.reqparse.add_argument('label', type=str, location='json', required=True)
self.reqparse.add_argument('comments', type=str, location='json') self.reqparse.add_argument('plugin', type=dict, location='json', required=True)
self.reqparse.add_argument('description', type=str, location='json')
args = self.reqparse.parse_args() args = self.reqparse.parse_args()
return service.create(args['account_number'], args['label'], args['comments']) return service.create(args['label'], args['plugin']['slug'], args['plugin']['pluginOptions'], args['description'])
class Accounts(AuthenticatedResource): class Destinations(AuthenticatedResource):
def __init__(self): def __init__(self):
self.reqparse = reqparse.RequestParser() self.reqparse = reqparse.RequestParser()
super(Accounts, self).__init__() super(Destinations, self).__init__()
@marshal_items(FIELDS) @marshal_items(FIELDS)
def get(self, account_id): def get(self, destination_id):
""" """
.. http:get:: /accounts/1 .. http:get:: /destinations/1
Get a specific account Get a specific account
@ -153,7 +154,7 @@ class Accounts(AuthenticatedResource):
.. sourcecode:: http .. sourcecode:: http
GET /accounts/1 HTTP/1.1 GET /destinations/1 HTTP/1.1
Host: example.com Host: example.com
Accept: application/json, text/javascript Accept: application/json, text/javascript
@ -175,13 +176,13 @@ class Accounts(AuthenticatedResource):
:reqheader Authorization: OAuth token to authenticate :reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error :statuscode 200: no error
""" """
return service.get(account_id) return service.get(destination_id)
@admin_permission.require(http_exception=403) @admin_permission.require(http_exception=403)
@marshal_items(FIELDS) @marshal_items(FIELDS)
def put(self, account_id): def put(self, destination_id):
""" """
.. http:put:: /accounts/1 .. http:put:: /destinations/1
Updates an account Updates an account
@ -189,15 +190,10 @@ class Accounts(AuthenticatedResource):
.. sourcecode:: http .. sourcecode:: http
POST /accounts/1 HTTP/1.1 POST /destinations/1 HTTP/1.1
Host: example.com Host: example.com
Accept: application/json, text/javascript Accept: application/json, text/javascript
{
"accountNumber": 11111111111,
"label": "labelChanged,
"comments": "this is a thing"
}
**Example response**: **Example response**:
@ -220,29 +216,29 @@ class Accounts(AuthenticatedResource):
:reqheader Authorization: OAuth token to authenticate :reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error :statuscode 200: no error
""" """
self.reqparse.add_argument('accountNumber', type=int, dest="account_number", location='json', required=True)
self.reqparse.add_argument('label', type=str, location='json', required=True) self.reqparse.add_argument('label', type=str, location='json', required=True)
self.reqparse.add_argument('comments', type=str, location='json') self.reqparse.add_argument('pluginOptions', type=dict, location='json', required=True)
self.reqparse.add_argument('description', type=str, location='json')
args = self.reqparse.parse_args() args = self.reqparse.parse_args()
return service.update(account_id, args['account_number'], args['label'], args['comments']) return service.update(destination_id, args['label'], args['options'], args['description'])
@admin_permission.require(http_exception=403) @admin_permission.require(http_exception=403)
def delete(self, account_id): def delete(self, destination_id):
service.delete(account_id) service.delete(destination_id)
return {'result': True} return {'result': True}
class CertificateAccounts(AuthenticatedResource): class CertificateDestinations(AuthenticatedResource):
""" Defines the 'certificate/<int:certificate_id/accounts'' endpoint """ """ Defines the 'certificate/<int:certificate_id/destinations'' endpoint """
def __init__(self): def __init__(self):
super(CertificateAccounts, self).__init__() super(CertificateDestinations, self).__init__()
@marshal_items(FIELDS) @marshal_items(FIELDS)
def get(self, certificate_id): def get(self, certificate_id):
""" """
.. http:get:: /certificates/1/accounts .. http:get:: /certificates/1/destinations
The current account list for a given certificates The current account list for a given certificates
@ -250,7 +246,7 @@ class CertificateAccounts(AuthenticatedResource):
.. sourcecode:: http .. sourcecode:: http
GET /certificates/1/accounts HTTP/1.1 GET /certificates/1/destinations HTTP/1.1
Host: example.com Host: example.com
Accept: application/json, text/javascript Accept: application/json, text/javascript
@ -262,24 +258,6 @@ class CertificateAccounts(AuthenticatedResource):
Vary: Accept Vary: Accept
Content-Type: text/javascript Content-Type: text/javascript
{
"items": [
{
"id": 2,
"accountNumber": 222222222,
"label": "account2",
"comments": "this is a thing"
},
{
"id": 1,
"accountNumber": 11111111111,
"label": "account1",
"comments": "this is a thing"
},
]
"total": 2
}
:query sortBy: field to sort on :query sortBy: field to sort on
:query sortDir: acs or desc :query sortDir: acs or desc
:query page: int. default is 1 :query page: int. default is 1
@ -294,7 +272,7 @@ class CertificateAccounts(AuthenticatedResource):
return service.render(args) return service.render(args)
api.add_resource(AccountsList, '/accounts', endpoint='accounts') api.add_resource(DestinationsList, '/destinations', endpoint='destinations')
api.add_resource(Accounts, '/accounts/<int:account_id>', endpoint='account') api.add_resource(Destinations, '/destinations/<int:destination_id>', endpoint='account')
api.add_resource(CertificateAccounts, '/certificates/<int:certificate_id>/accounts', endpoint='certificateAccounts') api.add_resource(CertificateDestinations, '/certificates/<int:certificate_id>/destinations', endpoint='certificateDestinations')

View File

@ -16,7 +16,7 @@ from lemur.listeners.models import Listener
class ELB(db.Model): class ELB(db.Model):
__tablename__ = 'elbs' __tablename__ = 'elbs'
id = Column(BigInteger, primary_key=True) id = Column(BigInteger, primary_key=True)
account_id = Column(BigInteger, ForeignKey("accounts.id"), index=True) #account_id = Column(BigInteger, ForeignKey("accounts.id"), index=True)
region = Column(String(32)) region = Column(String(32))
name = Column(String(128)) name = Column(String(128))
vpc_id = Column(String(128)) vpc_id = Column(String(128))

View File

@ -12,9 +12,9 @@
""" """
from flask import current_app from flask import current_app
from lemur.accounts import service as account_service #from lemur.accounts import service as account_service
from lemur.elbs import service as elb_service from lemur.elbs import service as elb_service
from lemur.common.services.aws.elb import get_all_elbs, get_all_regions #from lemur.common.services.aws.elb import get_all_elbs, get_all_regions
def create_new(known, aws, account): def create_new(known, aws, account):

View File

@ -18,7 +18,7 @@ from lemur.listeners.models import Listener
from lemur.elbs import service as elb_service from lemur.elbs import service as elb_service
from lemur.certificates import service as certificate_service from lemur.certificates import service as certificate_service
from lemur.common.services.aws.elb import update_listeners, create_new_listeners, delete_listeners #from lemur.common.services.aws.elb import update_listeners, create_new_listeners, delete_listeners
def verify_attachment(certificate_id, elb_account_number): def verify_attachment(certificate_id, elb_account_number):

View File

@ -13,9 +13,11 @@ from flask_script.commands import ShowUrls, Clean, Server
from lemur import database from lemur import database
from lemur.users import service as user_service from lemur.users import service as user_service
from lemur.roles import service as role_service from lemur.roles import service as role_service
from lemur.accounts import service as account_service from lemur.destinations import service as destination_service
from lemur.certificates import service as cert_service from lemur.certificates import service as cert_service
from lemur.plugins.base import plugins
from lemur.certificates.verify import verify_string from lemur.certificates.verify import verify_string
from lemur.certificates import sync from lemur.certificates import sync
from lemur.elbs.sync import sync_all_elbs from lemur.elbs.sync import sync_all_elbs
@ -27,7 +29,7 @@ from lemur.users.models import User
from lemur.roles.models import Role from lemur.roles.models import Role
from lemur.authorities.models import Authority from lemur.authorities.models import Authority
from lemur.certificates.models import Certificate from lemur.certificates.models import Certificate
from lemur.accounts.models import Account from lemur.destinations.models import Destination
from lemur.domains.models import Domain from lemur.domains.models import Domain
from lemur.elbs.models import ELB from lemur.elbs.models import ELB
from lemur.listeners.models import Listener from lemur.listeners.models import Listener
@ -96,12 +98,12 @@ SQLALCHEMY_DATABASE_URI = ''
## AWS ## ## AWS ##
######### #########
# Lemur will need STS assume role access to every account you want to monitor # Lemur will need STS assume role access to every destination you want to monitor
#AWS_ACCOUNT_MAPPINGS = {{ #AWS_ACCOUNT_MAPPINGS = {{
# '1111111111': 'myawsacount' # '1111111111': 'myawsacount'
#}} #}}
## This is useful if you know you only want to monitor one account ## This is useful if you know you only want to monitor one destination
#AWS_REGIONS = ['us-east-1'] #AWS_REGIONS = ['us-east-1']
#LEMUR_INSTANCE_PROFILE = 'Lemur' #LEMUR_INSTANCE_PROFILE = 'Lemur'
@ -133,6 +135,11 @@ def create():
stamp(revision='head') stamp(revision='head')
@MigrateCommand.command
def drop_all():
database.db.drop_all()
@manager.command @manager.command
def check_revoked(): def check_revoked():
""" """
@ -227,7 +234,7 @@ class Sync(Command):
class InitializeApp(Command): class InitializeApp(Command):
""" """
This command will bootstrap our database with any accounts as This command will bootstrap our database with any destinations as
specified by our config. specified by our config.
Additionally a Lemur user will be created as a default user Additionally a Lemur user will be created as a default user
@ -262,15 +269,20 @@ class InitializeApp(Command):
sys.stdout.write("[-] Default user has already been created, skipping...!\n") sys.stdout.write("[-] Default user has already been created, skipping...!\n")
if current_app.config.get('AWS_ACCOUNT_MAPPINGS'): if current_app.config.get('AWS_ACCOUNT_MAPPINGS'):
if plugins.get('aws-destination'):
for account_name, account_number in current_app.config.get('AWS_ACCOUNT_MAPPINGS').items(): for account_name, account_number in current_app.config.get('AWS_ACCOUNT_MAPPINGS').items():
account = account_service.get_by_account_number(account_number)
if not account: destination = destination_service.get_by_label(account_name)
account_service.create(account_number, label=account_name)
sys.stdout.write("[+] Added new account {0}:{1}!\n".format(account_number, account_name)) options = dict(account_number=account_number)
if not destination:
destination_service.create(account_name, 'aws-destination', options,
description="This is an auto-generated AWS destination.")
sys.stdout.write("[+] Added new destination {0}:{1}!\n".format(account_number, account_name))
else: else:
sys.stdout.write("[-] Account already exists, skipping...!\n") sys.stdout.write("[-] Account already exists, skipping...!\n")
else:
sys.stdout.write("[!] Skipping adding AWS destinations AWS plugin no available\n")
sys.stdout.write("[/] Done!\n") sys.stdout.write("[/] Done!\n")

View File

@ -18,8 +18,8 @@ certificate_associations = db.Table('certificate_associations',
Column('certificate_id', Integer, ForeignKey('certificates.id')) Column('certificate_id', Integer, ForeignKey('certificates.id'))
) )
certificate_account_associations = db.Table('certificate_account_associations', certificate_destination_associations = db.Table('certificate_destination_associations',
Column('account_id', Integer, ForeignKey('accounts.id', ondelete='cascade')), Column('destination_id', Integer, ForeignKey('destinations.id', ondelete='cascade')),
Column('certificate_id', Integer, ForeignKey('certificates.id', ondelete='cascade')) Column('certificate_id', Integer, ForeignKey('certificates.id', ondelete='cascade'))
) )

View File

@ -9,5 +9,8 @@
from lemur.plugins.base import Plugin from lemur.plugins.base import Plugin
class DestinationPlugin(Plugin): class DestinationPlugin(Plugin):
pass type = 'destination'
def upload(self):
raise NotImplemented

View File

@ -0,0 +1,5 @@
try:
VERSION = __import__('pkg_resources') \
.get_distribution(__name__).version
except Exception, e:
VERSION = 'unknown'

View File

@ -10,7 +10,7 @@ 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.common.services.aws.sts import assume_service from lemur.plugins.lemur_aws.sts import assume_service
def is_valid(listener_tuple): def is_valid(listener_tuple):

View File

@ -6,8 +6,17 @@
: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 flask import current_app from lemur.plugins.lemur_aws.sts import assume_service
from lemur.common.services.aws.sts import assume_service
def get_name_from_arn(arn):
"""
Extract the certificate name from an arn.
:param arn: IAM SSL arn
:return: name of the certificate as uploaded to AWS
"""
return arn.split("/", 1)[1]
def ssl_split(param_string): def ssl_split(param_string):

View File

@ -0,0 +1,77 @@
"""
.. module: lemur.plugins.lemur_aws.aws
: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 lemur.plugins.bases import DestinationPlugin, SourcePlugin
from lemur.plugins.lemur_aws import iam, elb
from lemur.plugins import lemur_aws as aws
def find_value(name, options):
for o in options:
if o.get(name):
return o['value']
class AWSDestinationPlugin(DestinationPlugin):
title = 'AWS'
slug = 'aws-destination'
description = 'Allow the uploading of certificates to AWS IAM'
version = aws.VERSION
author = 'Kevin Glisson'
author_url = 'https://github.com/netflix/lemur'
options = [
{
'name': 'accountNumber',
'type': 'int',
'required': True,
'validation': '/^[0-9]{12,12}$/',
'helpMessage': 'Must be a valid AWS account number!',
}
]
#'elb': {
# 'name': {'type': 'name'},
# 'region': {'type': 'str'},
# 'port': {'type': 'int'}
#}
def upload(self, cert, private_key, cert_chain, options, **kwargs):
iam.upload_cert(find_value('accountNumber', options), cert, private_key, cert_chain=cert_chain)
e = find_value('elb', options)
if e:
elb.attach_certificate(kwargs['accountNumber'], ['region'], e['name'], e['port'], e['certificateId'])
class AWSSourcePlugin(SourcePlugin):
title = 'AWS'
slug = 'aws-source'
description = 'Discovers all SSL certificates in an AWS account'
version = aws.VERSION
author = 'Kevin Glisson'
author_url = 'https://github.com/netflix/lemur'
options = {
'accountNumber': {'type': 'int'},
'pollRate': {'type': 'int', 'default': '60'}
}
def get_certificates(self, **kwargs):
certs = []
arns = elb.get_all_server_certs(kwargs['account_number'])
for arn in arns:
cert_body = iam.get_cert_from_arn(arn)
cert_name = iam.get_name_from_arn(arn)
cert = dict(
public_certificate=cert_body,
name=cert_name
)
certs.append(cert)
return certs

View File

@ -1,27 +0,0 @@
'use strict';
angular.module('lemur')
.config(function config($routeProvider) {
$routeProvider.when('/accounts/create', {
templateUrl: '/angular/accounts/account/account.tpl.html',
controller: 'AccountsCreateController'
});
$routeProvider.when('/accounts/:id/edit', {
templateUrl: '/angular/accounts/account/account.tpl.html',
controller: 'AccountsEditController'
});
})
.controller('AccountsCreateController', function ($scope, AccountService, LemurRestangular){
$scope.account = LemurRestangular.restangularizeElement(null, {}, 'accounts');
$scope.save = AccountService.create;
})
.controller('AccountsEditController', function ($scope, $routeParams, AccountService, AccountApi) {
AccountApi.get($routeParams.id).then(function (account) {
$scope.account = account;
});
$scope.save = AccountService.update;
});

View File

@ -1,45 +0,0 @@
<h2 class="featurette-heading"><span ng-show="!account.fromServer">Create</span><span ng-show="account.fromServer">Edit</span> Account <span class="text-muted"><small>next in line please
</small></span></h2>
<div class="panel panel-default">
<div class="panel-heading">
<a href="/#/accounts/" class="btn btn-danger pull-right">Cancel</a>
<div class="clearfix"></div>
</div>
<div class="panel-body">
<form name="createForm" class="form-horizontal" role="form" novalidate>
<div class="form-group"
ng-class="{'has-error': createForm.name.$invalid, 'has-success': !createForm.name.$invalid&&createForm.name.$dirty}">
<label class="control-label col-sm-2">
Name
</label>
<div class="col-sm-10">
<input name="name" ng-model="account.label" placeholder="Name" class="form-control" required/>
<p ng-show="createForm.name.$invalid && !createForm.name.$pristine" class="help-block">You must enter an account name</p>
</div>
</div>
<div class="form-group"
ng-class="{'has-error': createForm.accountNumber.$invalid, 'has-success': !createForm.accountNumber.$invalid&&createForm.accountNumber.$dirty}">
<label class="control-label col-sm-2">
Account Number
</label>
<div class="col-sm-10">
<input type="number" name="accountNumber" ng-model="account.accountNumber" placeholder="111111111111" class="form-control" ng-minlength="12" ng-maxlength="12" required/>
<p ng-show="createForm.accountNumber.$invalid && !createForm.accountNumber.$pristine" class="help-block">You must enter an account number</p>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2">
Comments
</label>
<div class="col-sm-10">
<textarea name="comments" ng-model="account.comments" placeholder="Something elegant" class="form-control" ></textarea>
</div>
</div>
</form>
</div>
<div class="panel-footer">
<button ng-click="save(account)" type="submit" ng-disabled="createForm.$invalid" class="btn btn-primary pull-right">Save</button>
<div class="clearfix"></div>
</div>
</div>

View File

@ -1,53 +0,0 @@
'use strict';
angular.module('lemur')
.service('AccountApi', function (LemurRestangular) {
return LemurRestangular.all('accounts');
})
.service('AccountService', function ($location, AccountApi, toaster) {
var AccountService = this;
AccountService.findAccountsByName = function (filterValue) {
return AccountApi.getList({'filter[label]': filterValue})
.then(function (accounts) {
return accounts;
});
};
AccountService.create = function (account) {
AccountApi.post(account).then(
function () {
toaster.pop({
type: 'success',
title: account.label,
body: 'Successfully created!'
});
$location.path('accounts');
},
function (response) {
toaster.pop({
type: 'error',
title: account.label,
body: 'Was not created! ' + response.data.message
});
});
};
AccountService.update = function (account) {
account.put().then(
function () {
toaster.pop({
type: 'success',
title: account.label,
body: 'Successfully updated!'
});
$location.path('accounts');
},
function (response) {
toaster.pop({
type: 'error',
title: account.label,
body: 'Was not updated! ' + response.data.message
});
});
};
return AccountService;
});

View File

@ -1,52 +0,0 @@
'use strict';
angular.module('lemur')
.config(function config($routeProvider) {
$routeProvider.when('/accounts', {
templateUrl: '/angular/accounts/view/view.tpl.html',
controller: 'AccountsViewController'
});
})
.controller('AccountsViewController', function ($scope, AccountApi, AccountService, ngTableParams, toaster) {
$scope.filter = {};
$scope.accountsTable = 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) {
AccountApi.getList(params.url()).then(
function (data) {
params.total(data.total);
$defer.resolve(data);
}
);
}
});
$scope.remove = function (account) {
account.remove().then(
function () {
$scope.accountsTable.reload();
},
function (response) {
toaster.pop({
type: 'error',
title: 'Opps',
body: 'I see what you did there' + response.data.message
});
}
);
};
$scope.toggleFilter = function (params) {
params.settings().$scope.show_filter = !params.settings().$scope.show_filter;
};
});

View File

@ -1,44 +0,0 @@
<div class="row">
<div class="col-md-12">
<h2 class="featurette-heading">Accounts
<span class="text-muted"><small>next in line please</small></span></h2>
<div class="panel panel-default">
<div class="panel-heading">
<div class="btn-group pull-right">
<a href="#/accounts/create" class="btn btn-primary">Create</a>
</div>
<div class="btn-group">
<button ng-click="toggleFilter(accountsTable)" class="btn btn-default">Filter</button>
</div>
<div class="clearfix"></div>
</div>
<div class="table-responsive">
<table ng-table="accountsTable" class="table table-striped" show-filter="false" template-pagination="angular/pager.html" >
<tbody>
<tr ng-repeat="account in $data track by $index">
<td data-title="'Name'" sortable="'label'" filter="{ 'label': 'text' }">
<ul class="list-unstyled">
<li>{{ account.label }}</li>
<li><span class="text-muted">{{ account.comments }}</span></li>
</ul>
</td>
<td data-title="'Account Number'" sortable="'account_number'" filter="{ 'account_number': 'text' }">
{{ account.accountNumber }}
</td>
<td data-title="''">
<div class="btn-group-vertical pull-right">
<a tooltip="Edit Account" href="#/accounts/{{ account.id }}/edit" class="btn btn-sm btn-info">
Edit
</a>
<button tooltip="Delete Account" ng-click="remove(account)" type="button" class="btn btn-sm btn-danger pull-left">
Remove
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>

View File

@ -1,15 +1,15 @@
from lemur.accounts.service import * from lemur.destinations.service import *
from lemur.accounts.views import * from lemur.destinations.views import *
from json import dumps from json import dumps
def test_crud(session): def test_crud(session):
account = create('111111', 'account1') destination = create('111111', 'destination1')
assert account.id > 0 assert destination.id > 0
account = update(account.id, 11111, 'account2') destination = update(destination.id, 11111, 'destination2')
assert account.label == 'account2' assert destination.label == 'destination2'
assert len(get_all()) == 1 assert len(get_all()) == 1
@ -17,116 +17,116 @@ def test_crud(session):
assert len(get_all()) == 0 assert len(get_all()) == 0
def test_account_get(client): def test_destination_get(client):
assert client.get(api.url_for(Accounts, account_id=1)).status_code == 401 assert client.get(api.url_for(Destinations, destination_id=1)).status_code == 401
def test_account_post(client): def test_destination_post(client):
assert client.post(api.url_for(Accounts, account_id=1), data={}).status_code == 405 assert client.post(api.url_for(Destinations, destination_id=1), data={}).status_code == 405
def test_account_put(client): def test_destination_put(client):
assert client.put(api.url_for(Accounts, account_id=1), data={}).status_code == 401 assert client.put(api.url_for(Destinations, destination_id=1), data={}).status_code == 401
def test_account_delete(client): def test_destination_delete(client):
assert client.delete(api.url_for(Accounts, account_id=1)).status_code == 401 assert client.delete(api.url_for(Destinations, destination_id=1)).status_code == 401
def test_account_patch(client): def test_destination_patch(client):
assert client.patch(api.url_for(Accounts, account_id=1), data={}).status_code == 405 assert client.patch(api.url_for(Destinations, destination_id=1), data={}).status_code == 405
VALID_USER_HEADER_TOKEN = { VALID_USER_HEADER_TOKEN = {
'Authorization': 'Basic ' + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE0MzUyMzMzNjksInN1YiI6MSwiZXhwIjoxNTIxNTQ2OTY5fQ.1qCi0Ip7mzKbjNh0tVd3_eJOrae3rNa_9MCVdA4WtQI'} 'Authorization': 'Basic ' + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE0MzUyMzMzNjksInN1YiI6MSwiZXhwIjoxNTIxNTQ2OTY5fQ.1qCi0Ip7mzKbjNh0tVd3_eJOrae3rNa_9MCVdA4WtQI'}
def test_auth_account_get(client): def test_auth_destination_get(client):
assert client.get(api.url_for(Accounts, account_id=1), headers=VALID_USER_HEADER_TOKEN).status_code == 200 assert client.get(api.url_for(Destinations, destination_id=1), headers=VALID_USER_HEADER_TOKEN).status_code == 200
def test_auth_account_post_(client): def test_auth_destination_post_(client):
assert client.post(api.url_for(Accounts, account_id=1), data={}, headers=VALID_USER_HEADER_TOKEN).status_code == 405 assert client.post(api.url_for(Destinations, destination_id=1), data={}, headers=VALID_USER_HEADER_TOKEN).status_code == 405
def test_auth_account_put(client): def test_auth_destination_put(client):
assert client.put(api.url_for(Accounts, account_id=1), data={}, headers=VALID_USER_HEADER_TOKEN).status_code == 403 assert client.put(api.url_for(Destinations, destination_id=1), data={}, headers=VALID_USER_HEADER_TOKEN).status_code == 403
def test_auth_account_delete(client): def test_auth_destination_delete(client):
assert client.delete(api.url_for(Accounts, account_id=1), headers=VALID_USER_HEADER_TOKEN).status_code == 403 assert client.delete(api.url_for(Destinations, destination_id=1), headers=VALID_USER_HEADER_TOKEN).status_code == 403
def test_auth_account_patch(client): def test_auth_destination_patch(client):
assert client.patch(api.url_for(Accounts, account_id=1), data={}, headers=VALID_USER_HEADER_TOKEN).status_code == 405 assert client.patch(api.url_for(Destinations, destination_id=1), data={}, headers=VALID_USER_HEADER_TOKEN).status_code == 405
VALID_ADMIN_HEADER_TOKEN = { VALID_ADMIN_HEADER_TOKEN = {
'Authorization': 'Basic ' + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE0MzUyNTAyMTgsInN1YiI6MiwiZXhwIjoxNTIxNTYzODE4fQ.6mbq4-Ro6K5MmuNiTJBB153RDhlM5LGJBjI7GBKkfqA'} 'Authorization': 'Basic ' + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE0MzUyNTAyMTgsInN1YiI6MiwiZXhwIjoxNTIxNTYzODE4fQ.6mbq4-Ro6K5MmuNiTJBB153RDhlM5LGJBjI7GBKkfqA'}
def test_admin_account_get(client): def test_admin_destination_get(client):
assert client.get(api.url_for(Accounts, account_id=1), headers=VALID_ADMIN_HEADER_TOKEN).status_code == 200 assert client.get(api.url_for(Destinations, destination_id=1), headers=VALID_ADMIN_HEADER_TOKEN).status_code == 200
def test_admin_account_post(client): def test_admin_destination_post(client):
assert client.post(api.url_for(Accounts, account_id=1), data={}, headers=VALID_ADMIN_HEADER_TOKEN).status_code == 405 assert client.post(api.url_for(Destinations, destination_id=1), data={}, headers=VALID_ADMIN_HEADER_TOKEN).status_code == 405
def test_admin_account_put(client): def test_admin_destination_put(client):
assert client.put(api.url_for(Accounts, account_id=1), data={}, headers=VALID_ADMIN_HEADER_TOKEN).status_code == 400 assert client.put(api.url_for(Destinations, destination_id=1), data={}, headers=VALID_ADMIN_HEADER_TOKEN).status_code == 400
def test_admin_account_delete(client): def test_admin_destination_delete(client):
assert client.delete(api.url_for(Accounts, account_id=1), headers=VALID_ADMIN_HEADER_TOKEN).status_code == 500 assert client.delete(api.url_for(Destinations, destination_id=1), headers=VALID_ADMIN_HEADER_TOKEN).status_code == 500
def test_admin_account_patch(client): def test_admin_destination_patch(client):
assert client.patch(api.url_for(Accounts, account_id=1), data={}, headers=VALID_ADMIN_HEADER_TOKEN).status_code == 405 assert client.patch(api.url_for(Destinations, destination_id=1), data={}, headers=VALID_ADMIN_HEADER_TOKEN).status_code == 405
def test_accounts_get(client): def test_destinations_get(client):
assert client.get(api.url_for(AccountsList)).status_code == 401 assert client.get(api.url_for(DestinationsList)).status_code == 401
def test_accounts_post(client): def test_destinations_post(client):
assert client.post(api.url_for(AccountsList), data={}).status_code == 401 assert client.post(api.url_for(DestinationsList), data={}).status_code == 401
def test_accounts_put(client): def test_destinations_put(client):
assert client.put(api.url_for(AccountsList), data={}).status_code == 405 assert client.put(api.url_for(DestinationsList), data={}).status_code == 405
def test_accounts_delete(client): def test_destinations_delete(client):
assert client.delete(api.url_for(AccountsList)).status_code == 405 assert client.delete(api.url_for(DestinationsList)).status_code == 405
def test_accounts_patch(client): def test_destinations_patch(client):
assert client.patch(api.url_for(AccountsList), data={}).status_code == 405 assert client.patch(api.url_for(DestinationsList), data={}).status_code == 405
def test_auth_accounts_get(client): def test_auth_destinations_get(client):
assert client.get(api.url_for(AccountsList), headers=VALID_USER_HEADER_TOKEN).status_code == 200 assert client.get(api.url_for(DestinationsList), headers=VALID_USER_HEADER_TOKEN).status_code == 200
def test_auth_accounts_post(client): def test_auth_destinations_post(client):
assert client.post(api.url_for(AccountsList), data={}, headers=VALID_USER_HEADER_TOKEN).status_code == 403 assert client.post(api.url_for(DestinationsList), data={}, headers=VALID_USER_HEADER_TOKEN).status_code == 403
def test_admin_accounts_get(client): def test_admin_destinations_get(client):
resp = client.get(api.url_for(AccountsList), headers=VALID_ADMIN_HEADER_TOKEN) resp = client.get(api.url_for(DestinationsList), headers=VALID_ADMIN_HEADER_TOKEN)
assert resp.status_code == 200 assert resp.status_code == 200
assert resp.json == {'items': [], 'total': 0} assert resp.json == {'items': [], 'total': 0}
def test_admin_accounts_crud(client): def test_admin_destinations_crud(client):
assert client.post(api.url_for(AccountsList), headers=VALID_ADMIN_HEADER_TOKEN).status_code == 400 assert client.post(api.url_for(DestinationsList), headers=VALID_ADMIN_HEADER_TOKEN).status_code == 400
data = {'accountNumber': 111, 'label': 'test', 'comments': 'test'} data = {'destinationNumber': 111, 'label': 'test', 'comments': 'test'}
resp = client.post(api.url_for(AccountsList), data=dumps(data), content_type='application/json', headers=VALID_ADMIN_HEADER_TOKEN) resp = client.post(api.url_for(DestinationsList), data=dumps(data), content_type='application/json', headers=VALID_ADMIN_HEADER_TOKEN)
assert resp.status_code == 200 assert resp.status_code == 200
assert client.get(api.url_for(Accounts, account_id=resp.json['id']), headers=VALID_ADMIN_HEADER_TOKEN).status_code == 200 assert client.get(api.url_for(Destinations, destination_id=resp.json['id']), headers=VALID_ADMIN_HEADER_TOKEN).status_code == 200
resp = client.get(api.url_for(AccountsList), headers=VALID_ADMIN_HEADER_TOKEN) resp = client.get(api.url_for(DestinationsList), headers=VALID_ADMIN_HEADER_TOKEN)
assert resp.status_code == 200 assert resp.status_code == 200
assert resp.json == {'items': [{'accountNumber': 111, 'label': 'test', 'comments': 'test', 'id': 2}], 'total': 1} assert resp.json == {'items': [{'destinationNumber': 111, 'label': 'test', 'comments': 'test', 'id': 2}], 'total': 1}
assert client.delete(api.url_for(Accounts, account_id=2), headers=VALID_ADMIN_HEADER_TOKEN).status_code == 200 assert client.delete(api.url_for(Destinations, destination_id=2), headers=VALID_ADMIN_HEADER_TOKEN).status_code == 200
resp = client.get(api.url_for(AccountsList), headers=VALID_ADMIN_HEADER_TOKEN) resp = client.get(api.url_for(DestinationsList), headers=VALID_ADMIN_HEADER_TOKEN)
assert resp.status_code == 200 assert resp.status_code == 200
assert resp.json == {'items': [], 'total': 0} assert resp.json == {'items': [], 'total': 0}