Merge pull request #20 from kevgliss/destinations

Adds support for 'Destinations'
This commit is contained in:
kevgliss 2015-07-10 17:20:05 -07:00
commit 14b62a145a
76 changed files with 1498 additions and 1312 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

@ -54,7 +54,7 @@ def create(kwargs):
kwargs['creator'] = g.current_user.email kwargs['creator'] = g.current_user.email
cert_body, intermediate, issuer_roles = issuer.create_authority(kwargs) cert_body, intermediate, issuer_roles = issuer.create_authority(kwargs)
cert = cert_service.save_cert(cert_body, None, intermediate, None) cert = cert_service.save_cert(cert_body, None, intermediate, [])
cert.user = g.current_user cert.user = g.current_user
# we create and attach any roles that the issuer gives us # we create and attach any roles that the issuer gives us

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

@ -41,7 +41,7 @@ class marshal_items(object):
return {'items': _filter_items(resp.items), 'total': resp.total} return {'items': _filter_items(resp.items), 'total': resp.total}
if isinstance(resp, list): if isinstance(resp, list):
return _filter_items(resp) return {'items': _filter_items(resp), 'total': len(resp)}
return marshal(resp, self.fields) return marshal(resp, self.fields)
except Exception as e: except Exception as e:

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

@ -8,7 +8,6 @@
from flask import current_app from flask import current_app
from lemur.common.managers import InstanceManager from lemur.common.managers import InstanceManager
# inspired by https://github.com/getsentry/sentry # inspired by https://github.com/getsentry/sentry
class PluginManager(InstanceManager): class PluginManager(InstanceManager):
def __iter__(self): def __iter__(self):
@ -17,8 +16,10 @@ class PluginManager(InstanceManager):
def __len__(self): def __len__(self):
return sum(1 for i in self.all()) return sum(1 for i in self.all())
def all(self, version=1): def all(self, version=1, plugin_type=None):
for plugin in sorted(super(PluginManager, self).all(), key=lambda x: x.get_title()): for plugin in sorted(super(PluginManager, self).all(), key=lambda x: x.get_title()):
if not plugin.type == plugin_type and plugin_type:
continue
if not plugin.is_enabled(): if not plugin.is_enabled():
continue continue
if version is not None and plugin.__version__ != version: if version is not None and plugin.__version__ != version:

View File

@ -47,12 +47,13 @@ class IPlugin(local):
# Configuration specifics # Configuration specifics
conf_key = None conf_key = None
conf_title = None conf_title = None
options = {}
# Global enabled state # Global enabled state
enabled = True enabled = True
can_disable = True can_disable = True
def is_enabled(self, project=None): def is_enabled(self):
""" """
Returns a boolean representing if this plugin is enabled. Returns a boolean representing if this plugin is enabled.
If ``project`` is passed, it will limit the scope to that project. If ``project`` is passed, it will limit the scope to that project.

View File

@ -1,2 +1,3 @@
from .destination import DestinationPlugin # NOQA from .destination import DestinationPlugin # NOQA
from .issuer import IssuerPlugin # NOQA from .issuer import IssuerPlugin # NOQA
from .source import SourcePlugin

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

@ -13,6 +13,8 @@ class IssuerPlugin(Plugin):
This is the base class from which all of the supported This is the base class from which all of the supported
issuers will inherit from. issuers will inherit from.
""" """
type = 'issuer'
def create_certificate(self): def create_certificate(self):
raise NotImplementedError raise NotImplementedError

View File

@ -0,0 +1,19 @@
"""
.. module: lemur.bases.source
: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.base import Plugin
class SourcePlugin(Plugin):
type = 'source'
def get_certificates(self):
raise NotImplemented
def get_options(self):
return {}

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

@ -18,12 +18,12 @@ from requests.adapters import HTTPAdapter
from flask import current_app from flask import current_app
from lemur.exceptions import LemurException from lemur.exceptions import LemurException
from lemur.plugins.bases import IssuerPlugin from lemur.plugins.bases import IssuerPlugin, SourcePlugin
from lemur.plugins import lemur_cloudca as cloudca from lemur.plugins import lemur_cloudca as cloudca
from lemur.authorities import service as authority_service from lemur.authorities import service as authority_service
API_ENDPOINT = '/v1/ca/netflix' API_ENDPOINT = '/v1/ca/netflix' # TODO this should be configurable
class CloudCAException(LemurException): class CloudCAException(LemurException):
@ -142,15 +142,7 @@ def get_auth_data(ca_name):
raise CloudCAException("You do not have the required role to issue certificates from {0}".format(ca_name)) raise CloudCAException("You do not have the required role to issue certificates from {0}".format(ca_name))
class CloudCAPlugin(IssuerPlugin): class CloudCA(object):
title = 'CloudCA'
slug = 'cloudca'
description = 'Enables the creation of certificates from the cloudca API.'
version = cloudca.VERSION
author = 'Kevin Glisson'
author_url = 'https://github.com/netflix/lemur'
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.session = requests.Session() self.session = requests.Session()
self.session.mount('https://', CloudCAHostNameCheckingAdapter()) self.session.mount('https://', CloudCAHostNameCheckingAdapter())
@ -162,7 +154,69 @@ class CloudCAPlugin(IssuerPlugin):
else: else:
current_app.logger.warning("No CLOUDCA credentials found, lemur will be unable to request certificates from CLOUDCA") current_app.logger.warning("No CLOUDCA credentials found, lemur will be unable to request certificates from CLOUDCA")
super(CloudCAPlugin, self).__init__(*args, **kwargs) super(CloudCA, self).__init__(*args, **kwargs)
def post(self, endpoint, data):
"""
HTTP POST to CloudCA
:param endpoint:
:param data:
:return:
"""
data = dumps(dict(data.items() + get_auth_data(data['caName']).items()))
# we set a low timeout, if cloudca is down it shouldn't bring down
# lemur
response = self.session.post(self.url + endpoint, data=data, timeout=10, verify=self.ca_bundle)
return process_response(response)
def get(self, endpoint):
"""
HTTP GET to CloudCA
:param endpoint:
:return:
"""
response = self.session.get(self.url + endpoint, timeout=10, verify=self.ca_bundle)
return process_response(response)
def random(self, length=10):
"""
Uses CloudCA as a decent source of randomness.
:param length:
:return:
"""
endpoint = '/v1/random/{0}'.format(length)
response = self.session.get(self.url + endpoint, verify=self.ca_bundle)
return response
def get_authorities(self):
"""
Retrieves authorities that were made outside of Lemur.
:return:
"""
endpoint = '{0}/listCAs'.format(API_ENDPOINT)
authorities = []
for ca in self.get(endpoint)['data']['caList']:
try:
authorities.append(ca['caName'])
except AttributeError as e:
current_app.logger.error("No authority has been defined for {}".format(ca['caName']))
return authorities
class CloudCAIssuerPlugin(IssuerPlugin, CloudCA):
title = 'CloudCA'
slug = 'cloudca-issuer'
description = 'Enables the creation of certificates from the cloudca API.'
version = cloudca.VERSION
author = 'Kevin Glisson'
author_url = 'https://github.com/netflix/lemur'
def create_authority(self, options): def create_authority(self, options):
""" """
@ -205,22 +259,6 @@ class CloudCAPlugin(IssuerPlugin):
return cert, "".join(intermediates), roles, return cert, "".join(intermediates), roles,
def get_authorities(self):
"""
Retrieves authorities that were made outside of Lemur.
:return:
"""
endpoint = '{0}/listCAs'.format(API_ENDPOINT)
authorities = []
for ca in self.get(endpoint)['data']['caList']:
try:
authorities.append(ca['caName'])
except AttributeError as e:
current_app.logger.error("No authority has been defined for {}".format(ca['caName']))
return authorities
def create_certificate(self, csr, options): def create_certificate(self, csr, options):
""" """
Creates a new certificate from cloudca Creates a new certificate from cloudca
@ -259,16 +297,25 @@ class CloudCAPlugin(IssuerPlugin):
return cert, "".join(intermediates), return cert, "".join(intermediates),
def random(self, length=10):
"""
Uses CloudCA as a decent source of randomness.
:param length: class CloudCASourcePlugin(SourcePlugin, CloudCA):
:return: title = 'CloudCA'
""" slug = 'cloudca-source'
endpoint = '/v1/random/{0}'.format(length) description = 'Discovers all SSL certificates in CloudCA'
response = self.session.get(self.url + endpoint, verify=self.ca_bundle) version = cloudca.VERSION
return response
author = 'Kevin Glisson'
author_url = 'https://github.com/netflix/lemur'
options = {
'pollRate': {'type': 'int', 'default': '60'}
}
def get_certificates(self, **kwargs):
certs = []
for authority in self.get_authorities():
certs += self.get_cert(ca_name=authority)
return
def get_cert(self, ca_name=None, cert_handle=None): def get_cert(self, ca_name=None, cert_handle=None):
""" """
@ -297,29 +344,3 @@ class CloudCAPlugin(IssuerPlugin):
}) })
return certs return certs
def post(self, endpoint, data):
"""
HTTP POST to CloudCA
:param endpoint:
:param data:
:return:
"""
data = dumps(dict(data.items() + get_auth_data(data['caName']).items()))
# we set a low timeout, if cloudca is down it shouldn't bring down
# lemur
response = self.session.post(self.url + endpoint, data=data, timeout=10, verify=self.ca_bundle)
return process_response(response)
def get(self, endpoint):
"""
HTTP GET to CloudCA
:param endpoint:
:return:
"""
response = self.session.get(self.url + endpoint, timeout=10, verify=self.ca_bundle)
return process_response(response)

View File

@ -75,9 +75,9 @@ def handle_response(content):
return d return d
class VerisignPlugin(IssuerPlugin): class VerisignIssuerPlugin(IssuerPlugin):
title = 'VeriSign' title = 'Verisign'
slug = 'verisign' slug = 'verisign-issuer'
description = 'Enables the creation of certificates by the VICE2.0 verisign API.' description = 'Enables the creation of certificates by the VICE2.0 verisign API.'
version = verisign.VERSION version = verisign.VERSION
@ -87,7 +87,7 @@ class VerisignPlugin(IssuerPlugin):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.session = requests.Session() self.session = requests.Session()
self.session.cert = current_app.config.get('VERISIGN_PEM_PATH') self.session.cert = current_app.config.get('VERISIGN_PEM_PATH')
super(VerisignPlugin, self).__init__(*args, **kwargs) super(VerisignIssuerPlugin, self).__init__(*args, **kwargs)
def create_certificate(self, csr, issuer_options): def create_certificate(self, csr, issuer_options):
""" """

140
lemur/plugins/views.py Normal file
View File

@ -0,0 +1,140 @@
"""
.. module: lemur.plugins.views
:platform: Unix
:synopsis: This module contains all of the accounts view code.
: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 Api, reqparse, fields
from lemur.auth.service import AuthenticatedResource
from lemur.common.utils import marshal_items
from lemur.plugins.base import plugins
mod = Blueprint('plugins', __name__)
api = Api(mod)
FIELDS = {
'title': fields.String,
'pluginOptions': fields.Raw(attribute='options'),
'description': fields.String,
'version': fields.String,
'author': fields.String,
'authorUrl': fields.String,
'type': fields.String,
'slug': fields.String,
}
class PluginsList(AuthenticatedResource):
""" Defines the 'plugins' endpoint """
def __init__(self):
self.reqparse = reqparse.RequestParser()
super(PluginsList, self).__init__()
@marshal_items(FIELDS)
def get(self):
"""
.. http:get:: /plugins
The current plugin list
**Example request**:
.. sourcecode:: http
GET /plugins 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
{
"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
}
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
"""
return plugins.all()
class PluginsTypeList(AuthenticatedResource):
""" Defines the 'plugins' endpoint """
def __init__(self):
self.reqparse = reqparse.RequestParser()
super(PluginsTypeList, self).__init__()
@marshal_items(FIELDS)
def get(self, plugin_type):
"""
.. http:get:: /plugins/issuer
The current plugin list
**Example request**:
.. sourcecode:: http
GET /plugins/issuer 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
{
"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
}
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
"""
return list(plugins.all(plugin_type=plugin_type))
api.add_resource(PluginsList, '/plugins', endpoint='plugins')
api.add_resource(PluginsTypeList, '/plugins/<plugin_type>', endpoint='pluginType')

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

@ -2,17 +2,6 @@
angular.module('lemur') angular.module('lemur')
.config(function config($routeProvider) {
$routeProvider.when('/authorities/create', {
templateUrl: '/angular/authorities/authority/authorityWizard.tpl.html',
controller: 'AuthorityCreateController'
});
$routeProvider.when('/authorities/:id/edit', {
templateUrl: '/angular/authorities/authority/authorityEdit.tpl.html',
controller: 'AuthorityEditController'
});
})
.controller('AuthorityEditController', function ($scope, $routeParams, AuthorityApi, AuthorityService, RoleService){ .controller('AuthorityEditController', function ($scope, $routeParams, AuthorityApi, AuthorityService, RoleService){
AuthorityApi.get($routeParams.id).then(function (authority) { AuthorityApi.get($routeParams.id).then(function (authority) {
AuthorityService.getRoles(authority); AuthorityService.getRoles(authority);
@ -24,16 +13,21 @@ angular.module('lemur')
$scope.roleService = RoleService; $scope.roleService = RoleService;
}) })
.controller('AuthorityCreateController', function ($scope, $modal, AuthorityService, LemurRestangular, RoleService) { .controller('AuthorityCreateController', function ($scope, $modalInstance, AuthorityService, LemurRestangular, RoleService, PluginService, WizardHandler) {
$scope.authority = LemurRestangular.restangularizeElement(null, {}, 'authorities'); $scope.authority = LemurRestangular.restangularizeElement(null, {}, 'authorities');
$scope.save = function (authority) { $scope.loading = false;
var loadingModal = $modal.open({backdrop: 'static', template: '<wave-spinner></wave-spinner>', windowTemplateUrl: 'angular/loadingModal.html', size: 'large'}); $scope.create = function (authority) {
return AuthorityService.create(authority).then(function (response) { WizardHandler.wizard().context.loading = true;
loadingModal.close(); AuthorityService.create(authority).then(function (resposne) {
}); WizardHandler.wizard().context.loading = false;
$modalInstance.close();
})
}; };
PluginService.get('issuer').then(function (plugins) {
$scope.plugins = plugins;
});
$scope.roleService = RoleService; $scope.roleService = RoleService;

View File

@ -1,17 +1,21 @@
<h2 class="featurette-heading"><span ng-show="!authority.id">Create</span><span ng-show="authority.id">Edit</span> Authority <span class="text-muted"><small>The nail that sticks out farthest gets hammered the hardest <div class="modal-header">
<div> <h3 class="modal-title"><span ng-show="!authority.id">Create</span><span ng-show="authority.id">Edit</span> Authority <span class="text-muted"><small>The nail that sticks out farthest gets hammered the hardest</small></span></h3>
<wizard on-finish="save(authority)" template="angular/wizard.html"> </div>
<div class="modal-body">
<div>
<wizard on-finish="create(authority)" template="angular/wizard.html">
<wz-step title="Tracking" canexit="exitTracking"> <wz-step title="Tracking" canexit="exitTracking">
<ng-include src="'angular/authorities/authority/tracking.tpl.html'"></ng-include> <ng-include src="'angular/authorities/authority/tracking.tpl.html'"></ng-include>
</wz-step> </wz-step>
<wz-step title="Distinguished Name" canenter="exitTracking" canexit="exitDN">
<ng-include src="'angular/authorities/authority/distinguishedName.tpl.html'"></ng-include>
</wz-step>
<wz-step title="Options" canenter="exitDN" canexit="exitOptions"> <wz-step title="Options" canenter="exitDN" canexit="exitOptions">
<ng-include src="'angular/authorities/authority/options.tpl.html'"></ng-include> <ng-include src="'angular/authorities/authority/options.tpl.html'"></ng-include>
</wz-step> </wz-step>
<wz-step title="Distinguished Name" canenter="exitTracking" canexit="exitDN">
<ng-include src="'angular/authorities/authority/distinguishedName.tpl.html'"></ng-include>
</wz-step>
<wz-step title="Extensions" canenter="exitOptions" canexit="exitExtensions"> <wz-step title="Extensions" canenter="exitOptions" canexit="exitExtensions">
<ng-include src="'angular/authorities/authority/extensions.tpl.html'"></ng-include> <ng-include src="'angular/authorities/authority/extensions.tpl.html'"></ng-include>
</wz-step> </wz-step>
</wizard> </wizard>
</div>
</div> </div>

View File

@ -15,7 +15,7 @@
<input type="text" ng-model="authority.caParent" placeholder="Parent Authority Name" <input type="text" ng-model="authority.caParent" placeholder="Parent Authority Name"
typeahead="authority.name for authority in authorityService.findAuthorityByName($viewValue)" typeahead-loading="loadingAuthorities" typeahead="authority.name for authority in authorityService.findAuthorityByName($viewValue)" typeahead-loading="loadingAuthorities"
class="form-control input-md" typeahead-min-wait="50" class="form-control input-md" typeahead-min-wait="50"
tooltip="When you specifiy a subordinate certificate authority you must specific the parent authority" tooltip="When you specify a subordinate certificate authority you must specific the parent authority"
tooltip-trigger="focus" tooltip-placement="top"> tooltip-trigger="focus" tooltip-placement="top">
</div> </div>
</div> </div>
@ -69,10 +69,10 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="control-label col-sm-2"> <label class="control-label col-sm-2">
Plugin Name Plugin
</label> </label>
<div class="col-sm-10"> <div class="col-sm-10">
<select class="form-control" ng-model="authority.pluginName" ng-options="option for option in ['cloudca', 'verisign']" ng-init="authority.pluginName = 'cloudca'" required></select> <select class="form-control" ng-model="authority.pluginName" ng-options="plugin.slug as plugin.title for plugin in plugins" ng-init="authority.pluginName = 'cloudca-issuer'" required></select>
</div> </div>
</div> </div>
</div> </div>

View File

@ -31,34 +31,37 @@
</div> </div>
</div> </div>
<div class="form-group" <div class="form-group"
ng-class="{'has-error': trackingForm.commonName.$invalid, 'has-success': !trackingForm.$invalid&&trackingForm.commanName.$dirty}"> ng-class="{'has-error': trackingForm.commonName.$invalid, 'has-success': !trackingForm.$invalid&&trackingForm.commonName.$dirty}">
<label class="control-label col-sm-2"> <label class="control-label col-sm-2">
Common Name Common Name
</label> </label>
<div class="col-sm-10"> <div class="col-sm-10">
<input name="commonName" ng-model="authority.caDN.commonName" placeholder="Common Name" class="form-control" required/> <input name="commonName" ng-model="authority.caDN.commonName" placeholder="Common Name" class="form-control" required/>
<p ng-show="trackingForm.commandName.$invalid && !trackingForm.commonName.$pristine" class="help-block">You must enter a common name</p> <p ng-show="trackingForm.commonName.$invalid && !trackingForm.commonName.$pristine" class="help-block">You must enter a common name</p>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group"
ng-class="{'has-error': trackingForm.validityEnd.$invalid || trackingForm.validityStart.$invalid, 'has-success': !trackingForm.$invalid&&trackingForm.validityEnd.$dirty&&trackingForm.validityStart.$dirty}">
<label class="control-label col-sm-2"> <label class="control-label col-sm-2">
Validity Range Validity Range
</label> </label>
<div class="col-sm-4"> <div class="col-sm-4">
<div> <div>
<div class="input-group"> <div class="input-group">
<input tooltip="Starting Date" class="form-control" datepicker-popup="yyyy/MM/dd" is-open="opened1" ng-model="authority.validityStart" /> <input name="validityStart" tooltip="Starting Date" class="form-control" datepicker-popup="yyyy/MM/dd" is-open="opened1" ng-model="authority.validityStart" required/>
<p ng-show="trackingForm.validityStart.$invalid && !trackingForm.validityStart.$pristine" class="help-block">A start date is required!</p>
<span class="input-group-btn"> <span class="input-group-btn">
<button class="btn btn-default" ng-click="open($event)"><i class="glyphicon glyphicon-calendar"></i></button> <button class="btn btn-default" ng-click="open($event)"><i class="glyphicon glyphicon-calendar"></i></button>
</span> </span>
</div> </div>
</div> </div>
</div> </div>
<span class="text-center col-sm-2"><label><span class="glyphicon glyphicon-resize-horizontal"></span></label></span> <span style="padding-top: 15px" class="text-center col-sm-2"><label><span class="glyphicon glyphicon-resize-horizontal"></span></label></span>
<div class="col-sm-4"> <div class="col-sm-4">
<div> <div>
<div class="input-group"> <div class="input-group">
<input tooltip="Ending Date" class="form-control" datepicker-popup="yyyy/MM/dd" is-open="opened2" ng-model="authority.validityEnd" /> <input name="validityEnd" tooltip="Ending Date" class="form-control" datepicker-popup="yyyy/MM/dd" is-open="opened2" ng-model="authority.validityEnd" required/>
<p ng-show="trackingForm.validityEnd.$invalid && !trackingForm.validityEnd.$pristine" class="help-block">A end date is required!</p>
<span class="input-group-btn"> <span class="input-group-btn">
<button class="btn btn-default" ng-click="open2($event)"><i class="glyphicon glyphicon-calendar"></i></button> <button class="btn btn-default" ng-click="open2($event)"><i class="glyphicon glyphicon-calendar"></i></button>
</span> </span>

View File

@ -9,7 +9,7 @@ angular.module('lemur')
}); });
}) })
.controller('AuthoritiesViewController', function ($scope, $q, AuthorityApi, AuthorityService, ngTableParams) { .controller('AuthoritiesViewController', function ($scope, $q, $modal, AuthorityApi, AuthorityService, ngTableParams) {
$scope.filter = {}; $scope.filter = {};
$scope.authoritiesTable = new ngTableParams({ $scope.authoritiesTable = new ngTableParams({
page: 1, // show first page page: 1, // show first page
@ -43,4 +43,36 @@ angular.module('lemur')
params.settings().$scope.show_filter = !params.settings().$scope.show_filter; params.settings().$scope.show_filter = !params.settings().$scope.show_filter;
}; };
$scope.edit = function (authorityId) {
var modalInstance = $modal.open({
animation: true,
templateUrl: '/angular/authorities/authority/authorityWizard.tpl.html',
controller: 'AuthorityEditController',
size: 'lg',
resolve: {
editId: function () {
return authorityId;
}
}
});
modalInstance.result.then(function () {
$scope.authoritiesTable.reload();
});
};
$scope.create = function () {
var modalInstance = $modal.open({
animation: true,
controller: 'AuthorityCreateController',
templateUrl: '/angular/authorities/authority/authorityWizard.tpl.html',
size: 'lg'
});
modalInstance.result.then(function () {
$scope.authoritiesTable.reload();
});
};
}); });

View File

@ -5,7 +5,7 @@
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<div class="btn-group pull-right"> <div class="btn-group pull-right">
<a href="#/authorities/create" class="btn btn-primary">Create</a> <button class="btn btn-primary" ng-click="create()">Create</button>
</div> </div>
<div class="btn-group"> <div class="btn-group">
<button ng-click="toggleFilter(authoritiesTable)" class="btn btn-default">Filter</button> <button ng-click="toggleFilter(authoritiesTable)" class="btn btn-default">Filter</button>
@ -36,9 +36,9 @@
</td> </td>
<td data-title="''"> <td data-title="''">
<div class="btn-group-vertical pull-right"> <div class="btn-group-vertical pull-right">
<a tooltip="Edit Authority" href="#/authorities/{{ authority.id }}/edit" class="btn btn-sm btn-info"> <button tooltip="Edit Authority" ng-click="edit(authority.id)" class="btn btn-sm btn-info">
Edit Edit
</a> </button>
</div> </div>
</td> </td>
</tr> </tr>

View File

@ -1,18 +1,6 @@
'use strict'; 'use strict';
angular.module('lemur') angular.module('lemur')
.config(function config($routeProvider) {
$routeProvider.when('/certificates/create', {
templateUrl: '/angular/certificates/certificate/certificateWizard.tpl.html',
controller: 'CertificateCreateController'
});
$routeProvider.when('/certificates/:id/edit', {
templateUrl: '/angular/certificates/certificate/edit.tpl.html',
controller: 'CertificateEditController'
});
})
.controller('CertificateEditController', function ($scope, $routeParams, CertificateApi, CertificateService, MomentService) { .controller('CertificateEditController', function ($scope, $routeParams, CertificateApi, CertificateService, MomentService) {
CertificateApi.get($routeParams.id).then(function (certificate) { CertificateApi.get($routeParams.id).then(function (certificate) {
$scope.certificate = certificate; $scope.certificate = certificate;
@ -23,13 +11,14 @@ angular.module('lemur')
}) })
.controller('CertificateCreateController', function ($scope, $modal, CertificateApi, CertificateService, AccountService, ELBService, AuthorityService, MomentService, LemurRestangular) { .controller('CertificateCreateController', function ($scope, $modalInstance, CertificateApi, CertificateService, DestinationService, ELBService, AuthorityService, PluginService, MomentService, WizardHandler, LemurRestangular) {
$scope.certificate = LemurRestangular.restangularizeElement(null, {}, 'certificates'); $scope.certificate = LemurRestangular.restangularizeElement(null, {}, 'certificates');
$scope.save = function (certificate) { $scope.create = function (certificate) {
var loadingModal = $modal.open({backdrop: 'static', template: '<wave-spinner></wave-spinner>', windowTemplateUrl: 'angular/loadingModal.html', size: 'large'}); WizardHandler.wizard().context.loading = true;
CertificateService.create(certificate).then(function (response) { CertificateService.create(certificate).then(function (response) {
loadingModal.close(); WizardHandler.wizard().context.loading = false;
$modalInstance.close();
}); });
}; };
@ -88,7 +77,11 @@ angular.module('lemur')
}; };
PluginService.get('destination').then(function (plugins) {
$scope.plugins = plugins;
});
$scope.elbService = ELBService; $scope.elbService = ELBService;
$scope.authorityService = AuthorityService; $scope.authorityService = AuthorityService;
$scope.accountService = AccountService; $scope.destinationService = DestinationService;
}); });

View File

@ -1,20 +0,0 @@
<h2 class="featurette-heading">Create a certificate <span class="text-muted"><small>encrypt all the things
</small></span></h2>
<div class="panel panel-default">
<div class="panel-heading">
<a href="/#/certificates/" 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" ng-submit="submitCreate()" novalidate>
</form>
</div>
<div class="panel-footer">
<button type="submit" ng-disabled="createForm.$invalid" class="btn btn-success pull-right"><span
ng-show="!loading"> Create </span><span ng-show="loading">Creating <i ng-show="loading"
class="fa fa-cog fa-spin"></i></span>
</button>
<div class="clearfix"></div>
</div>
</div>

View File

@ -1,17 +1,23 @@
<h2 class="featurette-heading"><span ng-show="!certificate.id">Create</span><span ng-show="certificate.id">Edit</span> Certificate <span class="text-muted"><small>encrypt all the things <div class="modal-header">
<h3 class="modal-title"><span ng-show="!certificate.id">Create</span><span ng-show="certificate.id">Edit</span> Certificate <span class="text-muted"><small>encrypt all the things</small></h3>
</div>
<div class="modal-body">
<div> <div>
<wizard on-finish="save(certificate)" template="angular/wizard.html"> <wizard on-finish="create(certificate)" template="angular/wizard.html">
<wz-step title="Tracking" canexit="trackingForm.$valid"> <wz-step title="Tracking" canexit="trackingForm.$valid">
<ng-include src="'angular/certificates/certificate/tracking.tpl.html'"></ng-include> <ng-include src="'angular/certificates/certificate/tracking.tpl.html'"></ng-include>
</wz-step> </wz-step>
<wz-step title="Distinguished Name" canenter="exitTracking" canexit="exitDN">
<ng-include src="'angular/certificates/certificate/distinguishedName.tpl.html'"></ng-include>
</wz-step>
<wz-step title="Options" canenter="enterValidation"> <wz-step title="Options" canenter="enterValidation">
<ng-include src="'angular/certificates/certificate/options.tpl.html'"></ng-include> <ng-include src="'angular/certificates/certificate/options.tpl.html'"></ng-include>
</wz-step> </wz-step>
<wz-step title="Distinguished Name" canenter="exitTracking" canexit="exitDN">
<ng-include src="'angular/certificates/certificate/distinguishedName.tpl.html'"></ng-include>
</wz-step>
<wz-step title="Destinations" canenter="enterValidation"> <wz-step title="Destinations" canenter="enterValidation">
<ng-include src="'angular/certificates/certificate/destinations.tpl.html'"></ng-include> <ng-include src="'angular/certificates/certificate/destinations.tpl.html'"></ng-include>
</wz-step> </wz-step>
</wizard> </wizard>
</div> </div>
</div>

View File

@ -1,62 +1,28 @@
<p>Destinations are purely optional, if you think the created certificate will be used in AWS select one or more accounts and Lemur will upload it for you.</p> <div class="form-group">
<div class="form-horizontal">
<div class="form-group">
<label class="control-label col-sm-2"> <label class="control-label col-sm-2">
Accounts Destinations
</label> </label>
<div class="col-sm-10"> <div class="col-sm-10">
<div class="input-group"> <div class="input-group">
<input type="text" ng-model="certificate.selectedAccount" placeholder="Account Name" <input type="text" ng-model="certificate.selectedDestination" placeholder="AWS, TheSecret..."
typeahead="account.label for account in accountService.findAccountsByName($viewValue)" typeahead-loading="loadingAccounts" typeahead="destination.label for destination in destinationService.findDestinationsByName($viewValue)" typeahead-loading="loadingDestinations"
class="form-control input-md" typeahead-on-select="certificate.attachAccount($item)" typeahead-min-wait="50" class="form-control input-md" typeahead-on-select="certificate.attachDestination($item)" typeahead-min-wait="50"
tooltip="Lemur can upload the certificate to any AWS account." tooltip="Lemur can upload certificates to any pre-defined destination"
tooltip-trigger="focus" tooltip-placement="top"> tooltip-trigger="focus" tooltip-placement="top">
<span class="input-group-btn"> <span class="input-group-btn">
<button ng-model="accounts.show" class="btn btn-md btn-default" btn-checkbox btn-checkbox-true="1" btn-checkbox-false="0"> <button ng-model="destinations.show" class="btn btn-md btn-default" btn-checkbox btn-checkbox-true="1" btn-checkbox-false="0">
<span class="badge">{{ certificate.accounts.length || 0 }}</span> <span class="badge">{{ certificate.destinations.length || 0 }}</span>
</button> </button>
</span> </span>
</div> </div>
<table class="table"> <table class="table">
<tr ng-repeat="account in certificate.accounts track by $index"> <tr ng-repeat="destination in certificate.destinations track by $index">
<td><a class="btn btn-sm btn-info" href="#/accounts/{{ account.id }}/certificates">{{ account.label }}</a></td> <td><a class="btn btn-sm btn-info" href="#/destinations/{{ destination.id }}/certificates">{{ destination.label }}</a></td>
<td><span class="text-muted">{{ account.comments }}</span></td> <td><span class="text-muted">{{ destination.description }}</span></td>
<td> <td>
<button type="button" ng-click="certificate.removeAccount($index)" class="btn btn-danger btn-sm pull-right">Remove</button> <button type="button" ng-click="certificate.removeDestination($index)" class="btn btn-danger btn-sm pull-right">Remove</button>
</td> </td>
</tr> </tr>
</table> </table>
</div> </div>
</div>
<!--<div ng-show="certificate.accounts" class="form-group">
<label class="control-label col-sm-2">
ELBs
</label>
<div class="col-sm-10">
<div class="input-group">
<input type="text" ng-model="certificate.selectedELB" placeholder="ELB Name"
typeahead="elb.name for elb in elbService.findELBByName($viewValue)" typeahead-loading="loadingAccounts"
class="form-control col-md-4" typeahead-min-wait="50"
tooltip="Lemur can upload a certificate to one or more ELBs"
tooltip-trigger="focus" tooltip-placement="top"/>
<input class="form-control col-md-2" type="integer" ng-model="certificate.selectedPort" placeholder="Port"/>
<select class="form-control col-md-2" ng-options="item for item in ['https', 'tcp']"></select>
<span class="input-group-btn">
<button ng-click="certificate.attachELB()" class="btn btn-info">Add</button>
</span>
</div>
<table class="table">
<tr ng-repeat="elb in certificate.elbs track by $index">
<td><a class="btn btn-sm btn-info" href="#/accounts/{{ elb.id }}/elbs">{{ elb.name }}</a></td>
<td>{{ elb.region }}</td>
<td>{{ elb.scheme }}</td>
<td>{{ elb.vpcId }}</td>
<td>{{ elb.listener.scheme }}</td>
<td>{{ elb.listener.port }}</td>
<td>
<button type="button" ng-click="certificate.removeELB($index)" class="btn btn-danger btn-sm pull-right">remove</button>
</td>
</tr>
</table>
</div>-->
</div> </div>

View File

@ -47,7 +47,7 @@
Common Name Common Name
</label> </label>
<div class="col-sm-10"> <div class="col-sm-10">
<input name="commonName" tooltip="If you need a certificate with multiple domains enter your primary domain here and the rest under 'Subject Alternate Names' in the options panel" ng-model="certificate.commonName" placeholder="Common Name" class="form-control" required/> <input name="commonName" tooltip="If you need a certificate with multiple domains enter your primary domain here and the rest under 'Subject Alternate Names' in the next panel" ng-model="certificate.commonName" placeholder="Common Name" class="form-control" required/>
<p ng-show="trackingForm.commonName.$invalid && !trackingForm.commonName.$pristine" class="help-block">You must enter a common name</p> <p ng-show="trackingForm.commonName.$invalid && !trackingForm.commonName.$pristine" class="help-block">You must enter a common name</p>
</div> </div>
</div> </div>
@ -65,7 +65,7 @@
</div> </div>
</div> </div>
</div> </div>
<span class="text-center col-sm-2"><label><span class="glyphicon glyphicon-resize-horizontal"></span></label></span> <span style="padding-top: 15px" class="text-center col-sm-2"><label><span class="glyphicon glyphicon-resize-horizontal"></span></label></span>
<div class="col-sm-4"> <div class="col-sm-4">
<div> <div>
<div class="input-group"> <div class="input-group">

View File

@ -2,20 +2,16 @@
angular.module('lemur') angular.module('lemur')
.config(function config($routeProvider) { .controller('CertificateUploadController', function ($scope, $modalInstance, CertificateService, LemurRestangular, DestinationService, ELBService, PluginService) {
$routeProvider.when('/certificates/upload', {
templateUrl: '/angular/certificates/certificate/upload.tpl.html',
controller: 'CertificatesUploadController'
});
})
.controller('CertificatesUploadController', function ($scope, CertificateService, LemurRestangular, AccountService, ELBService) {
$scope.certificate = LemurRestangular.restangularizeElement(null, {}, 'certificates'); $scope.certificate = LemurRestangular.restangularizeElement(null, {}, 'certificates');
$scope.upload = CertificateService.upload; $scope.upload = CertificateService.upload;
$scope.accountService = AccountService; $scope.destinationService = DestinationService;
$scope.elbService = ELBService; $scope.elbService = ELBService;
PluginService.get('destination').then(function (plugins) {
$scope.plugins = plugins;
});
$scope.attachELB = function (elb) { $scope.attachELB = function (elb) {
$scope.certificate.attachELB(elb); $scope.certificate.attachELB(elb);
@ -23,4 +19,9 @@ angular.module('lemur')
$scope.certificate.elb.listeners = listeners; $scope.certificate.elb.listeners = listeners;
}); });
}; };
$scope.cancel = function () {
$modalInstance.dismiss('cancel');
};
}); });

View File

@ -1,11 +1,8 @@
<h2 class="featurette-heading">Upload a certificate <span class="text-muted"><small>encrypt all the things <div class="modal-header">
</small></span></h2> <div class="modal-title">
<div class="panel panel-default"> <h3 class="modal-header">Upload a certificate <span class="text-muted"><small>encrypt all the things</small></span></h3>
<div class="panel-heading">
<a href="/#/certificates/" class="btn btn-danger pull-right">Cancel</a>
<div class="clearfix"></div>
</div> </div>
<div class="panel-body"> <div class="modal-body">
<form name="uploadForm" class="form-horizontal" role="form" novalidate> <form name="uploadForm" class="form-horizontal" role="form" novalidate>
<div class="form-group" <div class="form-group"
ng-class="{'has-error': uploadForm.owner.$invalid, 'has-success': !uploadForm.owner.$invalid&&uploadForm.owner.$dirty}"> ng-class="{'has-error': uploadForm.owner.$invalid, 'has-success': !uploadForm.owner.$invalid&&uploadForm.owner.$dirty}">
@ -64,73 +61,11 @@
class="help-block">Enter a valid certificate.</p> class="help-block">Enter a valid certificate.</p>
</div> </div>
</div> </div>
<div class="form-group"> <div ng-include="'angular/certificates/certificate/destinations.tpl.html'"></div>
<label class="control-label col-sm-2">
Accounts
</label>
<div class="col-sm-10">
<div class="input-group">
<input type="text" ng-model="certificate.selectedAccount" placeholder="Account Name"
typeahead="account.label for account in accountService.findAccountsByName($viewValue)" typeahead-loading="loadingAccounts"
class="form-control" typeahead-on-select="certificate.attachAccount($item)" typeahead-min-wait="50"
tooltip="Lemur can upload the certificate to any AWS account."
tooltip-trigger="focus" tooltip-placement="top">
<span class="input-group-btn">
<button ng-model="certificate.accounts" class="btn btn-default" btn-checkbox btn-checkbox-true="1" btn-checkbox-false="0">
<span class="badge">{{ certificate.accounts.length || 0 }}</span>
</button>
</span>
</div>
<table ng-show="certificate.accounts" class="table">
<tr ng-repeat="account in certificate.accounts track by $index">
<td><a class="btn btn-sm btn-info" href="#/accounts/{{ account.id }}/certificates">{{ account.label }}</a></td>
<td><span class="text-muted">{{ account.comments }}</span></td>
<td>
<button type="button" ng-click="certificate.removeAccount($index)" class="btn btn-danger btn-sm pull-right">
Remove
</button>
</td>
</tr>
</table>
</div>
</div>
<!--<div class="form-group">
<label class="control-label col-sm-2">
ELBs
</label>
<div class="col-sm-10">
<div class="input-group">
<input type="text" ng-model="certificate.selectedELB" placeholder="ELB Name"
typeahead="elb.name for elb in elbService.findELBByName($viewValue)" typeahead-loading="loadingAccounts"
class="form-control" typeahead-on-select="attachELB($item)" typeahead-min-wait="50"
tooltip="Lemur can upload a certificate to one or more ELBs searching will be limited to the accounts selected above"
tooltip-trigger="focus" tooltip-placement="top">
<span class="input-group-btn">
<button ng-model="certificate.elbs" class="btn btn-default" btn-checkbox btn-checkbox-true="1" btn-checkbox-false="0">
<span class="badge">{{ certificate.elbs.length || 0 }}</span>
</button>
</span>
</div>
<table ng-show="certificate.elbs" class="table">
<tr ng-repeat="elb in certificate.elbs track by $index">
<td><a class="btn btn-sm btn-info" href="#/accounts/{{ elb.id }}/elbs">{{ elb.name }}</a></td>
<td>{{ elb.region }}</td>
<td>{{ elb.scheme }}</td>
<td>{{ elb.vpcId }}</td>
<td><a class="btn btn-info" ng-model="elb.showListeners" btn-checkbox btn-checkbox-true="1" btn-checkbox-false="0">View Listeners</a></td>
<td>
<button type="button" ng-click="certificate.removeAccount($index)" class="btn btn-danger btn-sm pull-right">
Remove
</button>
</td>
</tr>
</table>
</div>
</div>-->
</form> </form>
</div> </div>
<div class="panel-footer"> <div class="modal-footer">
<button type="submit" ng-click="upload(certificate)" ng-disabled="uploadForm.$invalid" class="btn btn-success pull-right">Upload</button> <button type="submit" ng-click="upload(certificate)" ng-disabled="uploadForm.$invalid" class="btn btn-success">Import</button>
<div class="clearfix"></div> <button ng-click="cancel()" class="btn btn-danger">Cancel</button>
</div> </div>
</div> </div>

View File

@ -58,15 +58,15 @@ angular.module('lemur')
removeCustom: function (index) { removeCustom: function (index) {
this.extensions.custom.splice(index, 1); this.extensions.custom.splice(index, 1);
}, },
attachAccount: function (account) { attachDestination: function (destination) {
this.selectedAccount = null; this.selectedDestination = null;
if (this.accounts === undefined) { if (this.destinations === undefined) {
this.accounts = []; this.destinations = [];
} }
this.accounts.push(account); this.destinations.push(destination);
}, },
removeAccount: function (index) { removeDestination: function (index) {
this.accounts.splice(index, 1); this.destinations.splice(index, 1);
}, },
attachELB: function (elb) { attachELB: function (elb) {
this.selectedELB = null; this.selectedELB = null;
@ -99,13 +99,6 @@ angular.module('lemur')
}); });
}; };
CertificateService.getARNs = function (certificate) {
certificate.arns = [];
_.each(certificate.accounts, function (account) {
certificate.arns.push('arn:aws:iam::' + account.accountNumber + ':server-certificate/' + certificate.name);
});
};
CertificateService.create = function (certificate) { CertificateService.create = function (certificate) {
certificate.attachSubAltName(); certificate.attachSubAltName();
return CertificateApi.post(certificate).then( return CertificateApi.post(certificate).then(
@ -191,10 +184,9 @@ angular.module('lemur')
}); });
}; };
CertificateService.getAccounts = function (certificate) { CertificateService.getDestinations = function (certificate) {
certificate.getList('accounts').then(function (accounts) { certificate.getList('destinations').then(function (destinations) {
certificate.accounts = accounts; certificate.destinations = destinations;
CertificateService.getARNs(certificate);
}); });
}; };

View File

@ -9,7 +9,7 @@ angular.module('lemur')
}); });
}) })
.controller('CertificatesViewController', function ($q, $scope, CertificateApi, CertificateService, MomentService, ngTableParams) { .controller('CertificatesViewController', function ($q, $scope, $modal, CertificateApi, CertificateService, MomentService, ngTableParams) {
$scope.filter = {}; $scope.filter = {};
$scope.certificateTable = new ngTableParams({ $scope.certificateTable = new ngTableParams({
page: 1, // show first page page: 1, // show first page
@ -26,7 +26,7 @@ angular.module('lemur')
// TODO we should attempt to resolve all of these in parallel // TODO we should attempt to resolve all of these in parallel
_.each(data, function (certificate) { _.each(data, function (certificate) {
CertificateService.getDomains(certificate); CertificateService.getDomains(certificate);
CertificateService.getAccounts(certificate); CertificateService.getDestinations(certificate);
CertificateService.getListeners(certificate); CertificateService.getListeners(certificate);
CertificateService.getAuthority(certificate); CertificateService.getAuthority(certificate);
CertificateService.getCreator(certificate); CertificateService.getCreator(certificate);
@ -60,4 +60,30 @@ angular.module('lemur')
$scope.toggleFilter = function (params) { $scope.toggleFilter = function (params) {
params.settings().$scope.show_filter = !params.settings().$scope.show_filter; params.settings().$scope.show_filter = !params.settings().$scope.show_filter;
}; };
$scope.create = function () {
var modalInstance = $modal.open({
animation: true,
controller: 'CertificateCreateController',
templateUrl: '/angular/certificates/certificate/certificateWizard.tpl.html',
size: 'lg'
});
modalInstance.result.then(function () {
$scope.certificateTable.reload();
});
};
$scope.import = function () {
var modalInstance = $modal.open({
animation: true,
controller: 'CertificateUploadController',
templateUrl: '/angular/certificates/certificate/upload.tpl.html',
size: 'lg'
});
modalInstance.result.then(function () {
$scope.certificateTable.reload();
});
};
}); });

View File

@ -5,14 +5,14 @@
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<div class="btn-group pull-right"> <div class="btn-group pull-right">
<a data-placement="left" data-title="Create Certificate" bs-tooltip href="#/certificates/create" <button data-placement="left" data-title="Create Certificate" bs-tooltip ng-click="create()"
class="btn btn-primary"> class="btn btn-primary">
Create Create
</a> </button>
<a data-placement="left" data-title="Upload Certificate" bs-tooltip href="#/certificates/upload" <button data-placement="left" data-title="Import Certificate" bs-tooltip ng-click="import()"
class="btn btn-info"> class="btn btn-info">
Upload Import
</a> </button>
</div> </div>
<div class="btn-group"> <div class="btn-group">
<button ng-click="toggleFilter(certificateTable)" class="btn btn-default">Filter</button> <button ng-click="toggleFilter(certificateTable)" class="btn btn-default">Filter</button>
@ -30,9 +30,9 @@
<li><span class="text-muted">{{ certificate.owner }}</span></li> <li><span class="text-muted">{{ certificate.owner }}</span></li>
</ul> </ul>
</td> </td>
<td data-title="'Accounts'" filter="{ 'account': 'select' }" filter-date="getAccountDropDown()"> <td data-title="'Destinations'" filter="{ 'destination': 'select' }" filter-date="getDestinationDropDown()">
<div class="btn-group"> <div class="btn-group">
<a href="#/accounts/{{ account.id }}/edit" class="btn btn-sm btn-default" ng-repeat="account in certificate.accounts">{{ account.label }}</a> <a href="#/destinations/{{ destination.id }}/edit" class="btn btn-sm btn-default" ng-repeat="account in certificate.destinations">{{ destination.label }}</a>
</div> </div>
</td> </td>
<td data-title="'Active'" filter="{ 'active': 'select' }" filter-data="getCertificateStatus()"> <td data-title="'Active'" filter="{ 'active': 'select' }" filter-data="getCertificateStatus()">
@ -106,7 +106,7 @@
<div class="list-group"> <div class="list-group">
<a href="#/domains/{{ domain.id }}" class="list-group-item" ng-repeat="domain in certificate.domains">{{ domain.name }}</a> <a href="#/domains/{{ domain.id }}" class="list-group-item" ng-repeat="domain in certificate.domains">{{ domain.name }}</a>
</div> </div>
<h4 ng-show="certificate.accounts.total">ARNs</h4> <h4 ng-show="certificate.destinations.total">ARNs</h4>
<ul class="list-group"> <ul class="list-group">
<li class="list-group-item" ng-repeat="arn in certificate.arns">{{ arn }}</li> <li class="list-group-item" ng-repeat="arn in certificate.arns">{{ arn }}</li>
</ul> </ul>

View File

@ -1,8 +1,8 @@
angular.module('lemur'). angular.module('lemur').
filter('titleCase', function () { filter('titleCase', function () {
return function (str) { return function (str) {
return (str === undefined || str === null) ? '' : str.replace(/_|-/, ' ').replace(/\w\S*/g, function (txt) { return (str === undefined || str === null) ? '' : str.replace(/([A-Z])/g, ' $1').replace(/^./, function (txt) {
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); return txt.toUpperCase();
}); });
}; };
}); });

View File

@ -0,0 +1,40 @@
'use strict';
angular.module('lemur')
.controller('DestinationsCreateController', function ($scope, $modalInstance, PluginService, DestinationService, LemurRestangular){
$scope.destination = LemurRestangular.restangularizeElement(null, {}, 'destinations');
PluginService.get('destination').then(function (plugins) {
$scope.plugins = plugins;
});
$scope.save = function (destination) {
DestinationService.create(destination).then(function () {
$modalInstance.close();
});
};
$scope.cancel = function () {
$modalInstance.dismiss('cancel');
};
})
.controller('DestinationsEditController', function ($scope, $modalInstance, DestinationService, DestinationApi, PluginService, editId) {
DestinationApi.get(editId).then(function (destination) {
$scope.destination = destination;
});
PluginService.get('destination').then(function (plugins) {
$scope.plugins = plugins;
});
$scope.save = function (destination) {
DestinationService.update(destination).then(function () {
$modalInstance.close();
});
}
$scope.cancel = function () {
$modalInstance.dismiss('cancel');
};
});

View File

@ -0,0 +1,56 @@
<div class="modal-header">
<div class="modal-title">
<h3 class="modal-header"><span ng-show="!destination.fromServer">Create</span><span ng-show="destination.fromServer">Edit</span> Destination <span class="text-muted"><small>oh the places you will go!</small></span></h3>
</div>
<div class="modal-body">
<form name="createForm" class="form-horizontal" role="form" novalidate>
<div class="form-group"
ng-class="{'has-error': createForm.label.$invalid, 'has-success': !createForm.label.$invalid&&createForm.label.$dirty}">
<label class="control-label col-sm-2">
Label
</label>
<div class="col-sm-10">
<input name="label" ng-model="destination.label" placeholder="Label" class="form-control" required/>
<p ng-show="createForm.label.$invalid && !createForm.label.$pristine" class="help-block">You must enter an destination label</p>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2">
Description
</label>
<div class="col-sm-10">
<textarea name="comments" ng-model="destination.description" placeholder="Something elegant" class="form-control" ></textarea>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2">
Plugin
</label>
<div class="col-sm-10">
<select class="form-control" ng-model="destination.plugin" ng-options="plugin.title for plugin in plugins" required></select>
</div>
</div>
<div class="form-group" ng-repeat="item in destination.plugin.pluginOptions">
<ng-form name="subForm" class="form-horizontal" role="form" novalidate>
<div ng-class="{'has-error': subForm.sub.$invalid, 'has-success': !subForm.sub.$invalid&&subForm.sub.$dirty}">
<label class="control-label col-sm-2">
{{ item.name | titleCase }}
</label>
<div class="col-sm-10">
<input name="sub" ng-if="item.type == 'int'" type="number" ng-pattern="/^[0-9]{12,12}$/" class="form-control" ng-model="item.value"/>
<select name="sub" ng-if="item.type == 'select'" class="form-control" ng-options="i for i in item.available" ng-model="item.value"></select>
<input name="sub" ng-if="item.type == 'bool'" class="form-control" type="checkbox" ng-model="item.value">
<input name="sub" ng-if="item.type == 'str'" type="text" class="form-control" ng-model="item.value"/>
<p ng-show="subForm.sub.$invalid && !subForm.sub.$pristine" class="help-block">{{ item.helpMessage }}</p>
</div>
</div>
</ng-form>
</div>
</form>
</div>
<div class="modal-footer">
<button ng-click="save(destination)" type="submit" ng-disabled="createForm.$invalid" class="btn btn-primary">Save</button>
<button ng-click="cancel()" class="btn btn-danger">Cancel</button>
</div>
</div>

View File

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

View File

@ -0,0 +1,85 @@
'use strict';
angular.module('lemur')
.config(function config($routeProvider) {
$routeProvider.when('/destinations', {
templateUrl: '/angular/destinations/view/view.tpl.html',
controller: 'DestinationsViewController'
});
})
.controller('DestinationsViewController', function ($scope, $modal, DestinationApi, DestinationService, ngTableParams, toaster) {
$scope.filter = {};
$scope.destinationsTable = 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) {
DestinationApi.getList(params.url()).then(
function (data) {
params.total(data.total);
$defer.resolve(data);
}
);
}
});
$scope.remove = function (destination) {
destination.remove().then(
function () {
$scope.destinationsTable.reload();
},
function (response) {
toaster.pop({
type: 'error',
title: 'Opps',
body: 'I see what you did there' + response.data.message
});
}
);
};
$scope.edit = function (destinationId) {
var modalInstance = $modal.open({
animation: true,
templateUrl: '/angular/destinations/destination/destination.tpl.html',
controller: 'DestinationsEditController',
size: 'lg',
resolve: {
editId: function () {
return destinationId;
}
}
});
modalInstance.result.then(function () {
$scope.destinationsTable.reload();
});
};
$scope.create = function () {
var modalInstance = $modal.open({
animation: true,
controller: 'DestinationsCreateController',
templateUrl: '/angular/destinations/destination/destination.tpl.html',
size: 'lg'
});
modalInstance.result.then(function () {
$scope.destinationsTable.reload();
});
};
$scope.toggleFilter = function (params) {
params.settings().$scope.show_filter = !params.settings().$scope.show_filter;
};
});

View File

@ -0,0 +1,47 @@
<div class="row">
<div class="col-md-12">
<h2 class="featurette-heading">Destinations
<span class="text-muted"><small>oh the places you will go</small></span></h2>
<div class="panel panel-default">
<div class="panel-heading">
<div class="btn-group pull-right">
<button ng-click="create()" class="btn btn-primary">Create</button>
</div>
<div class="btn-group">
<button ng-click="toggleFilter(destinationsTable)" class="btn btn-default">Filter</button>
</div>
<div class="clearfix"></div>
</div>
<div class="table-responsive">
<table ng-table="destinationsTable" class="table table-striped" show-filter="false" template-pagination="angular/pager.html" >
<tbody>
<tr ng-repeat="destination in $data track by $index">
<td data-title="'Label'" sortable="'label'" filter="{ 'label': 'text' }">
<ul class="list-unstyled">
<li>{{ destination.label }}</li>
<li><span class="text-muted">{{ destination.description }}</span></li>
</ul>
</td>
<td data-title="'Plugin'">
<ul class="list-unstyled">
<li>{{ destination.plugin.title }}</li>
<li><span class="text-muted">{{ destination.plugin.description }}</span></li>
</ul>
</td>
<td data-title="''">
<div class="btn-group-vertical pull-right">
<button tooltip="Edit Destination" ng-click="edit(destination.id)" class="btn btn-sm btn-info">
Edit
</button>
<button tooltip="Delete Destination" ng-click="remove(destination)" type="button" class="btn btn-sm btn-danger pull-left">
Remove
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>

View File

@ -1,6 +0,0 @@
<div tabindex="-1" role="dialog" class="modal fade" ng-class="{in: animate}" ng-style="{'z-index': 1050 + index*10, display: 'block'}">
<div class="modal-dialog-center">
<div modal-transclude>
</div>
</div>
</div>

View File

@ -2,29 +2,37 @@
angular.module('lemur') angular.module('lemur')
.config(function config($routeProvider) { .controller('RolesEditController', function ($scope, $modalInstance, RoleApi, RoleService, UserService, editId) {
$routeProvider.when('/roles/create', { RoleApi.get(editId).then(function (role) {
templateUrl: '/angular/roles/role/role.tpl.html',
controller: 'RoleCreateController'
});
$routeProvider.when('/roles/:id/edit', {
templateUrl: '/angular/roles/role/role.tpl.html',
controller: 'RoleEditController'
});
})
.controller('RoleEditController', function ($scope, $routeParams, RoleApi, RoleService, UserService) {
RoleApi.get($routeParams.id).then(function (role) {
$scope.role = role; $scope.role = role;
RoleService.getUsers(role); RoleService.getUsers(role);
}); });
$scope.save = RoleService.update; $scope.save = function (role) {
RoleService.update(role).then(function () {
$modalInstance.close();
});
};
$scope.cancel = function () {
$modalInstance.dismiss('cancel');
};
$scope.userService = UserService; $scope.userService = UserService;
$scope.roleService = RoleService; $scope.roleService = RoleService;
}) })
.controller('RoleCreateController', function ($scope, RoleApi, RoleService, UserService, LemurRestangular ) { .controller('RolesCreateController', function ($scope,$modalInstance, RoleApi, RoleService, UserService, LemurRestangular) {
$scope.role = LemurRestangular.restangularizeElement(null, {}, 'roles'); $scope.role = LemurRestangular.restangularizeElement(null, {}, 'roles');
$scope.userService = UserService; $scope.userService = UserService;
$scope.save = RoleService.create;
$scope.save = function (role) {
RoleService.create(role).then(function () {
$modalInstance.close();
});
};
$scope.cancel = function () {
$modalInstance.dismiss('cancel');
};
}); });

View File

@ -1,12 +1,8 @@
<h2 class="featurette-heading"><span ng-show="!role.fromServer">Create</span><span ng-show="role.fromServer">Edit</span> Role <span class="text-muted"><small>The nail that sticks out farthest gets hammered the hardest <div class="modal-header">
</small></span></h2> <div class="modal-title">
<div class="panel panel-default"> <h3 class="modal-header"><span ng-show="!role.fromServer">Create</span><span ng-show="role.fromServer">Edit</span> Role <span class="text-muted"><small>The nail that sticks out farthest gets hammered the hardest</small></span></h3>
<div class="panel-heading">
<button ng-click="roleService.loadPassword(role)" class="btn btn-warning">Show Credentials</button>
<a href="/#/roles" class="btn btn-danger pull-right">Cancel</a>
<div class="clearfix"></div>
</div> </div>
<div class="panel-body"> <div class="modal-body">
<form name="createForm" class="form-horizontal" ng-submit="save(role)" role="form" novalidate> <form name="createForm" class="form-horizontal" ng-submit="save(role)" role="form" novalidate>
<div class="form-group" <div class="form-group"
ng-class="{'has-error': createForm.name.$invalid, 'has-success': !createForm.name.$invalid&&createForm.name.$dirty}"> ng-class="{'has-error': createForm.name.$invalid, 'has-success': !createForm.name.$invalid&&createForm.name.$dirty}">
@ -77,9 +73,10 @@
</div> </div>
</div> </div>
</form> </form>
<div class="modal-footer">
<button ng-click="roleService.loadPassword(role)" class="btn btn-warning pull-left">Show Credentials</button>
<button ng-click="save(role)" type="submit" ng-disabled="createForm.$invalid" class="btn btn-primary">Save</button>
<button ng-click="cancel()" class="btn btn-danger">Cancel</button>
</div> </div>
<div class="panel-footer">
<button ng-click="save(role)" class="btn btn-success pull-right">Save</button>
<div class="clearfix"></div>
</div> </div>
</div> </div>

View File

@ -38,7 +38,7 @@ angular.module('lemur')
}; };
RoleService.create = function (role) { RoleService.create = function (role) {
RoleApi.post(role).then( return RoleApi.post(role).then(
function () { function () {
toaster.pop({ toaster.pop({
type: 'success', type: 'success',
@ -57,7 +57,7 @@ angular.module('lemur')
}; };
RoleService.update = function (role) { RoleService.update = function (role) {
role.put().then( return role.put().then(
function () { function () {
toaster.pop({ toaster.pop({
type: 'success', type: 'success',

View File

@ -9,7 +9,7 @@ angular.module('lemur')
}); });
}) })
.controller('RolesViewController', function ($scope, RoleApi, RoleService, ngTableParams) { .controller('RolesViewController', function ($scope, $modal, RoleApi, RoleService, ngTableParams) {
$scope.filter = {}; $scope.filter = {};
$scope.rolesTable = new ngTableParams({ $scope.rolesTable = new ngTableParams({
page: 1, // show first page page: 1, // show first page
@ -39,4 +39,38 @@ angular.module('lemur')
params.settings().$scope.show_filter = !params.settings().$scope.show_filter; params.settings().$scope.show_filter = !params.settings().$scope.show_filter;
}; };
$scope.edit = function (roleId) {
var modalInstance = $modal.open({
animation: true,
templateUrl: '/angular/roles/role/role.tpl.html',
controller: 'RolesEditController',
size: 'lg',
resolve: {
editId: function () {
return roleId;
}
}
});
modalInstance.result.then(function () {
$scope.rolesTable.reload();
});
};
$scope.create = function () {
var modalInstance = $modal.open({
animation: true,
controller: 'RolesCreateController',
templateUrl: '/angular/roles/role/role.tpl.html',
size: 'lg'
});
modalInstance.result.then(function () {
$scope.rolesTable.reload();
});
};
}); });

View File

@ -5,7 +5,7 @@
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<div class="btn-group pull-right"> <div class="btn-group pull-right">
<a data-placement="left" data-title="Create Role" bs-tooltip href="#/roles/create" class="btn btn-primary">Create</a> <button ng-click="create()" class="btn btn-primary">Create</button>
</div> </div>
<div class="btn-group"> <div class="btn-group">
<button ng-click="toggleFilter(rolesTable)" class="btn btn-default">Filter</button> <button ng-click="toggleFilter(rolesTable)" class="btn btn-default">Filter</button>
@ -24,9 +24,9 @@
</td> </td>
<td data-title="''"> <td data-title="''">
<div class="btn-group-vertical pull-right"> <div class="btn-group-vertical pull-right">
<a href="#/roles/{{ role.id }}/edit" class="btn btn-sm btn-info"> <button ng-click="edit(role.id)" class="btn btn-sm btn-info">
Edit Edit
</a> </button>
<a ng-click="remove(role)" class="btn btn-sm btn-danger"> <a ng-click="remove(role)" class="btn btn-sm btn-danger">
Remove Remove
</a> </a>

View File

@ -50,7 +50,7 @@ angular.module('lemur')
}; };
UserService.create = function (user) { UserService.create = function (user) {
UserApi.post(user).then( return UserApi.post(user).then(
function () { function () {
toaster.pop({ toaster.pop({
type: 'success', type: 'success',
@ -69,7 +69,7 @@ angular.module('lemur')
}; };
UserService.update = function (user) { UserService.update = function (user) {
user.put().then( return user.put().then(
function () { function () {
toaster.pop({ toaster.pop({
type: 'success', type: 'success',

View File

@ -2,19 +2,8 @@
angular.module('lemur') angular.module('lemur')
.config(function config($routeProvider) { .controller('UsersEditController', function ($scope, $modalInstance, UserApi, UserService, RoleService, editId) {
$routeProvider.when('/users/create', { UserApi.get(editId).then(function (user) {
templateUrl: '/angular/users/user/user.tpl.html',
controller: 'UsersCreateController'
});
$routeProvider.when('/users/:id/edit', {
templateUrl: '/angular/users/user/user.tpl.html',
controller: 'UsersEditController'
});
})
.controller('UsersEditController', function ($scope, $routeParams, UserApi, UserService, RoleService) {
UserApi.get($routeParams.id).then(function (user) {
UserService.getRoles(user); UserService.getRoles(user);
$scope.user = user; $scope.user = user;
}); });
@ -24,15 +13,36 @@ angular.module('lemur')
$scope.rolePage = 1; $scope.rolePage = 1;
$scope.save = function (user) {
UserService.update(user).then(function () {
$modalInstance.close();
});
}
$scope.cancel = function () {
$modalInstance.dismiss('cancel');
};
$scope.loadMoreRoles = function () { $scope.loadMoreRoles = function () {
$scope.rolePage += 1; $scope.rolePage += 1;
UserService.loadMoreRoles($scope.user, $scope.rolePage); UserService.loadMoreRoles($scope.user, $scope.rolePage);
}; };
}) })
.controller('UsersCreateController', function ($scope, UserService, LemurRestangular, RoleService) { .controller('UsersCreateController', function ($scope, $modalInstance, UserService, LemurRestangular, RoleService) {
$scope.user = LemurRestangular.restangularizeElement(null, {}, 'users'); $scope.user = LemurRestangular.restangularizeElement(null, {}, 'users');
$scope.save = UserService.create; $scope.save = UserService.create;
$scope.roleService = RoleService; $scope.roleService = RoleService;
$scope.create = function (user) {
UserService.create(user).then(function () {
$modalInstance.close();
});
};
$scope.cancel = function () {
$modalInstance.dismiss('cancel');
};
}); });

View File

@ -1,11 +1,8 @@
<h2 class="featurette-heading"><span ng-show="!user.fromServer">Create</span><span ng-show="user.fromServer">Edit</span> User <span class="text-muted"><small>what was your name again? <div class="modal-header">
</small></span></h2> <div class="modal-title">
<div class="panel panel-default"> <h3 class="modal-header"><span ng-show="!user.fromServer">Create</span><span ng-show="user.fromServer">Edit</span> User <span class="text-muted"><small>what was your name again?</small></span></h3>
<div class="panel-heading">
<a href="#/users" class="btn btn-danger pull-right">Cancel</a>
<div class="clearfix"></div>
</div> </div>
<div class="panel-body"> <div class="modal-body">
<form name="createForm" class="form-horizontal" role="form" novalidate> <form name="createForm" class="form-horizontal" role="form" novalidate>
<div class="form-group" <div class="form-group"
ng-class="{'has-error': createForm.username.$invalid, 'has-success': !createForm.username.$invalid&&createForm.username.$dirty}"> ng-class="{'has-error': createForm.username.$invalid, 'has-success': !createForm.username.$invalid&&createForm.username.$dirty}">
@ -43,7 +40,7 @@
</label> </label>
<div class="col-sm-10"> <div class="col-sm-10">
<div class="checkbox col-sm-10"> <div class="checkbox col-sm-10">
<input ng-model="user.active" type="checkbox" value="true"> <switch ng-model="user.active" id="active" name="active" ng-init="user.active=true" class="green small"></switch>
</div> </div>
</div> </div>
</div> </div>
@ -81,9 +78,9 @@
</div> </div>
</div> </div>
</form> </form>
<div class="modal-footer">
<button ng-click="save(user)" type="submit" ng-disabled="createForm.$invalid" class="btn btn-primary">Save</button>
<button ng-click="cancel()" class="btn btn-danger">Cancel</button>
</div> </div>
<div class="panel-footer">
<button ng-click="save(user)" class="btn btn-success pull-right">Save</button>
<div class="clearfix"></div>
</div> </div>
</div> </div>

View File

@ -9,7 +9,7 @@ angular.module('lemur')
}); });
}) })
.controller('UsersViewController', function ($scope, UserApi, UserService, ngTableParams) { .controller('UsersViewController', function ($scope, $modal, UserApi, UserService, ngTableParams) {
$scope.filter = {}; $scope.filter = {};
$scope.usersTable = new ngTableParams({ $scope.usersTable = new ngTableParams({
page: 1, // show first page page: 1, // show first page
@ -36,6 +36,39 @@ angular.module('lemur')
}); });
}; };
$scope.edit = function (userId) {
var modalInstance = $modal.open({
animation: true,
templateUrl: '/angular/users/user/user.tpl.html',
controller: 'UsersEditController',
size: 'lg',
resolve: {
editId: function () {
return userId;
}
}
});
modalInstance.result.then(function () {
$scope.usersTable.reload();
});
};
$scope.create = function () {
var modalInstance = $modal.open({
animation: true,
controller: 'UsersCreateController',
templateUrl: '/angular/users/user/user.tpl.html',
size: 'lg'
});
modalInstance.result.then(function () {
$scope.usersTable.reload();
});
};
$scope.toggleFilter = function (params) { $scope.toggleFilter = function (params) {
params.settings().$scope.show_filter = !params.settings().$scope.show_filter; params.settings().$scope.show_filter = !params.settings().$scope.show_filter;
}; };

View File

@ -5,7 +5,7 @@
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<div class="btn-group pull-right"> <div class="btn-group pull-right">
<a href="#/users/create" class="btn btn-primary">Create</a> <button ng-click="create()" class="btn btn-primary">Create</button>
</div> </div>
<div class="btn-group"> <div class="btn-group">
<button ng-click="toggleFilter(usersTable)" class="btn btn-default">Filter</button> <button ng-click="toggleFilter(usersTable)" class="btn btn-default">Filter</button>
@ -29,9 +29,9 @@
</td> </td>
<td data-title="''"> <td data-title="''">
<div class="btn-group-vertical pull-right"> <div class="btn-group-vertical pull-right">
<a tooltip="Edit User" href="#/users/{{ user.id }}/edit" class="btn btn-sm btn-info"> <button tooltip="Edit User" ng-click="edit(user.id)" class="btn btn-sm btn-info">
Edit Edit
</a> </button>
</div> </div>
</td> </td>
</tr> </tr>

View File

@ -1,23 +1,12 @@
<div> <div>
<div class="panel panel-default"> <div class="modal-body">
<div class="panel-heading">
<ul class="steps-indicator steps-{{steps.length}}" ng-if="!hideIndicators">
<li ng-class="{default: !step.completed && !step.selected, current: step.selected && !step.completed, done: step.completed && !step.selected, editing: step.selected && step.completed}" ng-repeat="step in steps">
<a ng-click="goTo(step)">{{step.title || step.wzTitle}}</a>
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="panel-body">
<div class="steps" ng-transclude></div> <div class="steps" ng-transclude></div>
</div> </div>
<div class="panel-footer"> <div class="modal-footer">
<input ng-hide="currentStepNumber() == 1" class="btn btn-default pull-left" type="submit" wz-previous value="Previous" /> <input ng-hide="currentStepNumber() == 1" class="btn btn-default pull-left" type="submit" wz-previous value="Previous" />
<input ng-show="currentStepNumber() != steps.length" class="btn btn-default pull-right" type="submit" wz-next value="Next" /> <input ng-show="currentStepNumber() != steps.length" class="btn btn-default pull-right" type="submit" wz-next value="Next" />
<button ng-show="currentStepNumber() == steps.length" class="btn btn-success pull-right" type="submit" wz-next> <input ng-show="!context.loading" class="btn btn-success pull-right" type="submit" wz-finish value="Create" />
Create <button ng-show="context.loading" class="btn btn-success pull-right disabled"><wave-spinner></wave-spinner></button>
</button>
<div class="clearfix"></div> <div class="clearfix"></div>
</div> </div>
</div>
</div> </div>

View File

@ -55,7 +55,7 @@
<li data-match-route="/domains"><a href="/#/domains">Domains</a></li> <li data-match-route="/domains"><a href="/#/domains">Domains</a></li>
<li><a href="/#/roles">Roles</a></li> <li><a href="/#/roles">Roles</a></li>
<li><a href="/#/users">Users</a></li> <li><a href="/#/users">Users</a></li>
<li><a href="/#/accounts">Accounts</a></li> <li><a href="/#/destinations">Destinations</a></li>
</ul> </ul>
<ul ng-show="!currentUser.username" class="nav navbar-nav navbar-right"> <ul ng-show="!currentUser.username" class="nav navbar-nav navbar-right">
<li><a href="/#/login">Login</a></li> <li><a href="/#/login">Login</a></li>

View File

@ -159,3 +159,13 @@ a {
margin-top: 10px; margin-top: 10px;
} }
.wave-spinner {
margin: 5px auto !important;
width: 40px !important;
height: 12px !important;
}
.wave-spinner>div {
background-color: #FFFFFF !important;
}

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}

View File

@ -106,8 +106,11 @@ setup(
'lemur = lemur.manage:main', 'lemur = lemur.manage:main',
], ],
'lemur.plugins': [ 'lemur.plugins': [
'verisign = lemur.plugins.lemur_verisign.plugin:VerisignPlugin', 'verisign_issuer = lemur.plugins.lemur_verisign.plugin:VerisignIssuerPlugin',
'cloudca = lemur.plugins.lemur_cloudca.plugin:CloudCAPlugin', 'cloudca_issuer = lemur.plugins.lemur_cloudca.plugin:CloudCAIssuerPlugin',
'cloudca_source = lemur.plugins.lemur_cloudca.plugin:CloudCASourcePlugin'
'aws_destination = lemur.plugins.lemur_aws.plugin:AWSDestinationPlugin',
'aws_source = lemur.plugins.lemur_aws.plugin:AWSSourcePlugin'
], ],
}, },
classifiers=[ classifiers=[