initial commit
This commit is contained in:
69
lemur/__init__.py
Normal file
69
lemur/__init__.py
Normal file
@ -0,0 +1,69 @@
|
||||
"""
|
||||
.. module: lemur
|
||||
:platform: Unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
|
||||
|
||||
"""
|
||||
from flask import jsonify
|
||||
|
||||
from lemur import factory
|
||||
|
||||
from lemur.users.views import mod as users
|
||||
from lemur.roles.views import mod as roles
|
||||
from lemur.auth.views import mod as auth
|
||||
from lemur.domains.views import mod as domains
|
||||
from lemur.elbs.views import mod as elbs
|
||||
from lemur.accounts.views import mod as accounts
|
||||
from lemur.authorities.views import mod as authorities
|
||||
from lemur.listeners.views import mod as listeners
|
||||
from lemur.certificates.views import mod as certificates
|
||||
from lemur.status.views import mod as status
|
||||
|
||||
LEMUR_BLUEPRINTS = (
|
||||
users,
|
||||
roles,
|
||||
auth,
|
||||
domains,
|
||||
elbs,
|
||||
accounts,
|
||||
authorities,
|
||||
listeners,
|
||||
certificates,
|
||||
status
|
||||
)
|
||||
|
||||
def create_app(config=None):
|
||||
app = factory.create_app(app_name=__name__, blueprints=LEMUR_BLUEPRINTS, config=config)
|
||||
configure_hook(app)
|
||||
return app
|
||||
|
||||
|
||||
def configure_hook(app):
|
||||
"""
|
||||
|
||||
:param app:
|
||||
:return:
|
||||
"""
|
||||
from flask.ext.principal import PermissionDenied
|
||||
from lemur.decorators import crossdomain
|
||||
if app.config.get('CORS'):
|
||||
@app.after_request
|
||||
@crossdomain(origin="http://localhost:3000", methods=['PUT', 'HEAD', 'GET', 'POST', 'OPTIONS', 'DELETE'])
|
||||
def after(response):
|
||||
return response
|
||||
|
||||
@app.errorhandler(PermissionDenied)
|
||||
def handle_invalid_usage(error):
|
||||
response = {'message': 'You are not allow to access this resource'}
|
||||
response.status_code = 403
|
||||
return response
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
0
lemur/accounts/__init__.py
Normal file
0
lemur/accounts/__init__.py
Normal file
29
lemur/accounts/models.py
Normal file
29
lemur/accounts/models.py
Normal file
@ -0,0 +1,29 @@
|
||||
"""
|
||||
.. 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
|
||||
|
112
lemur/accounts/service.py
Normal file
112
lemur/accounts/service.py
Normal file
@ -0,0 +1,112 @@
|
||||
"""
|
||||
.. 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)
|
||||
|
300
lemur/accounts/views.py
Normal file
300
lemur/accounts/views.py
Normal file
@ -0,0 +1,300 @@
|
||||
"""
|
||||
.. module: lemur.accounts.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.accounts import service
|
||||
|
||||
from lemur.auth.service import AuthenticatedResource
|
||||
from lemur.auth.permissions import admin_permission
|
||||
from lemur.common.utils import paginated_parser, marshal_items
|
||||
|
||||
|
||||
mod = Blueprint('accounts', __name__)
|
||||
api = Api(mod)
|
||||
|
||||
|
||||
FIELDS = {
|
||||
'accountNumber': fields.Integer(attribute='account_number'),
|
||||
'label': fields.String,
|
||||
'comments': fields.String(attribute='notes'),
|
||||
'id': fields.Integer,
|
||||
}
|
||||
|
||||
|
||||
class AccountsList(AuthenticatedResource):
|
||||
""" Defines the 'accounts' endpoint """
|
||||
def __init__(self):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(AccountsList, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
def get(self):
|
||||
"""
|
||||
.. http:get:: /accounts
|
||||
|
||||
The current account list
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /accounts 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
|
||||
}
|
||||
|
||||
:query sortBy: field to sort on
|
||||
:query sortDir: acs or desc
|
||||
:query page: int. default is 1
|
||||
:query filter: key value pair. format is k=v;
|
||||
:query limit: limit number. default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
"""
|
||||
parser = paginated_parser.copy()
|
||||
args = parser.parse_args()
|
||||
return service.render(args)
|
||||
|
||||
@admin_permission.require(http_exception=403)
|
||||
@marshal_items(FIELDS)
|
||||
def post(self):
|
||||
"""
|
||||
.. http:post:: /accounts
|
||||
|
||||
Creates a new account
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /accounts HTTP/1.1
|
||||
Host: example.com
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
{
|
||||
"accountNumber": 11111111111,
|
||||
"label": "account1,
|
||||
"comments": "this is a thing"
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"accountNumber": 11111111111,
|
||||
"label": "account1",
|
||||
"comments": "this is a thing"
|
||||
}
|
||||
|
||||
:arg accountNumber: aws account number
|
||||
:arg label: human readable account label
|
||||
:arg comments: some description about the account
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
: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('comments', type=str, location='json')
|
||||
|
||||
args = self.reqparse.parse_args()
|
||||
return service.create(args['account_number'], args['label'], args['comments'])
|
||||
|
||||
|
||||
class Accounts(AuthenticatedResource):
|
||||
def __init__(self):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(Accounts, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
def get(self, account_id):
|
||||
"""
|
||||
.. http:get:: /accounts/1
|
||||
|
||||
Get a specific account
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /accounts/1 HTTP/1.1
|
||||
Host: example.com
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"accountNumber": 11111111111,
|
||||
"label": "account1",
|
||||
"comments": "this is a thing"
|
||||
}
|
||||
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
"""
|
||||
return service.get(account_id)
|
||||
|
||||
@admin_permission.require(http_exception=403)
|
||||
@marshal_items(FIELDS)
|
||||
def put(self, account_id):
|
||||
"""
|
||||
.. http:post:: /accounts/1
|
||||
|
||||
Updates an account
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /accounts/1 HTTP/1.1
|
||||
Host: example.com
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
{
|
||||
"accountNumber": 11111111111,
|
||||
"label": "labelChanged,
|
||||
"comments": "this is a thing"
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"accountNumber": 11111111111,
|
||||
"label": "labelChanged",
|
||||
"comments": "this is a thing"
|
||||
}
|
||||
|
||||
:arg accountNumber: aws account number
|
||||
:arg label: human readable account label
|
||||
:arg comments: some description about the account
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
: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('comments', type=str, location='json')
|
||||
|
||||
args = self.reqparse.parse_args()
|
||||
return service.update(account_id, args['account_number'], args['label'], args['comments'])
|
||||
|
||||
@admin_permission.require(http_exception=403)
|
||||
def delete(self, account_id):
|
||||
service.delete(account_id)
|
||||
return {'result': True}
|
||||
|
||||
|
||||
|
||||
class CertificateAccounts(AuthenticatedResource):
|
||||
""" Defines the 'certificate/<int:certificate_id/accounts'' endpoint """
|
||||
def __init__(self):
|
||||
super(CertificateAccounts, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
def get(self, certificate_id):
|
||||
"""
|
||||
.. http:get:: /certificates/1/accounts
|
||||
|
||||
The current account list for a given certificates
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /certificates/1/accounts 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
|
||||
}
|
||||
|
||||
:query sortBy: field to sort on
|
||||
:query sortDir: acs or desc
|
||||
:query page: int. default is 1
|
||||
:query filter: key value pair. format is k=v;
|
||||
:query limit: limit number. default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
"""
|
||||
parser = paginated_parser.copy()
|
||||
args = parser.parse_args()
|
||||
args['certificate_id'] = certificate_id
|
||||
return service.render(args)
|
||||
|
||||
|
||||
api.add_resource(AccountsList, '/accounts', endpoint='accounts')
|
||||
api.add_resource(Accounts, '/accounts/<int:account_id>', endpoint='account')
|
||||
api.add_resource(CertificateAccounts, '/certificates/<int:certificate_id>/accounts', endpoint='certificateAccounts')
|
||||
|
0
lemur/analyze/__init__.py
Normal file
0
lemur/analyze/__init__.py
Normal file
62
lemur/analyze/service.py
Normal file
62
lemur/analyze/service.py
Normal file
@ -0,0 +1,62 @@
|
||||
"""
|
||||
.. module: lemur.analyze.service
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
#def analyze(endpoints, truststores):
|
||||
# results = {"headings": ["Endpoint"],
|
||||
# "results": [],
|
||||
# "time": datetime.now().strftime("#Y%m%d %H:%M:%S")}
|
||||
#
|
||||
# for store in truststores:
|
||||
# results['headings'].append(os.path.basename(store))
|
||||
#
|
||||
# for endpoint in endpoints:
|
||||
# result_row = [endpoint]
|
||||
# for store in truststores:
|
||||
# result = {'details': []}
|
||||
#
|
||||
# tests = []
|
||||
# for region, ip in REGIONS.items():
|
||||
# try:
|
||||
# domain = dns.name.from_text(endpoint)
|
||||
# if not domain.is_absolute():
|
||||
# domain = domain.concatenate(dns.name.root)
|
||||
#
|
||||
# my_resolver = dns.resolver.Resolver()
|
||||
# my_resolver.nameservers = [ip]
|
||||
# answer = my_resolver.query(domain)
|
||||
#
|
||||
# #force the testing of regional enpoints by changing the dns server
|
||||
# response = requests.get('https://' + str(answer[0]), verify=store)
|
||||
# tests.append('pass')
|
||||
# result['details'].append("{}: SSL testing completed without errors".format(region))
|
||||
#
|
||||
# except SSLError as e:
|
||||
# log.debug(e)
|
||||
# if 'hostname' in str(e):
|
||||
# tests.append('pass')
|
||||
# result['details'].append("{}: This test passed ssl negotiation but failed hostname verification becuase the hostname is not included in the certificate".format(region))
|
||||
# elif 'certificate verify failed' in str(e):
|
||||
# tests.append('fail')
|
||||
# result['details'].append("{}: This test failed to verify the SSL certificate".format(region))
|
||||
# else:
|
||||
# tests.append('fail')
|
||||
# result['details'].append("{}: {}".format(region, str(e)))
|
||||
#
|
||||
# except Exception as e:
|
||||
# log.debug(e)
|
||||
# tests.append('fail')
|
||||
# result['details'].append("{}: {}".format(region, str(e)))
|
||||
#
|
||||
# #any failing tests fails the whole endpoint
|
||||
# if 'fail' in tests:
|
||||
# result['test'] = 'fail'
|
||||
# else:
|
||||
# result['test'] = 'pass'
|
||||
#
|
||||
# result_row.append(result)
|
||||
# results['results'].append(result_row)
|
||||
# return results
|
||||
#
|
0
lemur/auth/__init__.py
Normal file
0
lemur/auth/__init__.py
Normal file
62
lemur/auth/permissions.py
Normal file
62
lemur/auth/permissions.py
Normal file
@ -0,0 +1,62 @@
|
||||
"""
|
||||
.. module: permissions
|
||||
:platform: Unix
|
||||
:synopsis: This module defines all the permission used within Lemur
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
from functools import partial
|
||||
from collections import namedtuple
|
||||
|
||||
from flask.ext.principal import Permission, RoleNeed
|
||||
|
||||
# Permissions
|
||||
operator_permission = Permission(RoleNeed('operator'))
|
||||
admin_permission = Permission(RoleNeed('secops@netflix.com'))
|
||||
|
||||
CertificateCreator = namedtuple('certificate', ['method', 'value'])
|
||||
CertificateCreatorNeed = partial(CertificateCreator, 'certificateView')
|
||||
|
||||
CertificateOwner = namedtuple('certificate', ['method', 'value'])
|
||||
CertificateOwnerNeed = partial(CertificateOwner, 'certificateView')
|
||||
|
||||
|
||||
class ViewKeyPermission(Permission):
|
||||
def __init__(self, role_id, certificate_id):
|
||||
c_need = CertificateCreatorNeed(unicode(certificate_id))
|
||||
o_need = CertificateOwnerNeed(unicode(role_id))
|
||||
super(ViewKeyPermission, self).__init__(o_need, c_need, RoleNeed('admin'))
|
||||
|
||||
|
||||
class UpdateCertificatePermission(Permission):
|
||||
def __init__(self, role_id, certificate_id):
|
||||
c_need = CertificateCreatorNeed(unicode(certificate_id))
|
||||
o_need = CertificateOwnerNeed(unicode(role_id))
|
||||
super(UpdateCertificatePermission, self).__init__(o_need, c_need, RoleNeed('admin'))
|
||||
|
||||
|
||||
RoleUser = namedtuple('role', ['method', 'value'])
|
||||
ViewRoleCredentialsNeed = partial(RoleUser, 'roleView')
|
||||
|
||||
|
||||
class ViewRoleCredentialsPermission(Permission):
|
||||
def __init__(self, role_id):
|
||||
need = ViewRoleCredentialsNeed(unicode(role_id))
|
||||
super(ViewRoleCredentialsPermission, self).__init__(need, RoleNeed('admin'))
|
||||
|
||||
|
||||
AuthorityCreator = namedtuple('authority', ['method', 'value'])
|
||||
AuthorityCreatorNeed = partial(AuthorityCreator, 'authorityUse')
|
||||
|
||||
AuthorityOwner = namedtuple('authority', ['method', 'value'])
|
||||
AuthorityOwnerNeed = partial(AuthorityOwner, 'role')
|
||||
|
||||
|
||||
class AuthorityPermission(Permission):
|
||||
def __init__(self, authority_id, roles):
|
||||
needs = [RoleNeed('admin'), AuthorityCreatorNeed(unicode(authority_id))]
|
||||
for r in roles:
|
||||
needs.append(AuthorityOwnerNeed(unicode(r)))
|
||||
|
||||
super(AuthorityPermission, self).__init__(*needs)
|
188
lemur/auth/service.py
Normal file
188
lemur/auth/service.py
Normal file
@ -0,0 +1,188 @@
|
||||
"""
|
||||
.. module: lemur.auth.service
|
||||
:platform: Unix
|
||||
:synopsis: This module contains all of the authentication duties for
|
||||
lemur
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
|
||||
"""
|
||||
import jwt
|
||||
import json
|
||||
import base64
|
||||
import binascii
|
||||
from functools import wraps
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from flask import g, current_app, jsonify, request
|
||||
|
||||
from flask.ext.restful import Resource
|
||||
from flask.ext.principal import identity_loaded, RoleNeed, UserNeed
|
||||
|
||||
from flask.ext.principal import Identity, identity_changed
|
||||
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicNumbers
|
||||
|
||||
from lemur.users import service as user_service
|
||||
from lemur.auth.permissions import CertificateOwnerNeed, CertificateCreatorNeed, \
|
||||
AuthorityCreatorNeed, AuthorityOwnerNeed, ViewRoleCredentialsNeed
|
||||
|
||||
|
||||
def base64url_decode(data):
|
||||
if isinstance(data, unicode):
|
||||
data = str(data)
|
||||
|
||||
rem = len(data) % 4
|
||||
|
||||
if rem > 0:
|
||||
data += b'=' * (4 - rem)
|
||||
|
||||
return base64.urlsafe_b64decode(data)
|
||||
|
||||
|
||||
def base64url_encode(data):
|
||||
return base64.urlsafe_b64encode(data).replace(b'=', b'')
|
||||
|
||||
|
||||
def get_rsa_public_key(n, e):
|
||||
"""
|
||||
Retrieve an RSA public key based on a module and exponent as provided by the JWKS format.
|
||||
|
||||
:param n:
|
||||
:param e:
|
||||
:return: a RSA Public Key in PEM format
|
||||
"""
|
||||
n = int(binascii.hexlify(base64url_decode(n)), 16)
|
||||
e = int(binascii.hexlify(base64url_decode(e)), 16)
|
||||
pub = RSAPublicNumbers(e, n).public_key(default_backend())
|
||||
return pub.public_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PublicFormat.SubjectPublicKeyInfo
|
||||
)
|
||||
|
||||
|
||||
def create_token(user):
|
||||
"""
|
||||
Create a valid JWT for a given user, this token is then used to authenticate
|
||||
sessions until the token expires.
|
||||
|
||||
:param user:
|
||||
:return:
|
||||
"""
|
||||
expiration_delta = timedelta(days=int(current_app.config.get('TOKEN_EXPIRATION', 1)))
|
||||
payload = {
|
||||
'sub': user.id,
|
||||
'iat': datetime.now(),
|
||||
'exp': datetime.now() + expiration_delta
|
||||
}
|
||||
token = jwt.encode(payload, current_app.config['TOKEN_SECRET'])
|
||||
return token.decode('unicode_escape')
|
||||
|
||||
|
||||
def login_required(f):
|
||||
"""
|
||||
Validates the JWT and ensures that is has not expired.
|
||||
|
||||
:param f:
|
||||
:return:
|
||||
"""
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if not request.headers.get('Authorization'):
|
||||
response = jsonify(message='Missing authorization header')
|
||||
response.status_code = 401
|
||||
return response
|
||||
|
||||
token = request.headers.get('Authorization').split()[1]
|
||||
|
||||
try:
|
||||
payload = jwt.decode(token, current_app.config['TOKEN_SECRET'])
|
||||
except jwt.DecodeError:
|
||||
return dict(message='Token is invalid'), 403
|
||||
except jwt.ExpiredSignatureError:
|
||||
return dict(message='Token has expired'), 403
|
||||
except jwt.InvalidTokenError:
|
||||
return dict(message='Token is invalid'), 403
|
||||
|
||||
g.current_user = user_service.get(payload['sub'])
|
||||
|
||||
if not g.current_user.id:
|
||||
return dict(message='You are not logged in'), 403
|
||||
|
||||
# Tell Flask-Principal the identity changed
|
||||
identity_changed.send(current_app._get_current_object(), identity=Identity(g.current_user.id))
|
||||
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return decorated_function
|
||||
|
||||
|
||||
def fetch_token_header(token):
|
||||
"""
|
||||
Fetch the header out of the JWT token.
|
||||
|
||||
:param token:
|
||||
:return: :raise jwt.DecodeError:
|
||||
"""
|
||||
token = token.encode('utf-8')
|
||||
try:
|
||||
signing_input, crypto_segment = token.rsplit(b'.', 1)
|
||||
header_segment, payload_segment = signing_input.split(b'.', 1)
|
||||
except ValueError:
|
||||
raise jwt.DecodeError('Not enough segments')
|
||||
|
||||
try:
|
||||
return json.loads(base64url_decode(header_segment))
|
||||
except TypeError, binascii.Error:
|
||||
raise jwt.DecodeError('Invalid header padding')
|
||||
|
||||
|
||||
|
||||
@identity_loaded.connect
|
||||
def on_identity_loaded(sender, identity):
|
||||
"""
|
||||
Sets the identity of a given option, assigns additional permissions based on
|
||||
the role that the user is a part of.
|
||||
|
||||
:param sender:
|
||||
:param identity:
|
||||
"""
|
||||
# load the user
|
||||
user = user_service.get(identity.id)
|
||||
|
||||
# add the UserNeed to the identity
|
||||
identity.provides.add(UserNeed(identity.id))
|
||||
|
||||
# identity with the roles that the user provides
|
||||
if hasattr(user, 'roles'):
|
||||
for role in user.roles:
|
||||
identity.provides.add(CertificateOwnerNeed(unicode(role.id)))
|
||||
identity.provides.add(ViewRoleCredentialsNeed(unicode(role.id)))
|
||||
identity.provides.add(RoleNeed(role.name))
|
||||
|
||||
# apply ownership for authorities
|
||||
if hasattr(user, 'authorities'):
|
||||
for authority in user.authorities:
|
||||
identity.provides.add(AuthorityCreatorNeed(unicode(authority.id)))
|
||||
|
||||
# apply ownership of certificates
|
||||
if hasattr(user, 'certificates'):
|
||||
for certificate in user.certificates:
|
||||
identity.provides.add(CertificateCreatorNeed(unicode(certificate.id)))
|
||||
|
||||
g.user = user
|
||||
|
||||
|
||||
class AuthenticatedResource(Resource):
|
||||
"""
|
||||
Inherited by all resources that need to be protected by authentication.
|
||||
"""
|
||||
method_decorators = [login_required]
|
||||
|
||||
def __init__(self):
|
||||
super(AuthenticatedResource, self).__init__()
|
||||
|
||||
|
257
lemur/auth/views.py
Normal file
257
lemur/auth/views.py
Normal file
@ -0,0 +1,257 @@
|
||||
"""
|
||||
.. module: lemur.auth.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>
|
||||
"""
|
||||
import jwt
|
||||
import base64
|
||||
import requests
|
||||
|
||||
from flask import g, Blueprint, current_app, abort
|
||||
|
||||
from flask.ext.restful import reqparse, Resource, Api
|
||||
from flask.ext.principal import Identity, identity_changed
|
||||
|
||||
from lemur.common.crypto import unlock
|
||||
|
||||
from lemur.auth.permissions import admin_permission
|
||||
from lemur.users import service as user_service
|
||||
from lemur.roles import service as role_service
|
||||
from lemur.certificates import service as cert_service
|
||||
from lemur.auth.service import AuthenticatedResource, create_token, fetch_token_header, get_rsa_public_key
|
||||
|
||||
|
||||
mod = Blueprint('auth', __name__)
|
||||
api = Api(mod)
|
||||
|
||||
|
||||
class Login(Resource):
|
||||
"""
|
||||
Provides an endpoint for Lemur's basic authentication. It takes a username and password
|
||||
combination and returns a JWT token.
|
||||
|
||||
This token token is required for each API request and must be provided in the Authorization Header for the request.
|
||||
::
|
||||
|
||||
Authorization:Bearer <token>
|
||||
|
||||
Tokens have a set expiration date. You can inspect the token expiration be base64 decoding the token and inspecting
|
||||
it's contents.
|
||||
|
||||
.. note:: It is recommended that the token expiration is fairly short lived (hours not days). This will largely depend \
|
||||
on your uses cases but. It is important to not that there is currently no build in method to revoke a users token \
|
||||
and force re-authentication.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(Login, self).__init__()
|
||||
|
||||
def post(self):
|
||||
"""
|
||||
.. http:post:: /auth/login
|
||||
|
||||
Login with username:password
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /auth/login HTTP/1.1
|
||||
Host: example.com
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
{
|
||||
"username": "test",
|
||||
"password": "test"
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"token": "12343243243"
|
||||
}
|
||||
|
||||
:arg username: username
|
||||
:arg password: password
|
||||
:statuscode 401: invalid credentials
|
||||
:statuscode 200: no error
|
||||
"""
|
||||
self.reqparse.add_argument('username', type=str, required=True, location='json')
|
||||
self.reqparse.add_argument('password', type=str, required=True, location='json')
|
||||
|
||||
args = self.reqparse.parse_args()
|
||||
|
||||
if '@' in args['username']:
|
||||
user = user_service.get_by_email(args['username'])
|
||||
else:
|
||||
user = user_service.get_by_username(args['username'])
|
||||
|
||||
if user and user.check_password(args['password']):
|
||||
# Tell Flask-Principal the identity changed
|
||||
identity_changed.send(current_app._get_current_object(),
|
||||
identity=Identity(user.id))
|
||||
return dict(token=create_token(user))
|
||||
|
||||
return dict(message='The supplied credentials are invalid'), 401
|
||||
|
||||
def get(self):
|
||||
return {'username': g.current_user.username, 'roles': [r.name for r in g.current_user.roles]}
|
||||
|
||||
|
||||
class Ping(Resource):
|
||||
"""
|
||||
This class serves as an example of how one might implement an SSO provider for use with Lemur. In
|
||||
this example we use a OpenIDConnect authentication flow, that is essentially OAuth2 underneath. If you have an
|
||||
OAuth2 provider you want to use Lemur there would be two steps:
|
||||
|
||||
1. Define your own class that inherits from :class:`flask.ext.restful.Resource` and create the HTTP methods the \
|
||||
provider uses for it's callbacks.
|
||||
2. Add or change the Lemur AngularJS Configuration to point to your new provider
|
||||
"""
|
||||
def __init__(self):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(Ping, self).__init__()
|
||||
|
||||
def post(self):
|
||||
self.reqparse.add_argument('clientId', type=str, required=True, location='json')
|
||||
self.reqparse.add_argument('redirectUri', type=str, required=True, location='json')
|
||||
self.reqparse.add_argument('code', type=str, required=True, location='json')
|
||||
|
||||
args = self.reqparse.parse_args()
|
||||
|
||||
# take the information we have received from Meechum to create a new request
|
||||
params = {
|
||||
'client_id': args['clientId'],
|
||||
'grant_type': 'authorization_code',
|
||||
'scope': 'openid email profile address',
|
||||
'redirect_uri': args['redirectUri'],
|
||||
'code': args['code']
|
||||
}
|
||||
|
||||
# you can either discover these dynamically or simply configure them
|
||||
access_token_url = current_app.config.get('PING_ACCESS_TOKEN_URL')
|
||||
user_api_url = current_app.config.get('PING_USER_API_URL')
|
||||
|
||||
# the secret and cliendId will be given to you when you signup for meechum
|
||||
basic = base64.b64encode('{0}:{1}'.format(args['clientId'], current_app.config.get("PING_SECRET")))
|
||||
headers = {'Authorization': 'Basic {0}'.format(basic)}
|
||||
|
||||
# exchange authorization code for access token.
|
||||
|
||||
r = requests.post(access_token_url, headers=headers, params=params)
|
||||
id_token = r.json()['id_token']
|
||||
access_token = r.json()['access_token']
|
||||
|
||||
# fetch token public key
|
||||
header_data = fetch_token_header(id_token)
|
||||
jwks_url = current_app.config.get('PING_JWKS_URL')
|
||||
|
||||
# retrieve the key material as specified by the token header
|
||||
r = requests.get(jwks_url)
|
||||
for key in r.json()['keys']:
|
||||
if key['kid'] == header_data['kid']:
|
||||
secret = get_rsa_public_key(key['n'], key['e'])
|
||||
algo = header_data['alg']
|
||||
break
|
||||
else:
|
||||
return dict(message='Key not found'), 403
|
||||
|
||||
# validate your token based on the key it was signed with
|
||||
try:
|
||||
jwt.decode(id_token, secret, algorithms=[algo], audience=args['clientId'])
|
||||
except jwt.DecodeError:
|
||||
return dict(message='Token is invalid'), 403
|
||||
except jwt.ExpiredSignatureError:
|
||||
return dict(message='Token has expired'), 403
|
||||
except jwt.InvalidTokenError:
|
||||
return dict(message='Token is invalid'), 403
|
||||
|
||||
user_params = dict(access_token=access_token, schema='profile')
|
||||
|
||||
# retrieve information about the current user.
|
||||
r = requests.get(user_api_url, params=user_params)
|
||||
profile = r.json()
|
||||
|
||||
user = user_service.get_by_email(profile['email'])
|
||||
|
||||
# update their google 'roles'
|
||||
roles = []
|
||||
|
||||
# Legacy edge case - 'admin' has some special privileges associated with it
|
||||
if 'secops@netflix.com' in profile['googleGroups']:
|
||||
roles.append(role_service.get_by_name('admin'))
|
||||
|
||||
for group in profile['googleGroups']:
|
||||
role = role_service.get_by_name(group)
|
||||
if not role:
|
||||
role = role_service.create(group, description='This is a google group based role created by Lemur')
|
||||
roles.append(role)
|
||||
|
||||
# if we get an sso user create them an account
|
||||
# we still pick a random password in case sso is down
|
||||
if not user:
|
||||
# every user is an operator (tied to the verisignCA)
|
||||
v = role_service.get_by_name('verisign')
|
||||
if v:
|
||||
roles.append(v)
|
||||
|
||||
user = user_service.create(
|
||||
profile['email'],
|
||||
cert_service.create_challenge(),
|
||||
profile['email'],
|
||||
True,
|
||||
profile.get('thumbnailPhotoUrl'),
|
||||
roles
|
||||
)
|
||||
|
||||
else:
|
||||
# we add 'lemur' specific roles, so they do not get marked as removed
|
||||
for ur in user.roles:
|
||||
if ur.authority_id:
|
||||
roles.append(ur)
|
||||
|
||||
# update any changes to the user
|
||||
user_service.update(
|
||||
user.id,
|
||||
profile['email'],
|
||||
profile['email'],
|
||||
True,
|
||||
profile.get('thumbnailPhotoUrl'), # incase profile isn't google+ enabled
|
||||
roles
|
||||
)
|
||||
|
||||
# Tell Flask-Principal the identity changed
|
||||
identity_changed.send(current_app._get_current_object(), identity=Identity(user.id))
|
||||
|
||||
return dict(token=create_token(user))
|
||||
|
||||
|
||||
class Unlock(AuthenticatedResource):
|
||||
def __init__(self):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(Unlock, self).__init__()
|
||||
|
||||
@admin_permission.require(http_exception=403)
|
||||
def post(self):
|
||||
self.reqparse.add_argument('password', type=str, required=True, location='json')
|
||||
args = self.reqparse.parse_args()
|
||||
unlock(args['password'])
|
||||
return {
|
||||
"message": "You have successfully unlocked this Lemur instance",
|
||||
"type": "success"
|
||||
}
|
||||
|
||||
|
||||
api.add_resource(Login, '/auth/login', endpoint='login')
|
||||
api.add_resource(Ping, '/auth/ping', endpoint='ping')
|
||||
api.add_resource(Unlock, '/auth/unlock', endpoint='unlock')
|
||||
|
||||
|
0
lemur/authorities/__init__.py
Normal file
0
lemur/authorities/__init__.py
Normal file
58
lemur/authorities/models.py
Normal file
58
lemur/authorities/models.py
Normal file
@ -0,0 +1,58 @@
|
||||
"""
|
||||
.. module: lemur.authorities.models
|
||||
:platform: unix
|
||||
:synopsis: This module contains all of the models need to create a authority within Lemur.
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy import Column, Integer, String, Text, func, ForeignKey, DateTime, PassiveDefault, Boolean
|
||||
from sqlalchemy.dialects.postgresql import JSON
|
||||
|
||||
from lemur.database import db
|
||||
from lemur.certificates.models import cert_get_cn, cert_get_not_after, cert_get_not_before
|
||||
|
||||
|
||||
class Authority(db.Model):
|
||||
__tablename__ = 'authorities'
|
||||
id = Column(Integer, primary_key=True)
|
||||
owner = Column(String(128))
|
||||
name = Column(String(128), unique=True)
|
||||
body = Column(Text())
|
||||
chain = Column(Text())
|
||||
bits = Column(Integer())
|
||||
cn = Column(String(128))
|
||||
not_before = Column(DateTime)
|
||||
not_after = Column(DateTime)
|
||||
active = Column(Boolean, default=True)
|
||||
date_created = Column(DateTime, PassiveDefault(func.now()), nullable=False)
|
||||
plugin_name = Column(String(64))
|
||||
description = Column(Text)
|
||||
options = Column(JSON)
|
||||
roles = relationship('Role', backref=db.backref('authority'), lazy='dynamic')
|
||||
user_id = Column(Integer, ForeignKey('users.id'))
|
||||
certificates = relationship("Certificate", backref='authority')
|
||||
|
||||
def __init__(self, name, owner, plugin_name, body, roles=None, chain=None, description=None):
|
||||
self.name = name
|
||||
self.body = body
|
||||
self.chain = chain
|
||||
self.owner = owner
|
||||
self.plugin_name = plugin_name
|
||||
cert = x509.load_pem_x509_certificate(str(body), default_backend())
|
||||
self.cn = cert_get_cn(cert)
|
||||
self.not_before = cert_get_not_before(cert)
|
||||
self.not_after = cert_get_not_after(cert)
|
||||
self.roles = roles
|
||||
self.description = description
|
||||
|
||||
def as_dict(self):
|
||||
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
|
||||
|
||||
def serialize(self):
|
||||
blob = self.as_dict()
|
||||
return blob
|
173
lemur/authorities/service.py
Normal file
173
lemur/authorities/service.py
Normal file
@ -0,0 +1,173 @@
|
||||
"""
|
||||
.. module: lemur.authorities.service
|
||||
:platform: Unix
|
||||
:synopsis: This module contains all of the services level functions used to
|
||||
administer authorities in Lemur
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
|
||||
"""
|
||||
from flask import g
|
||||
|
||||
from lemur import database
|
||||
from lemur.authorities.models import Authority
|
||||
from lemur.roles import service as role_service
|
||||
|
||||
from lemur.roles.models import Role
|
||||
import lemur.certificates.service as cert_service
|
||||
|
||||
from lemur.common.services.issuers.manager import get_plugin_by_name
|
||||
|
||||
def update(authority_id, active=None, roles=None):
|
||||
"""
|
||||
Update a an authority with new values.
|
||||
|
||||
:param authority_id:
|
||||
:param roles: roles that are allowed to use this authority
|
||||
:rtype : Authority
|
||||
:return:
|
||||
"""
|
||||
authority = get(authority_id)
|
||||
if roles:
|
||||
authority = database.update_list(authority, 'roles', Role, roles)
|
||||
|
||||
if active:
|
||||
authority.active = active
|
||||
return database.update(authority)
|
||||
|
||||
|
||||
def create(kwargs):
|
||||
"""
|
||||
Create a new authority.
|
||||
|
||||
:param name: name of the authority
|
||||
:param roles: roles that are allowed to use this authority
|
||||
:param options: available options for authority
|
||||
:param description:
|
||||
:rtype : Authority
|
||||
:return:
|
||||
"""
|
||||
|
||||
issuer = get_plugin_by_name(kwargs.get('pluginName'))
|
||||
|
||||
kwargs['creator'] = g.current_user.email
|
||||
cert_body, intermediate, issuer_roles = issuer.create_authority(kwargs)
|
||||
|
||||
cert = cert_service.save_cert(cert_body, None, intermediate, None, None, None)
|
||||
cert.user = g.current_user
|
||||
|
||||
# we create and attach any roles that cloudCA gives us
|
||||
role_objs = []
|
||||
for r in issuer_roles:
|
||||
role = role_service.create(r['name'], password=r['password'], description="CloudCA auto generated role",
|
||||
username=r['username'])
|
||||
# the user creating the authority should be able to administer it
|
||||
if role.username == 'admin':
|
||||
g.current_user.roles.append(role)
|
||||
role_objs.append(role)
|
||||
|
||||
authority = Authority(
|
||||
kwargs.get('caName'),
|
||||
kwargs['ownerEmail'],
|
||||
kwargs['pluginName'],
|
||||
cert_body,
|
||||
description=kwargs['caDescription'],
|
||||
chain=intermediate,
|
||||
roles=role_objs
|
||||
)
|
||||
|
||||
# do this last encase we need to roll back/abort
|
||||
database.update(cert)
|
||||
authority = database.create(authority)
|
||||
|
||||
g.current_user.authorities.append(authority)
|
||||
|
||||
return authority
|
||||
|
||||
|
||||
def get_all():
|
||||
"""
|
||||
Get all authorities that are currently in Lemur.
|
||||
|
||||
:rtype : List
|
||||
:return:
|
||||
"""
|
||||
query = database.session_query(Authority)
|
||||
return database.find_all(query, Authority, {}).all()
|
||||
|
||||
|
||||
def get(authority_id):
|
||||
"""
|
||||
Retrieves an authority given it's ID
|
||||
|
||||
:rtype : Authority
|
||||
:param authority_id:
|
||||
:return:
|
||||
"""
|
||||
return database.get(Authority, authority_id)
|
||||
|
||||
|
||||
def get_by_name(authority_name):
|
||||
"""
|
||||
Retrieves an authority given it's name.
|
||||
|
||||
:param authority_name:
|
||||
:rtype : Authority
|
||||
:return:
|
||||
"""
|
||||
return database.get(Authority, authority_name, field='name')
|
||||
|
||||
|
||||
def get_authority_role(ca_name):
|
||||
"""
|
||||
Attempts to get the authority role for a given ca uses current_user
|
||||
as a basis for accomplishing that.
|
||||
|
||||
:param ca_name:
|
||||
"""
|
||||
if g.current_user.is_admin:
|
||||
authority = get_by_name(ca_name)
|
||||
#TODO we should pick admin ca roles for admin
|
||||
return authority.roles[0]
|
||||
else:
|
||||
for role in g.current_user.roles:
|
||||
if role.authority:
|
||||
if role.authority.name == ca_name:
|
||||
return role
|
||||
|
||||
|
||||
def render(args):
|
||||
"""
|
||||
Helper that helps us render the REST Api responses.
|
||||
:param args:
|
||||
:return:
|
||||
"""
|
||||
query = database.session_query(Authority)
|
||||
sort_by = args.pop('sort_by')
|
||||
sort_dir = args.pop('sort_dir')
|
||||
page = args.pop('page')
|
||||
count = args.pop('count')
|
||||
filt = args.pop('filter')
|
||||
|
||||
if filt:
|
||||
terms = filt.split(';')
|
||||
if 'active' in filt: # this is really weird but strcmp seems to not work here??
|
||||
query = query.filter(Authority.active == terms[1])
|
||||
else:
|
||||
query = database.filter(query, Authority, terms)
|
||||
|
||||
# we make sure that a user can only use an authority they either own are are a member of - admins can see all
|
||||
if not g.current_user.is_admin:
|
||||
authority_ids = []
|
||||
for role in g.current_user.roles:
|
||||
if role.authority:
|
||||
authority_ids.append(role.authority.id)
|
||||
query = query.filter(Authority.id.in_(authority_ids))
|
||||
|
||||
query = database.find_all(query, Authority, args)
|
||||
|
||||
if sort_by and sort_dir:
|
||||
query = database.sort(query, Authority, sort_by, sort_dir)
|
||||
|
||||
return database.paginate(query, page, count)
|
372
lemur/authorities/views.py
Normal file
372
lemur/authorities/views.py
Normal file
@ -0,0 +1,372 @@
|
||||
"""
|
||||
.. module: lemur.authorities.views
|
||||
:platform: Unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
from flask import Blueprint, g
|
||||
from flask.ext.restful import reqparse, fields, Api
|
||||
|
||||
from lemur.authorities import service
|
||||
from lemur.roles import service as role_service
|
||||
from lemur.certificates import service as certificate_service
|
||||
from lemur.auth.service import AuthenticatedResource
|
||||
|
||||
from lemur.auth.permissions import AuthorityPermission
|
||||
|
||||
from lemur.common.utils import paginated_parser, marshal_items
|
||||
|
||||
|
||||
FIELDS = {
|
||||
'name': fields.String,
|
||||
'description': fields.String,
|
||||
'options': fields.Raw,
|
||||
'pluginName': fields.String,
|
||||
'body': fields.String,
|
||||
'chain': fields.String,
|
||||
'active': fields.Boolean,
|
||||
'notBefore': fields.DateTime(dt_format='iso8601', attribute='not_before'),
|
||||
'notAfter': fields.DateTime(dt_format='iso8601', attribute='not_after'),
|
||||
'id': fields.Integer,
|
||||
}
|
||||
|
||||
mod = Blueprint('authorities', __name__)
|
||||
api = Api(mod)
|
||||
|
||||
|
||||
class AuthoritiesList(AuthenticatedResource):
|
||||
""" Defines the 'authorities' endpoint """
|
||||
def __init__(self):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(AuthoritiesList, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
def get(self):
|
||||
"""
|
||||
.. http:get:: /authorities
|
||||
|
||||
The current list of authorities
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /authorities 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": 1,
|
||||
"name": "authority1",
|
||||
"description": "this is authority1",
|
||||
"pluginName": null,
|
||||
"chain": "-----Begin ...",
|
||||
"body": "-----Begin ...",
|
||||
"active": true,
|
||||
"notBefore": "2015-06-05T17:09:39",
|
||||
"notAfter": "2015-06-10T17:09:39"
|
||||
"options": null
|
||||
}
|
||||
]
|
||||
"total": 1
|
||||
}
|
||||
|
||||
:query sortBy: field to sort on
|
||||
:query sortDir: acs or desc
|
||||
:query page: int. default is 1
|
||||
:query filter: key value pair. format is k=v;
|
||||
:query limit: limit number. default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
|
||||
:note: this will only show certificates that the current user is authorized to use
|
||||
"""
|
||||
parser = paginated_parser.copy()
|
||||
args = parser.parse_args()
|
||||
return service.render(args)
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
def post(self):
|
||||
"""
|
||||
.. http:post:: /authorities
|
||||
|
||||
Create an authority
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /authorities HTTP/1.1
|
||||
Host: example.com
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
{
|
||||
"caDN": {
|
||||
"country": "US",
|
||||
"state": "CA",
|
||||
"location": "A Location",
|
||||
"organization": "ExampleInc",
|
||||
"organizationalUnit": "Operations",
|
||||
"commonName": "a common name"
|
||||
},
|
||||
"caType": "root",
|
||||
"caSigningAlgo": "sha256WithRSA",
|
||||
"caSensitivity": "medium",
|
||||
"keyType": "RSA2048",
|
||||
"pluginName": "cloudca",
|
||||
"validityStart": "2015-06-11T07:00:00.000Z",
|
||||
"validityEnd": "2015-06-13T07:00:00.000Z",
|
||||
"caName": "DoctestCA",
|
||||
"ownerEmail": "jimbob@example.com",
|
||||
"caDescription": "Example CA",
|
||||
"extensions": {
|
||||
"subAltNames": {
|
||||
"names": []
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"name": "authority1",
|
||||
"description": "this is authority1",
|
||||
"pluginName": null,
|
||||
"chain": "-----Begin ...",
|
||||
"body": "-----Begin ...",
|
||||
"active": true,
|
||||
"notBefore": "2015-06-05T17:09:39",
|
||||
"notAfter": "2015-06-10T17:09:39"
|
||||
"options": null
|
||||
}
|
||||
|
||||
:arg caName: authority's name
|
||||
:arg caDescription: a sensible description about what the CA with be used for
|
||||
:arg ownerEmail: the team or person who 'owns' this authority
|
||||
:arg validityStart: when this authority should start issuing certificates
|
||||
:arg validityEnd: when this authority should stop issuing certificates
|
||||
:arg extensions: certificate extensions
|
||||
:arg pluginName: name of the plugin to create the authority
|
||||
:arg caType: the type of authority (root/subca)
|
||||
:arg caParent: the parent authority if this is to be a subca
|
||||
:arg caSigningAlgo: algorithm used to sign the authority
|
||||
:arg keyType: key type
|
||||
:arg caSensitivity: the sensitivity of the root key, for CloudCA this determines if the root keys are stored
|
||||
in an HSM
|
||||
:arg caKeyName: name of the key to store in the HSM (CloudCA)
|
||||
:arg caSerialNumber: serial number of the authority
|
||||
:arg caFirstSerial: specifies the starting serial number for certificates issued off of this authority
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 403: unauthenticated
|
||||
:statuscode 200: no error
|
||||
"""
|
||||
self.reqparse.add_argument('caName', type=str, location='json', required=True)
|
||||
self.reqparse.add_argument('caDescription', type=str, location='json', required=False)
|
||||
self.reqparse.add_argument('ownerEmail', type=str, location='json', required=True)
|
||||
self.reqparse.add_argument('caDN', type=dict, location='json', required=False)
|
||||
self.reqparse.add_argument('validityStart', type=str, location='json', required=False) # TODO validate
|
||||
self.reqparse.add_argument('validityEnd', type=str, location='json', required=False) # TODO validate
|
||||
self.reqparse.add_argument('extensions', type=dict, location='json', required=False)
|
||||
self.reqparse.add_argument('pluginName', type=str, location='json', required=True)
|
||||
self.reqparse.add_argument('caType', type=str, location='json', required=False)
|
||||
self.reqparse.add_argument('caParent', type=str, location='json', required=False)
|
||||
self.reqparse.add_argument('caSigningAlgo', type=str, location='json', required=False)
|
||||
self.reqparse.add_argument('keyType', type=str, location='json', required=False)
|
||||
self.reqparse.add_argument('caSensitivity', type=str, location='json', required=False)
|
||||
self.reqparse.add_argument('caKeyName', type=str, location='json', required=False)
|
||||
self.reqparse.add_argument('caSerialNumber', type=int, location='json', required=False)
|
||||
self.reqparse.add_argument('caFirstSerial', type=int, location='json', required=False)
|
||||
|
||||
args = self.reqparse.parse_args()
|
||||
return service.create(args)
|
||||
|
||||
|
||||
class Authorities(AuthenticatedResource):
|
||||
def __init__(self):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(Authorities, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
def get(self, authority_id):
|
||||
"""
|
||||
.. http:get:: /authorities/1
|
||||
|
||||
One authority
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /authorities/1 HTTP/1.1
|
||||
Host: example.com
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"name": "authority1",
|
||||
"description": "this is authority1",
|
||||
"pluginName": null,
|
||||
"chain": "-----Begin ...",
|
||||
"body": "-----Begin ...",
|
||||
"active": true,
|
||||
"notBefore": "2015-06-05T17:09:39",
|
||||
"notAfter": "2015-06-10T17:09:39"
|
||||
"options": null
|
||||
}
|
||||
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
"""
|
||||
return service.get(authority_id)
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
def put(self, authority_id):
|
||||
"""
|
||||
.. http:put:: /authorities/1
|
||||
|
||||
Update a authority
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PUT /authorities/1 HTTP/1.1
|
||||
Host: example.com
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
{
|
||||
"roles": [],
|
||||
"active": false
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"name": "authority1",
|
||||
"description": "this is authority1",
|
||||
"pluginname": null,
|
||||
"chain": "-----begin ...",
|
||||
"body": "-----begin ...",
|
||||
"active": false,
|
||||
"notbefore": "2015-06-05t17:09:39",
|
||||
"notafter": "2015-06-10t17:09:39"
|
||||
"options": null
|
||||
}
|
||||
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
"""
|
||||
self.reqparse.add_argument('roles', type=list, location='json')
|
||||
self.reqparse.add_argument('active', type=str, location='json')
|
||||
args = self.reqparse.parse_args()
|
||||
|
||||
authority = service.get(authority_id)
|
||||
role = role_service.get_by_name(authority.owner)
|
||||
|
||||
# all the authority role members should be allowed
|
||||
roles = [x.name for x in authority.roles]
|
||||
|
||||
# allow "owner" roles by team DL
|
||||
roles.append(role)
|
||||
permission = AuthorityPermission(authority_id, roles)
|
||||
|
||||
# we want to make sure that we cannot add roles that we are not members of
|
||||
if not g.current_user.is_admin:
|
||||
role_ids = set([r['id'] for r in args['roles']])
|
||||
user_role_ids = set([r.id for r in g.current_user.roles])
|
||||
|
||||
if not role_ids.issubset(user_role_ids):
|
||||
return dict(message="You are not allowed to associate a role which you are not a member of"), 400
|
||||
|
||||
if permission.can():
|
||||
return service.update(authority_id, active=args['active'], roles=args['roles'])
|
||||
|
||||
return dict(message="You are not authorized to update this authority"), 403
|
||||
|
||||
|
||||
class CertificateAuthority(AuthenticatedResource):
|
||||
def __init__(self):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(CertificateAuthority, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
def get(self, certificate_id):
|
||||
"""
|
||||
.. http:get:: /certificates/1/authority
|
||||
|
||||
One authority for given certificate
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /certificates/1/authority 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
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"name": "authority1",
|
||||
"description": "this is authority1",
|
||||
"pluginName": null,
|
||||
"chain": "-----Begin ...",
|
||||
"body": "-----Begin ...",
|
||||
"active": true,
|
||||
"notBefore": "2015-06-05T17:09:39",
|
||||
"notAfter": "2015-06-10T17:09:39"
|
||||
"options": null
|
||||
}
|
||||
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
"""
|
||||
return certificate_service.get(certificate_id).authority
|
||||
|
||||
api.add_resource(AuthoritiesList, '/authorities', endpoint='authorities')
|
||||
api.add_resource(Authorities, '/authorities/<int:authority_id>', endpoint='authority')
|
||||
api.add_resource(CertificateAuthority, '/certificates/<int:certificate_id>/authority', endpoint='certificateAuthority')
|
0
lemur/certificates/__init__.py
Normal file
0
lemur/certificates/__init__.py
Normal file
87
lemur/certificates/exceptions.py
Normal file
87
lemur/certificates/exceptions.py
Normal file
@ -0,0 +1,87 @@
|
||||
"""
|
||||
.. module: lemur.certificates.exceptions
|
||||
:synopsis: Defines all monterey specific exceptions
|
||||
: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 current_app
|
||||
from lemur.exceptions import LemurException
|
||||
|
||||
|
||||
class UnknownAuthority(LemurException):
|
||||
def __init__(self, authority):
|
||||
self.code = 404
|
||||
self.authority = authority
|
||||
self.data = {"message": "The authority specified '{}' is not a valid authority".format(self.authority)}
|
||||
|
||||
current_app.logger.warning(self)
|
||||
|
||||
def __str__(self):
|
||||
return repr(self.data['message'])
|
||||
|
||||
|
||||
class InsufficientDomains(LemurException):
|
||||
def __init__(self):
|
||||
self.code = 400
|
||||
self.data = {"message": "Need at least one domain specified in order create a certificate"}
|
||||
|
||||
current_app.logger.warning(self)
|
||||
|
||||
def __str__(self):
|
||||
return repr(self.data['message'])
|
||||
|
||||
|
||||
class InvalidCertificate(LemurException):
|
||||
def __init__(self):
|
||||
self.code = 400
|
||||
self.data = {"message": "Need at least one domain specified in order create a certificate"}
|
||||
|
||||
current_app.logger.warning(self)
|
||||
|
||||
def __str__(self):
|
||||
return repr(self.data['message'])
|
||||
|
||||
|
||||
class UnableToCreateCSR(LemurException):
|
||||
def __init__(self):
|
||||
self.code = 500
|
||||
self.data = {"message": "Unable to generate CSR"}
|
||||
|
||||
current_app.logger.error(self)
|
||||
|
||||
def __str__(self):
|
||||
return repr(self.data['message'])
|
||||
|
||||
class UnableToCreatePrivateKey(LemurException):
|
||||
def __init__(self):
|
||||
self.code = 500
|
||||
self.data = {"message": "Unable to generate Private Key"}
|
||||
|
||||
current_app.logger.error(self)
|
||||
|
||||
def __str__(self):
|
||||
return repr(self.data['message'])
|
||||
|
||||
class MissingFiles(LemurException):
|
||||
def __init__(self, path):
|
||||
self.code = 500
|
||||
self.path = path
|
||||
self.data = {"path": self.path, "message": "Expecting missing files"}
|
||||
|
||||
current_app.logger.error(self)
|
||||
|
||||
def __str__(self):
|
||||
return repr(self.data['message'])
|
||||
|
||||
|
||||
class NoPersistanceFound(LemurException):
|
||||
def __init__(self):
|
||||
self.code = 500
|
||||
self.data = {"code": 500, "message": "No peristence method found, Lemur cannot persist sensitive information"}
|
||||
|
||||
current_app.logger.error(self)
|
||||
|
||||
def __str__(self):
|
||||
return repr(self.data['message'])
|
||||
|
307
lemur/certificates/models.py
Normal file
307
lemur/certificates/models.py
Normal file
@ -0,0 +1,307 @@
|
||||
"""
|
||||
.. module: lemur.certificates.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 os
|
||||
import datetime
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
|
||||
from flask import current_app
|
||||
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy import Integer, ForeignKey, String, DateTime, PassiveDefault, func, Column, Text, Boolean
|
||||
|
||||
from sqlalchemy_utils import EncryptedType
|
||||
|
||||
from lemur.database import db
|
||||
|
||||
from lemur.domains.models import Domain
|
||||
from lemur.users import service as user_service
|
||||
|
||||
from lemur.constants import SAN_NAMING_TEMPLATE, DEFAULT_NAMING_TEMPLATE, NONSTANDARD_NAMING_TEMPLATE
|
||||
from lemur.models import certificate_associations, certificate_account_associations
|
||||
|
||||
|
||||
def cert_get_cn(cert):
|
||||
"""
|
||||
Attempts to get a sane common name from a given certificate.
|
||||
|
||||
:param cert:
|
||||
:return: Common name or None
|
||||
"""
|
||||
try:
|
||||
return cert.subject.get_attributes_for_oid(
|
||||
x509.OID_COMMON_NAME
|
||||
)[0].value.strip()
|
||||
except Exception as e:
|
||||
current_app.logger.error("Unable to get CN! {0}".format(e))
|
||||
|
||||
|
||||
def cert_get_domains(cert):
|
||||
"""
|
||||
Attempts to get an domains listed in a certificate.
|
||||
If 'subjectAltName' extension is not available we simply
|
||||
return the common name.
|
||||
|
||||
:param cert:
|
||||
:return: List of domains
|
||||
"""
|
||||
domains = []
|
||||
try:
|
||||
ext = cert.extensions.get_extension_for_oid(x509.OID_SUBJECT_ALTERNATIVE_NAME)
|
||||
entries = ext.get_values_for(x509.DNSName)
|
||||
for entry in entries:
|
||||
domains.append(entry.split(":")[1].strip(", "))
|
||||
except Exception as e:
|
||||
current_app.logger.warning("Failed to get SubjectAltName: {0}".format(e))
|
||||
domains.append(cert_get_cn(cert))
|
||||
return domains
|
||||
|
||||
|
||||
def cert_get_serial(cert):
|
||||
"""
|
||||
Fetch the serial number from the certificate.
|
||||
|
||||
:param cert:
|
||||
:return: serial number
|
||||
"""
|
||||
return cert.serial
|
||||
|
||||
|
||||
def cert_is_san(cert):
|
||||
"""
|
||||
Determines if a given certificate is a SAN certificate.
|
||||
SAN certificates are simply certificates that cover multiple domains.
|
||||
|
||||
:param cert:
|
||||
:return: Bool
|
||||
"""
|
||||
domains = cert_get_domains(cert)
|
||||
if len(domains) > 1:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def cert_is_wildcard(cert):
|
||||
"""
|
||||
Determines if certificate is a wildcard certificate.
|
||||
|
||||
:param cert:
|
||||
:return: Bool
|
||||
"""
|
||||
domains = cert_get_domains(cert)
|
||||
if len(domains) == 1 and domains[0][0:1] == "*":
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def cert_get_bitstrength(cert):
|
||||
"""
|
||||
Calculates a certificates public key bit length.
|
||||
|
||||
:param cert:
|
||||
:return: Integer
|
||||
"""
|
||||
return cert.public_key().key_size * 8
|
||||
|
||||
|
||||
def cert_get_issuer(cert):
|
||||
"""
|
||||
Gets a sane issuer from a given certificate.
|
||||
|
||||
:param cert:
|
||||
:return: Issuer
|
||||
"""
|
||||
try:
|
||||
return cert.subject.get_attributes_for_oid(x509.OID_ORGANIZATION_NAME)[0].value
|
||||
except Exception as e:
|
||||
current_app.logger.error("Unable to get issuer! {0}".format(e))
|
||||
|
||||
|
||||
def cert_is_internal(cert):
|
||||
"""
|
||||
Uses an internal resource in order to determine if
|
||||
a given certificate was issued by an 'internal' certificate
|
||||
authority.
|
||||
|
||||
:param cert:
|
||||
:return: Bool
|
||||
"""
|
||||
if cert_get_issuer(cert) in current_app.config.get('INTERNAL_CA', []):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def cert_get_not_before(cert):
|
||||
"""
|
||||
Gets the naive datetime of the certificates 'not_before' field.
|
||||
This field denotes the first date in time which the given certificate
|
||||
is valid.
|
||||
|
||||
:param cert:
|
||||
:return: Datetime
|
||||
"""
|
||||
return cert.not_valid_before
|
||||
|
||||
|
||||
def cert_get_not_after(cert):
|
||||
"""
|
||||
Gets the naive datetime of the certificates 'not_after' field.
|
||||
This field denotes the last date in time which the given certificate
|
||||
is valid.
|
||||
|
||||
:param cert:
|
||||
:return: Datetime
|
||||
"""
|
||||
return cert.not_valid_after
|
||||
|
||||
|
||||
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 get_account_number(arn):
|
||||
"""
|
||||
Extract the account number from an arn.
|
||||
|
||||
:param arn: IAM SSL arn
|
||||
:return: account number associated with ARN
|
||||
"""
|
||||
return arn.split(":")[4]
|
||||
|
||||
|
||||
class Certificate(db.Model):
|
||||
__tablename__ = 'certificates'
|
||||
id = Column(Integer, primary_key=True)
|
||||
owner = Column(String(128))
|
||||
body = Column(Text())
|
||||
private_key = Column(EncryptedType(String, os.environ.get('LEMUR_ENCRYPTION_KEY')))
|
||||
challenge = Column(EncryptedType(String, os.environ.get('LEMUR_ENCRYPTION_KEY')))
|
||||
csr_config = Column(Text())
|
||||
status = Column(String(128))
|
||||
deleted = Column(Boolean, index=True)
|
||||
name = Column(String(128))
|
||||
chain = Column(Text())
|
||||
bits = Column(Integer())
|
||||
issuer = Column(String(128))
|
||||
serial = Column(String(128))
|
||||
cn = Column(String(128))
|
||||
description = Column(String(1024))
|
||||
active = Column(Boolean, default=True)
|
||||
san = Column(String(1024))
|
||||
not_before = Column(DateTime)
|
||||
not_after = Column(DateTime)
|
||||
date_created = Column(DateTime, PassiveDefault(func.now()), nullable=False)
|
||||
user_id = Column(Integer, ForeignKey('users.id'))
|
||||
authority_id = Column(Integer, ForeignKey('authorities.id'))
|
||||
accounts = relationship("Account", secondary=certificate_account_associations, backref='certificate')
|
||||
domains = relationship("Domain", secondary=certificate_associations, backref="certificate")
|
||||
elb_listeners = relationship("Listener", lazy='dynamic', backref='certificate')
|
||||
|
||||
def __init__(self, body, private_key=None, challenge=None, chain=None, csr_config=None):
|
||||
self.body = body
|
||||
# We encrypt the private_key on creation
|
||||
self.private_key = private_key
|
||||
self.chain = chain
|
||||
self.csr_config = csr_config
|
||||
self.challenge = challenge
|
||||
cert = x509.load_pem_x509_certificate(str(self.body), default_backend())
|
||||
self.bits = cert_get_bitstrength(cert)
|
||||
self.issuer = cert_get_issuer(cert)
|
||||
self.serial = cert_get_serial(cert)
|
||||
self.cn = cert_get_cn(cert)
|
||||
self.san = cert_is_san(cert)
|
||||
self.not_before = cert_get_not_before(cert)
|
||||
self.not_after = cert_get_not_after(cert)
|
||||
self.name = self.create_name
|
||||
|
||||
for domain in cert_get_domains(cert):
|
||||
self.domains.append(Domain(name=domain))
|
||||
|
||||
@property
|
||||
def create_name(self):
|
||||
"""
|
||||
Create a name for our certificate. A naming standard
|
||||
is based on a series of templates. The name includes
|
||||
useful information such as Common Name, Validation dates,
|
||||
and Issuer.
|
||||
|
||||
:rtype : str
|
||||
:return:
|
||||
"""
|
||||
# aws doesn't allow special chars
|
||||
if self.cn:
|
||||
subject = self.cn.replace('*', "WILDCARD")
|
||||
|
||||
if self.san:
|
||||
t = SAN_NAMING_TEMPLATE
|
||||
else:
|
||||
t = DEFAULT_NAMING_TEMPLATE
|
||||
|
||||
temp = t.format(
|
||||
subject=subject,
|
||||
issuer=self.issuer,
|
||||
not_before=self.not_before.strftime('%Y%m%d'),
|
||||
not_after=self.not_after.strftime('%Y%m%d')
|
||||
)
|
||||
|
||||
else:
|
||||
t = NONSTANDARD_NAMING_TEMPLATE
|
||||
|
||||
temp = t.format(
|
||||
issuer=self.issuer,
|
||||
not_before=self.not_before.strftime('%Y%m%d'),
|
||||
not_after=self.not_after.strftime('%Y%m%d')
|
||||
)
|
||||
|
||||
return temp
|
||||
|
||||
@property
|
||||
def is_expired(self):
|
||||
if self.not_after < datetime.datetime.now():
|
||||
return True
|
||||
|
||||
@property
|
||||
def is_unused(self):
|
||||
if self.elb_listeners.count() == 0:
|
||||
return True
|
||||
|
||||
@property
|
||||
def is_revoked(self):
|
||||
# we might not yet know the condition of the cert
|
||||
if self.status:
|
||||
if 'revoked' in self.status:
|
||||
return True
|
||||
|
||||
def get_arn(self, account_number):
|
||||
"""
|
||||
Generate a valid AWS IAM arn
|
||||
|
||||
:rtype : str
|
||||
:param account_number:
|
||||
:return:
|
||||
"""
|
||||
return "arn:aws:iam::{}:server-certificate/{}".format(account_number, self.name)
|
||||
|
||||
def as_dict(self):
|
||||
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
|
||||
|
||||
def serialize(self):
|
||||
blob = self.as_dict()
|
||||
# TODO this should be done with relationships
|
||||
user = user_service.get(self.user_id)
|
||||
if user:
|
||||
blob['creator'] = user.username
|
||||
|
||||
return blob
|
||||
|
446
lemur/certificates/service.py
Normal file
446
lemur/certificates/service.py
Normal file
@ -0,0 +1,446 @@
|
||||
"""
|
||||
.. module: 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>
|
||||
"""
|
||||
import os
|
||||
import arrow
|
||||
import string
|
||||
import random
|
||||
import hashlib
|
||||
import datetime
|
||||
import subprocess
|
||||
|
||||
from sqlalchemy import func, or_
|
||||
from flask import g, current_app
|
||||
|
||||
from lemur import database
|
||||
from lemur.common.services.aws import iam
|
||||
from lemur.common.services.issuers.manager import get_plugin_by_name
|
||||
|
||||
from lemur.certificates.models import Certificate
|
||||
from lemur.certificates.exceptions import UnableToCreateCSR, \
|
||||
UnableToCreatePrivateKey, MissingFiles
|
||||
|
||||
from lemur.accounts.models import Account
|
||||
from lemur.accounts import service as account_service
|
||||
from lemur.authorities.models import Authority
|
||||
|
||||
from lemur.roles.models import Role
|
||||
|
||||
|
||||
def get(cert_id):
|
||||
"""
|
||||
Retrieves certificate by it's ID.
|
||||
|
||||
:param cert_id:
|
||||
:return:
|
||||
"""
|
||||
return database.get(Certificate, cert_id)
|
||||
|
||||
|
||||
def get_by_name(name):
|
||||
"""
|
||||
Retrieves certificate by it's Name.
|
||||
|
||||
:param name:
|
||||
:return:
|
||||
"""
|
||||
return database.get(Certificate, name, field='name')
|
||||
|
||||
|
||||
def delete(cert_id):
|
||||
"""
|
||||
Delete's a certificate.
|
||||
|
||||
:param 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():
|
||||
"""
|
||||
Retrieves all certificates within Lemur.
|
||||
|
||||
:return:
|
||||
"""
|
||||
return Certificate.query.all()
|
||||
|
||||
|
||||
def find_duplicates(cert_body):
|
||||
"""
|
||||
Finds certificates that already exist within Lemur. We do this by looking for
|
||||
certificate bodies that are the same. This is the most reliable way to determine
|
||||
if a certificate is already being tracked by Lemur.
|
||||
|
||||
:param cert_body:
|
||||
:return:
|
||||
"""
|
||||
return Certificate.query.filter_by(body=cert_body).all()
|
||||
|
||||
|
||||
def update(cert_id, owner, active):
|
||||
"""
|
||||
Updates a certificate.
|
||||
|
||||
:param cert_id:
|
||||
:param owner:
|
||||
:param active:
|
||||
:return:
|
||||
"""
|
||||
cert = get(cert_id)
|
||||
cert.owner = owner
|
||||
cert.active = active
|
||||
return database.update(cert)
|
||||
|
||||
|
||||
def mint(issuer_options):
|
||||
"""
|
||||
Minting is slightly different for each authority.
|
||||
Support for multiple authorities is handled by individual plugins.
|
||||
|
||||
:param issuer_options:
|
||||
"""
|
||||
authority = issuer_options['authority']
|
||||
|
||||
issuer = get_plugin_by_name(authority.plugin_name)
|
||||
# NOTE if we wanted to support more issuers it might make sense to
|
||||
# push CSR creation down to the plugin
|
||||
path = create_csr(issuer.get_csr_config(issuer_options))
|
||||
challenge, csr, csr_config, private_key = load_ssl_pack(path)
|
||||
|
||||
issuer_options['challenge'] = challenge
|
||||
issuer_options['creator'] = g.user.email
|
||||
cert_body, cert_chain = issuer.create_certificate(csr, issuer_options)
|
||||
|
||||
cert = save_cert(cert_body, private_key, cert_chain, challenge, csr_config, issuer_options.get('accounts'))
|
||||
cert.user = g.user
|
||||
cert.authority = authority
|
||||
database.update(cert)
|
||||
|
||||
# securely delete pack after saving it to RDS and IAM (if applicable)
|
||||
delete_ssl_pack(path)
|
||||
|
||||
return cert, private_key, cert_chain,
|
||||
|
||||
|
||||
def import_certificate(**kwargs):
|
||||
"""
|
||||
Uploads already minted certificates and pulls the required information into Lemur.
|
||||
|
||||
This is to be used for certificates that are reated outside of Lemur but
|
||||
should still be tracked.
|
||||
|
||||
Internally this is used to bootstrap Lemur with external
|
||||
certificates, and used when certificates are 'discovered' through various discovery
|
||||
techniques. was still in aws.
|
||||
|
||||
:param kwargs:
|
||||
"""
|
||||
cert = Certificate(kwargs['public_certificate'])
|
||||
cert.owner = kwargs.get('owner', )
|
||||
cert.creator = kwargs.get('creator', 'Lemur')
|
||||
|
||||
# NOTE existing certs may not follow our naming standard we will
|
||||
# overwrite the generated name with the actual cert name
|
||||
if kwargs.get('name'):
|
||||
cert.name = kwargs.get('name')
|
||||
|
||||
if kwargs.get('user'):
|
||||
cert.user = kwargs.get('user')
|
||||
|
||||
if kwargs.get('account'):
|
||||
cert.accounts.append(kwargs.get('account'))
|
||||
|
||||
cert = database.create(cert)
|
||||
return cert
|
||||
|
||||
|
||||
def save_cert(cert_body, private_key, cert_chain, challenge, csr_config, accounts):
|
||||
"""
|
||||
Determines if the certificate needs to be uploaded to AWS or other services.
|
||||
|
||||
:param cert_body:
|
||||
:param private_key:
|
||||
:param cert_chain:
|
||||
:param challenge:
|
||||
:param csr_config:
|
||||
:param account_ids:
|
||||
"""
|
||||
cert = Certificate(cert_body, private_key, challenge, cert_chain, csr_config)
|
||||
# if we have an AWS accounts lets upload them
|
||||
if accounts:
|
||||
for account in accounts:
|
||||
account = account_service.get(account['id'])
|
||||
iam.upload_cert(account.account_number, cert, private_key, cert_chain)
|
||||
cert.accounts.append(account)
|
||||
return cert
|
||||
|
||||
|
||||
def upload(**kwargs):
|
||||
"""
|
||||
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(
|
||||
kwargs.get('public_cert'),
|
||||
kwargs.get('private_key'),
|
||||
kwargs.get('intermediate_cert'),
|
||||
None,
|
||||
None,
|
||||
kwargs.get('accounts')
|
||||
)
|
||||
|
||||
cert.owner = kwargs['owner']
|
||||
cert = database.create(cert)
|
||||
g.user.certificates.append(cert)
|
||||
return cert
|
||||
|
||||
|
||||
def create(**kwargs):
|
||||
"""
|
||||
Creates a new certificate.
|
||||
"""
|
||||
cert, private_key, cert_chain = mint(kwargs)
|
||||
|
||||
cert.owner = kwargs['owner']
|
||||
database.create(cert)
|
||||
g.user.certificates.append(cert)
|
||||
database.update(g.user)
|
||||
return cert
|
||||
|
||||
|
||||
def render(args):
|
||||
"""
|
||||
Helper function that allows use to render our REST Api.
|
||||
|
||||
:param args:
|
||||
:return:
|
||||
"""
|
||||
query = database.session_query(Certificate)
|
||||
|
||||
time_range = args.pop('time_range')
|
||||
account_id = args.pop('account_id')
|
||||
show = args.pop('show')
|
||||
owner = args.pop('owner')
|
||||
creator = args.pop('creator') # TODO we should enabling filtering by owner
|
||||
|
||||
filt = args.pop('filter')
|
||||
|
||||
if filt:
|
||||
terms = filt.split(';')
|
||||
if 'issuer' in terms:
|
||||
# we can't rely on issuer being correct in the cert directly so we combine queries
|
||||
sub_query = database.session_query(Authority.id)\
|
||||
.filter(Authority.name.ilike('%{0}%'.format(terms[1])))\
|
||||
.subquery()
|
||||
|
||||
query = query.filter(
|
||||
or_(
|
||||
Certificate.issuer.ilike('%{0}%'.format(terms[1])),
|
||||
Certificate.authority_id.in_(sub_query)
|
||||
)
|
||||
)
|
||||
return database.sort_and_page(query, Certificate, args)
|
||||
|
||||
if 'account' in terms:
|
||||
query = query.filter(Certificate.accounts.any(Account.id == terms[1]))
|
||||
elif 'active' in filt: # this is really weird but strcmp seems to not work here??
|
||||
query = query.filter(Certificate.active == terms[1])
|
||||
else:
|
||||
query = database.filter(query, Certificate, terms)
|
||||
|
||||
if show:
|
||||
sub_query = database.session_query(Role.name).filter(Role.user_id == g.user.id).subquery()
|
||||
query = query.filter(
|
||||
or_(
|
||||
Certificate.user_id == g.user.id,
|
||||
Certificate.owner.in_(sub_query)
|
||||
)
|
||||
)
|
||||
|
||||
if account_id:
|
||||
query = query.filter(Certificate.accounts.any(Account.id == account_id))
|
||||
|
||||
if time_range:
|
||||
to = arrow.now().replace(weeks=+time_range).format('YYYY-MM-DD')
|
||||
now = arrow.now().format('YYYY-MM-DD')
|
||||
query = query.filter(Certificate.not_after <= to).filter(Certificate.not_after >= now)
|
||||
|
||||
return database.sort_and_page(query, Certificate, args)
|
||||
|
||||
|
||||
def create_csr(csr_config):
|
||||
"""
|
||||
Given a list of domains create the appropriate csr
|
||||
for those domains
|
||||
|
||||
:param csr_config:
|
||||
"""
|
||||
|
||||
# we create a no colliding file name
|
||||
path = create_path(hashlib.md5(csr_config).hexdigest())
|
||||
|
||||
challenge = create_challenge()
|
||||
challenge_path = os.path.join(path, 'challenge.txt')
|
||||
|
||||
with open(challenge_path, 'w') as c:
|
||||
c.write(challenge)
|
||||
|
||||
csr_path = os.path.join(path, 'csr_config.txt')
|
||||
|
||||
with open(csr_path, 'w') as f:
|
||||
f.write(csr_config)
|
||||
|
||||
#TODO use cloudCA to seed a -rand file for each call
|
||||
#TODO replace openssl shell calls with cryptograph
|
||||
with open('/dev/null', 'w') as devnull:
|
||||
code = subprocess.call(['openssl', 'genrsa',
|
||||
'-out', os.path.join(path, 'private.key'), '2048'],
|
||||
stdout=devnull, stderr=devnull)
|
||||
|
||||
if code != 0:
|
||||
raise UnableToCreatePrivateKey(code)
|
||||
|
||||
with open('/dev/null', 'w') as devnull:
|
||||
code = subprocess.call(['openssl', 'req', '-new', '-sha256', '-nodes',
|
||||
'-config', csr_path, "-key", os.path.join(path, 'private.key'),
|
||||
"-out", os.path.join(path, 'request.csr')], stdout=devnull, stderr=devnull)
|
||||
|
||||
if code != 0:
|
||||
raise UnableToCreateCSR(code)
|
||||
|
||||
return path
|
||||
|
||||
|
||||
def create_path(domain_hash):
|
||||
"""
|
||||
|
||||
:param domain_hash:
|
||||
:return:
|
||||
"""
|
||||
path = os.path.join('/tmp', domain_hash)
|
||||
|
||||
try:
|
||||
os.mkdir(path)
|
||||
except OSError as e:
|
||||
now = datetime.datetime.now()
|
||||
path = os.path.join('/tmp', "{}.{}".format(domain_hash, now.strftime('%s')))
|
||||
os.mkdir(path)
|
||||
current_app.logger.warning(e)
|
||||
|
||||
current_app.logger.debug("Writing ssl files to: {}".format(path))
|
||||
return path
|
||||
|
||||
|
||||
def load_ssl_pack(path):
|
||||
"""
|
||||
Loads the information created by openssl to be used by other functions.
|
||||
|
||||
:param path:
|
||||
"""
|
||||
if len(os.listdir(path)) != 4:
|
||||
raise MissingFiles(path)
|
||||
|
||||
with open(os.path.join(path, 'challenge.txt')) as c:
|
||||
challenge = c.read()
|
||||
|
||||
with open(os.path.join(path, 'request.csr')) as r:
|
||||
csr = r.read()
|
||||
|
||||
with open(os.path.join(path, 'csr_config.txt')) as config:
|
||||
csr_config = config.read()
|
||||
|
||||
with open(os.path.join(path, 'private.key')) as key:
|
||||
private_key = key.read()
|
||||
|
||||
return (challenge, csr, csr_config, private_key,)
|
||||
|
||||
|
||||
def delete_ssl_pack(path):
|
||||
"""
|
||||
Removes the temporary files associated with CSR creation.
|
||||
|
||||
:param path:
|
||||
"""
|
||||
subprocess.check_call(['srm', '-r', path])
|
||||
|
||||
|
||||
def create_challenge():
|
||||
"""
|
||||
Create a random and strongish csr challenge.
|
||||
"""
|
||||
challenge = ''.join(random.choice(string.ascii_uppercase) for x in range(6))
|
||||
challenge += ''.join(random.choice("~!@#$%^&*()_+") for x in range(6))
|
||||
challenge += ''.join(random.choice(string.ascii_lowercase) for x in range(6))
|
||||
challenge += ''.join(random.choice(string.digits) for x in range(6))
|
||||
return challenge
|
||||
|
||||
|
||||
def stats(**kwargs):
|
||||
"""
|
||||
Helper that defines some useful statistics about certifications.
|
||||
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
query = database.session_query(Certificate)
|
||||
|
||||
if kwargs.get('active') == 'true':
|
||||
query = query.filter(Certificate.elb_listeners.any())
|
||||
|
||||
if kwargs.get('account_id'):
|
||||
query = query.filter(Certificate.accounts.any(Account.id == kwargs.get('account_id')))
|
||||
|
||||
if kwargs.get('metric') == 'not_after':
|
||||
start = arrow.utcnow()
|
||||
end = start.replace(weeks=+32)
|
||||
items = database.db.session.query(Certificate.issuer, func.count(Certificate.id))\
|
||||
.group_by(Certificate.issuer)\
|
||||
.filter(Certificate.not_after <= end.format('YYYY-MM-DD')) \
|
||||
.filter(Certificate.not_after >= start.format('YYYY-MM-DD')).all()
|
||||
|
||||
else:
|
||||
attr = getattr(Certificate, kwargs.get('metric'))
|
||||
query = database.db.session.query(attr, func.count(attr))
|
||||
|
||||
# TODO this could be cleaned up
|
||||
if kwargs.get('active') == 'true':
|
||||
query = query.filter(Certificate.elb_listeners.any())
|
||||
|
||||
items = query.group_by(attr).all()
|
||||
|
||||
keys = []
|
||||
values = []
|
||||
for key, count in items:
|
||||
keys.append(key)
|
||||
values.append(count)
|
||||
|
||||
return {'labels': keys, 'values': values}
|
||||
|
||||
|
169
lemur/certificates/sync.py
Normal file
169
lemur/certificates/sync.py
Normal file
@ -0,0 +1,169 @@
|
||||
"""
|
||||
.. module: sync
|
||||
:platform: Unix
|
||||
:synopsis: This module contains various certificate syncing operations.
|
||||
Because of the nature of the SSL environment there are multiple ways
|
||||
a certificate could be created without Lemur's knowledge. Lemur attempts
|
||||
to 'sync' with as many different datasources as possible to try and track
|
||||
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
|
||||
line or a cron job.
|
||||
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
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.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.common.services.issuers.manager import get_plugin_by_name
|
||||
|
||||
|
||||
def aws():
|
||||
"""
|
||||
Attempts to retrieve all certificates located in known AWS accounts
|
||||
:raise e:
|
||||
"""
|
||||
new = 0
|
||||
updated = 0
|
||||
|
||||
# all certificates 'discovered' by lemur are tracked by the lemur
|
||||
# user
|
||||
user = user_service.get_by_email('lemur@nobody')
|
||||
|
||||
# we don't need to check regions as IAM is a global service
|
||||
for account in account_service.get_all():
|
||||
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
|
||||
|
||||
elif len(existing) == 1: # we check to make sure we know about the current account for this certificate
|
||||
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
|
||||
|
||||
else:
|
||||
current_app.logger.error(
|
||||
"Multiple certificates with the same body found, unable to correctly determine which entry to update"
|
||||
)
|
||||
|
||||
# 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 = get_plugin_by_name('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))
|
135
lemur/certificates/verify.py
Normal file
135
lemur/certificates/verify.py
Normal file
@ -0,0 +1,135 @@
|
||||
"""
|
||||
.. module: lemur.certificates.verify
|
||||
: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 os
|
||||
import re
|
||||
import hashlib
|
||||
import requests
|
||||
import subprocess
|
||||
from OpenSSL import crypto
|
||||
|
||||
from flask import current_app
|
||||
|
||||
|
||||
def ocsp_verify(cert_path, issuer_chain_path):
|
||||
"""
|
||||
Attempts to verify a certificate via OCSP. OCSP is a more modern version
|
||||
of CRL in that it will query the OCSP URI in order to determine if the
|
||||
certificate as been revoked
|
||||
|
||||
:param cert_path:
|
||||
:param issuer_chain_path:
|
||||
:return bool: True if certificate is valid, False otherwise
|
||||
"""
|
||||
command = ['openssl', 'x509', '-noout', '-ocsp_uri', '-in', cert_path]
|
||||
p1 = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
url, err = p1.communicate()
|
||||
|
||||
p2 = subprocess.Popen(['openssl', 'ocsp', '-issuer', issuer_chain_path,
|
||||
'-cert', cert_path, "-url", url.strip()], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
|
||||
message, err = p2.communicate()
|
||||
if 'error' in message or 'Error' in message:
|
||||
raise Exception("Got error when parsing OCSP url")
|
||||
|
||||
elif 'revoked' in message:
|
||||
return
|
||||
|
||||
elif 'good' not in message:
|
||||
raise Exception("Did not receive a valid response")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def crl_verify(cert_path):
|
||||
"""
|
||||
Attempts to verify a certificate using CRL.
|
||||
|
||||
:param cert_path:
|
||||
:return: True if certificate is valid, False otherwise
|
||||
:raise Exception: If certificate does not have CRL
|
||||
"""
|
||||
s = "(http(s)?\://[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}/\S*?$)"
|
||||
regex = re.compile(s, re.MULTILINE)
|
||||
|
||||
x509 = crypto.load_certificate(crypto.FILETYPE_PEM, open(cert_path, 'rt').read())
|
||||
for x in range(x509.get_extension_count()):
|
||||
ext = x509.get_extension(x)
|
||||
if ext.get_short_name() == 'crlDistributionPoints':
|
||||
r = regex.search(ext.get_data())
|
||||
points = r.groups()
|
||||
break
|
||||
else:
|
||||
raise Exception("Certificate does not have a CRL distribution point")
|
||||
|
||||
for point in points:
|
||||
if point:
|
||||
response = requests.get(point)
|
||||
crl = crypto.load_crl(crypto.FILETYPE_ASN1, response.content)
|
||||
revoked = crl.get_revoked()
|
||||
for r in revoked:
|
||||
if x509.get_serial_number() == r.get_serial():
|
||||
return
|
||||
return True
|
||||
|
||||
|
||||
def verify(cert_path, issuer_chain_path):
|
||||
"""
|
||||
Verify a certificate using OCSP and CRL
|
||||
|
||||
:param cert_path:
|
||||
:param issuer_chain_path:
|
||||
:return: True if valid, False otherwise
|
||||
"""
|
||||
# OCSP is our main source of truth, in a lot of cases CRLs
|
||||
# have been deprecated and are no longer updated
|
||||
try:
|
||||
return ocsp_verify(cert_path, issuer_chain_path)
|
||||
except Exception as e:
|
||||
current_app.logger.debug("Could not use OCSP: {0}".format(e))
|
||||
try:
|
||||
return crl_verify(cert_path)
|
||||
except Exception as e:
|
||||
current_app.logger.debug("Could not use CRL: {0}".format(e))
|
||||
raise Exception("Failed to verify")
|
||||
raise Exception("Failed to verify")
|
||||
|
||||
|
||||
def make_tmp_file(string):
|
||||
"""
|
||||
Creates a temporary file for a given string
|
||||
|
||||
:param string:
|
||||
:return: Full file path to created file
|
||||
"""
|
||||
m = hashlib.md5()
|
||||
m.update(string)
|
||||
hexdigest = m.hexdigest()
|
||||
path = os.path.join(os.path.dirname(os.path.abspath(__file__)), hexdigest)
|
||||
with open(path, 'w') as f:
|
||||
f.write(string)
|
||||
return path
|
||||
|
||||
|
||||
def verify_string(cert_string, issuer_string):
|
||||
"""
|
||||
Verify a certificate given only it's string value
|
||||
|
||||
:param cert_string:
|
||||
:param issuer_string:
|
||||
:return: True if valid, False otherwise
|
||||
"""
|
||||
cert_path = make_tmp_file(cert_string)
|
||||
issuer_path = make_tmp_file(issuer_string)
|
||||
status = verify(cert_path, issuer_path)
|
||||
remove_tmp_file(cert_path)
|
||||
remove_tmp_file(issuer_path)
|
||||
return status
|
||||
|
||||
|
||||
def remove_tmp_file(file_path):
|
||||
os.remove(file_path)
|
575
lemur/certificates/views.py
Normal file
575
lemur/certificates/views.py
Normal file
@ -0,0 +1,575 @@
|
||||
"""
|
||||
.. module: lemur.certificates.views
|
||||
:platform: Unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
from flask import Blueprint, make_response, jsonify
|
||||
from flask.ext.restful import reqparse, Api, fields
|
||||
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
|
||||
from lemur.certificates import service
|
||||
from lemur.authorities.models import Authority
|
||||
|
||||
from lemur.auth.service import AuthenticatedResource
|
||||
from lemur.auth.permissions import ViewKeyPermission, AuthorityPermission, UpdateCertificatePermission
|
||||
|
||||
from lemur.roles import service as role_service
|
||||
|
||||
from lemur.common.utils import marshal_items, paginated_parser
|
||||
|
||||
|
||||
mod = Blueprint('certificates', __name__)
|
||||
api = Api(mod)
|
||||
|
||||
|
||||
FIELDS = {
|
||||
'name': fields.String,
|
||||
'id': fields.Integer,
|
||||
'bits': fields.Integer,
|
||||
'deleted': fields.String,
|
||||
'issuer': fields.String,
|
||||
'serial': fields.String,
|
||||
'owner': fields.String,
|
||||
'chain': fields.String,
|
||||
'san': fields.String,
|
||||
'active': fields.Boolean,
|
||||
'description': fields.String,
|
||||
'notBefore': fields.DateTime(dt_format='iso8601', attribute='not_before'),
|
||||
'notAfter': fields.DateTime(dt_format='iso8601', attribute='not_after'),
|
||||
'cn': fields.String,
|
||||
'status': fields.String,
|
||||
'body': fields.String
|
||||
}
|
||||
|
||||
|
||||
def valid_authority(authority_options):
|
||||
"""
|
||||
Defends against invalid authorities
|
||||
|
||||
:param authority_name:
|
||||
:return: :raise ValueError:
|
||||
"""
|
||||
name = authority_options['name']
|
||||
authority = Authority.query.filter(Authority.name == name).one()
|
||||
|
||||
if not authority:
|
||||
raise ValueError("Unable to find authority specified")
|
||||
|
||||
if not authority.active:
|
||||
raise ValueError("Selected authority [{0}] is not currently active".format(name))
|
||||
|
||||
return authority
|
||||
|
||||
|
||||
def pem_str(value, name):
|
||||
"""
|
||||
Used to validate that the given string is a PEM formatted string
|
||||
|
||||
:param value:
|
||||
:param name:
|
||||
:return: :raise ValueError:
|
||||
"""
|
||||
try:
|
||||
x509.load_pem_x509_certificate(str(value), default_backend())
|
||||
except Exception as e:
|
||||
raise ValueError("The parameter '{0}' needs to be a valid PEM string".format(name))
|
||||
return value
|
||||
|
||||
|
||||
def private_key_str(value, name):
|
||||
"""
|
||||
User to validate that a given string is a RSA private key
|
||||
|
||||
:param value:
|
||||
:param name:
|
||||
:return: :raise ValueError:
|
||||
"""
|
||||
try:
|
||||
serialization.load_pem_private_key(str(value), backend=default_backend())
|
||||
except Exception as e:
|
||||
raise ValueError("The parameter '{0}' needs to be a valid RSA private key".format(name))
|
||||
return value
|
||||
|
||||
|
||||
|
||||
class CertificatesList(AuthenticatedResource):
|
||||
""" Defines the 'certificates' endpoint """
|
||||
def __init__(self):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(CertificatesList, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
def get(self):
|
||||
"""
|
||||
.. http:get:: /certificates
|
||||
|
||||
The current list of certificates
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /certificates 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": 1,
|
||||
"name": "cert1",
|
||||
"description": "this is cert1",
|
||||
"bits": 2048,
|
||||
"deleted": false,
|
||||
"issuer": "ExampeInc.",
|
||||
"serial": "123450",
|
||||
"chain": "-----Begin ...",
|
||||
"body": "-----Begin ...",
|
||||
"san": true,
|
||||
"owner": 'bob@example.com",
|
||||
"active": true,
|
||||
"notBefore": "2015-06-05T17:09:39",
|
||||
"notAfter": "2015-06-10T17:09:39",
|
||||
"cn": "example.com",
|
||||
"status": "unknown"
|
||||
}
|
||||
]
|
||||
"total": 1
|
||||
}
|
||||
|
||||
:query sortBy: field to sort on
|
||||
:query sortDir: acs or desc
|
||||
:query page: int. default is 1
|
||||
:query filter: key value pair. format is k=v;
|
||||
:query limit: limit number. default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
"""
|
||||
parser = paginated_parser.copy()
|
||||
parser.add_argument('timeRange', type=int, dest='time_range', location='args')
|
||||
parser.add_argument('owner', type=bool, location='args')
|
||||
parser.add_argument('id', type=str, location='args')
|
||||
parser.add_argument('active', type=bool, location='args')
|
||||
parser.add_argument('accountId', type=int, dest="account_id", location='args')
|
||||
parser.add_argument('creator', type=str, location='args')
|
||||
parser.add_argument('show', type=str, location='args')
|
||||
|
||||
args = parser.parse_args()
|
||||
return service.render(args)
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
def post(self):
|
||||
"""
|
||||
.. http:post:: /certificates
|
||||
|
||||
Creates a new certificate
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /certificates HTTP/1.1
|
||||
Host: example.com
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
{
|
||||
"country": "US",
|
||||
"state": "CA",
|
||||
"location": "A Place",
|
||||
"organization": "ExampleInc.",
|
||||
"organizationalUnit": "Operations",
|
||||
"owner": "bob@example.com",
|
||||
"description": "test",
|
||||
"selectedAuthority": "timetest2",
|
||||
"authority": {
|
||||
"body": "-----BEGIN...",
|
||||
"name": "timetest2",
|
||||
"chain": "",
|
||||
"notBefore": "2015-06-05T15:20:59",
|
||||
"active": true,
|
||||
"id": 50,
|
||||
"notAfter": "2015-06-17T15:21:08",
|
||||
"description": "dsfdsf"
|
||||
},
|
||||
"extensions": {
|
||||
"basicConstraints": {},
|
||||
"keyUsage": {
|
||||
"isCritical": true,
|
||||
"useKeyEncipherment": true,
|
||||
"useDigitalSignature": true
|
||||
},
|
||||
"extendedKeyUsage": {
|
||||
"isCritical": true,
|
||||
"useServerAuthentication": true
|
||||
},
|
||||
"subjectKeyIdentifier": {
|
||||
"includeSKI": true
|
||||
},
|
||||
"subAltNames": {
|
||||
"names": []
|
||||
}
|
||||
},
|
||||
"commonName": "test",
|
||||
"validityStart": "2015-06-05T07:00:00.000Z",
|
||||
"validityEnd": "2015-06-16T07:00:00.000Z"
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"name": "cert1",
|
||||
"description": "this is cert1",
|
||||
"bits": 2048,
|
||||
"deleted": false,
|
||||
"issuer": "ExampeInc.",
|
||||
"serial": "123450",
|
||||
"chain": "-----Begin ...",
|
||||
"body": "-----Begin ...",
|
||||
"san": true,
|
||||
"owner": "jimbob@example.com",
|
||||
"active": false,
|
||||
"notBefore": "2015-06-05T17:09:39",
|
||||
"notAfter": "2015-06-10T17:09:39",
|
||||
"cn": "example.com",
|
||||
"status": "unknown"
|
||||
}
|
||||
|
||||
:arg extensions: extensions to be used in the certificate
|
||||
:arg description: description for new certificate
|
||||
:arg owner: owner email
|
||||
:arg validityStart: when the certificate should start being valid
|
||||
:arg validityEnd: when the certificate should expire
|
||||
:arg authority: authority that should issue the certificate
|
||||
:arg country: country for the CSR
|
||||
:arg state: state for the CSR
|
||||
:arg location: location for the CSR
|
||||
:arg organization: organization for CSR
|
||||
:arg commonName: certiifcate common name
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
"""
|
||||
self.reqparse.add_argument('extensions', type=dict, location='json')
|
||||
self.reqparse.add_argument('accounts', 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('validityStart', type=str, location='json') # parse date
|
||||
self.reqparse.add_argument('validityEnd', type=str, location='json') # parse date
|
||||
self.reqparse.add_argument('authority', type=valid_authority, location='json')
|
||||
self.reqparse.add_argument('description', type=str, location='json')
|
||||
self.reqparse.add_argument('country', type=str, location='json')
|
||||
self.reqparse.add_argument('state', type=str, location='json')
|
||||
self.reqparse.add_argument('location', type=str, location='json')
|
||||
self.reqparse.add_argument('organization', type=str, location='json')
|
||||
self.reqparse.add_argument('organizationalUnit', type=str, location='json')
|
||||
self.reqparse.add_argument('owner', type=str, location='json')
|
||||
self.reqparse.add_argument('commonName', type=str, location='json')
|
||||
|
||||
args = self.reqparse.parse_args()
|
||||
|
||||
authority = args['authority']
|
||||
role = role_service.get_by_name(authority.owner)
|
||||
|
||||
# all the authority role members should be allowed
|
||||
roles = [x.name for x in authority.roles]
|
||||
|
||||
# allow "owner" roles by team DL
|
||||
roles.append(role)
|
||||
permission = AuthorityPermission(authority.id, roles)
|
||||
|
||||
if permission.can():
|
||||
return service.create(**args)
|
||||
|
||||
return dict(message="You are not authorized to use {0}".format(args['authority'].name)), 403
|
||||
|
||||
|
||||
class CertificatesUpload(AuthenticatedResource):
|
||||
""" Defines the 'certificates' upload endpoint """
|
||||
def __init__(self):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(CertificatesUpload, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
def post(self):
|
||||
"""
|
||||
.. http:post:: /certificates/upload
|
||||
|
||||
Upload a certificate
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /certificates/upload HTTP/1.1
|
||||
Host: example.com
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
{
|
||||
"owner": "joe@exmaple.com",
|
||||
"publicCert": "---Begin Public...",
|
||||
"intermediateCert": "---Begin Public...",
|
||||
"privateKey": "---Begin Private..."
|
||||
"accounts": []
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"name": "cert1",
|
||||
"description": "this is cert1",
|
||||
"bits": 2048,
|
||||
"deleted": false,
|
||||
"issuer": "ExampeInc.",
|
||||
"serial": "123450",
|
||||
"chain": "-----Begin ...",
|
||||
"body": "-----Begin ...",
|
||||
"san": true,
|
||||
"owner": "joe@example.com",
|
||||
"active": true,
|
||||
"notBefore": "2015-06-05T17:09:39",
|
||||
"notAfter": "2015-06-10T17:09:39",
|
||||
"cn": "example.com",
|
||||
"status": "unknown"
|
||||
}
|
||||
|
||||
:arg owner: owner email for certificate
|
||||
:arg publicCert: valid PEM public key for certificate
|
||||
:arg intermediateCert valid PEM intermediate key for certificate
|
||||
:arg privateKey: valid PEM private key for certificate
|
||||
:arg accounts: list of aws accounts to upload the certificate to
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 403: unauthenticated
|
||||
:statuscode 200: no error
|
||||
"""
|
||||
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('accounts', type=list, dest='accounts', 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')
|
||||
|
||||
args = self.reqparse.parse_args()
|
||||
if args.get('accounts'):
|
||||
if args.get('private_key'):
|
||||
return service.upload(**args)
|
||||
else:
|
||||
raise Exception("Private key must be provided in order to upload certificate to AWS")
|
||||
return service.upload(**args)
|
||||
|
||||
|
||||
class CertificatesStats(AuthenticatedResource):
|
||||
""" Defines the 'certificates' stats endpoint """
|
||||
def __init__(self):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(CertificatesStats, self).__init__()
|
||||
|
||||
def get(self):
|
||||
self.reqparse.add_argument('metric', type=str, 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('active', type=str, default='true', location='args')
|
||||
|
||||
args = self.reqparse.parse_args()
|
||||
|
||||
items = service.stats(**args)
|
||||
return dict(items=items, total=len(items))
|
||||
|
||||
|
||||
class CertificatePrivateKey(AuthenticatedResource):
|
||||
def __init__(self):
|
||||
super(CertificatePrivateKey, self).__init__()
|
||||
|
||||
def get(self, certificate_id):
|
||||
"""
|
||||
.. http:get:: /certificates/1/key
|
||||
|
||||
Retrieves the private key for a given certificate
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /certificates/1/key 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
|
||||
|
||||
{
|
||||
"key": "----Begin ...",
|
||||
}
|
||||
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
"""
|
||||
cert = service.get(certificate_id)
|
||||
role = role_service.get_by_name(cert.owner)
|
||||
|
||||
permission = ViewKeyPermission(certificate_id, hasattr(role, 'id'))
|
||||
|
||||
if permission.can():
|
||||
response = make_response(jsonify(key=cert.private_key), 200)
|
||||
response.headers['cache-control'] = 'private, max-age=0, no-cache, no-store'
|
||||
response.headers['pragma'] = 'no-cache'
|
||||
return response
|
||||
|
||||
return dict(message='You are not authorized to view this key'), 403
|
||||
|
||||
|
||||
class Certificates(AuthenticatedResource):
|
||||
def __init__(self):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(Certificates, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
def get(self, certificate_id):
|
||||
"""
|
||||
.. http:get:: /certificates/1
|
||||
|
||||
One certificate
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /certificates/1 HTTP/1.1
|
||||
Host: example.com
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"name": "cert1",
|
||||
"description": "this is cert1",
|
||||
"bits": 2048,
|
||||
"deleted": false,
|
||||
"issuer": "ExampeInc.",
|
||||
"serial": "123450",
|
||||
"chain": "-----Begin ...",
|
||||
"body": "-----Begin ...",
|
||||
"san": true,
|
||||
"owner": "bob@example.com",
|
||||
"active": true,
|
||||
"notBefore": "2015-06-05T17:09:39",
|
||||
"notAfter": "2015-06-10T17:09:39",
|
||||
"cn": "example.com",
|
||||
"status": "unknown"
|
||||
}
|
||||
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
"""
|
||||
return service.get(certificate_id)
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
def put(self, certificate_id):
|
||||
"""
|
||||
.. http:put:: /certificates/1
|
||||
|
||||
Update a certificate
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PUT /certificates/1 HTTP/1.1
|
||||
Host: example.com
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
{
|
||||
"owner": "jimbob@example.com",
|
||||
"active": false
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"name": "cert1",
|
||||
"description": "this is cert1",
|
||||
"bits": 2048,
|
||||
"deleted": false,
|
||||
"issuer": "ExampeInc.",
|
||||
"serial": "123450",
|
||||
"chain": "-----Begin ...",
|
||||
"body": "-----Begin ...",
|
||||
"san": true,
|
||||
"owner": "jimbob@example.com",
|
||||
"active": false,
|
||||
"notBefore": "2015-06-05T17:09:39",
|
||||
"notAfter": "2015-06-10T17:09:39",
|
||||
"cn": "example.com",
|
||||
"status": "unknown"
|
||||
}
|
||||
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
"""
|
||||
self.reqparse.add_argument('active', type=bool, location='json')
|
||||
self.reqparse.add_argument('owner', type=str, location='json')
|
||||
args = self.reqparse.parse_args()
|
||||
|
||||
cert = service.get(certificate_id)
|
||||
role = role_service.get_by_name(cert.owner)
|
||||
permission = UpdateCertificatePermission(certificate_id, hasattr(role, 'id'))
|
||||
|
||||
if permission.can():
|
||||
return service.update(certificate_id, args['owner'], args['active'])
|
||||
|
||||
return dict(message='You are not authorized to update this certificate'), 403
|
||||
|
||||
|
||||
api.add_resource(CertificatesList, '/certificates', endpoint='certificates')
|
||||
api.add_resource(Certificates, '/certificates/<int:certificate_id>', endpoint='certificate')
|
||||
api.add_resource(CertificatesStats, '/certificates/stats', endpoint='certificateStats')
|
||||
api.add_resource(CertificatesUpload, '/certificates/upload', endpoint='certificateUpload')
|
||||
api.add_resource(CertificatePrivateKey, '/certificates/<int:certificate_id>/key', endpoint='privateKeyCertificates')
|
0
lemur/common/__init__.py
Normal file
0
lemur/common/__init__.py
Normal file
185
lemur/common/crypto.py
Normal file
185
lemur/common/crypto.py
Normal file
@ -0,0 +1,185 @@
|
||||
"""
|
||||
.. module: lemur.common.crypto
|
||||
:platform: Unix
|
||||
:synopsis: This module contains all cryptographic function's in Lemur
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
|
||||
"""
|
||||
import os
|
||||
import ssl
|
||||
import StringIO
|
||||
import functools
|
||||
from Crypto import Random
|
||||
from Crypto.Cipher import AES
|
||||
from hashlib import sha512
|
||||
|
||||
from flask import current_app
|
||||
|
||||
from lemur.factory import create_app
|
||||
|
||||
|
||||
old_init = ssl.SSLSocket.__init__
|
||||
|
||||
@functools.wraps(old_init)
|
||||
def ssl_bug(self, *args, **kwargs):
|
||||
kwargs['ssl_version'] = ssl.PROTOCOL_TLSv1
|
||||
old_init(self, *args, **kwargs)
|
||||
|
||||
ssl.SSLSocket.__init__ = ssl_bug
|
||||
|
||||
|
||||
def derive_key_and_iv(password, salt, key_length, iv_length):
|
||||
"""
|
||||
Derives the key and iv from the password and salt.
|
||||
|
||||
:param password:
|
||||
:param salt:
|
||||
:param key_length:
|
||||
:param iv_length:
|
||||
:return: key, iv
|
||||
"""
|
||||
d = d_i = ''
|
||||
|
||||
while len(d) < key_length + iv_length:
|
||||
d_i = sha512(d_i + password + salt).digest()
|
||||
d += d_i
|
||||
|
||||
return d[:key_length], d[key_length:key_length+iv_length]
|
||||
|
||||
|
||||
def encrypt(in_file, out_file, password, key_length=32):
|
||||
"""
|
||||
Encrypts a file.
|
||||
|
||||
:param in_file:
|
||||
:param out_file:
|
||||
:param password:
|
||||
:param key_length:
|
||||
"""
|
||||
bs = AES.block_size
|
||||
salt = Random.new().read(bs - len('Salted__'))
|
||||
key, iv = derive_key_and_iv(password, salt, key_length, bs)
|
||||
cipher = AES.new(key, AES.MODE_CBC, iv)
|
||||
out_file.write('Salted__' + salt)
|
||||
finished = False
|
||||
while not finished:
|
||||
chunk = in_file.read(1024 * bs)
|
||||
if len(chunk) == 0 or len(chunk) % bs != 0:
|
||||
padding_length = bs - (len(chunk) % bs)
|
||||
chunk += padding_length * chr(padding_length)
|
||||
finished = True
|
||||
out_file.write(cipher.encrypt(chunk))
|
||||
|
||||
|
||||
def decrypt(in_file, out_file, password, key_length=32):
|
||||
"""
|
||||
Decrypts a file.
|
||||
|
||||
:param in_file:
|
||||
:param out_file:
|
||||
:param password:
|
||||
:param key_length:
|
||||
:raise ValueError:
|
||||
"""
|
||||
bs = AES.block_size
|
||||
salt = in_file.read(bs)[len('Salted__'):]
|
||||
key, iv = derive_key_and_iv(password, salt, key_length, bs)
|
||||
cipher = AES.new(key, AES.MODE_CBC, iv)
|
||||
next_chunk = ''
|
||||
finished = False
|
||||
while not finished:
|
||||
chunk, next_chunk = next_chunk, cipher.decrypt(in_file.read(1024 * bs))
|
||||
if len(next_chunk) == 0:
|
||||
padding_length = ord(chunk[-1])
|
||||
if padding_length < 1 or padding_length > bs:
|
||||
raise ValueError("bad decrypt pad (%d)" % padding_length)
|
||||
# all the pad-bytes must be the same
|
||||
if chunk[-padding_length:] != (padding_length * chr(padding_length)):
|
||||
# this is similar to the bad decrypt:evp_enc.c from openssl program
|
||||
raise ValueError("bad decrypt")
|
||||
chunk = chunk[:-padding_length]
|
||||
finished = True
|
||||
out_file.write(chunk)
|
||||
|
||||
|
||||
def encrypt_string(string, password):
|
||||
"""
|
||||
Encrypts a string.
|
||||
|
||||
:param string:
|
||||
:param password:
|
||||
:return:
|
||||
"""
|
||||
in_file = StringIO.StringIO(string)
|
||||
enc_file = StringIO.StringIO()
|
||||
encrypt(in_file, enc_file, password)
|
||||
enc_file.seek(0)
|
||||
return enc_file.read()
|
||||
|
||||
|
||||
def decrypt_string(string, password):
|
||||
"""
|
||||
Decrypts a string.
|
||||
|
||||
:param string:
|
||||
:param password:
|
||||
:return:
|
||||
"""
|
||||
in_file = StringIO.StringIO(string)
|
||||
out_file = StringIO.StringIO()
|
||||
decrypt(in_file, out_file, password)
|
||||
out_file.seek(0)
|
||||
return out_file.read()
|
||||
|
||||
|
||||
def lock(password):
|
||||
"""
|
||||
Encrypts Lemur's KEY_PATH. This directory can be used to store secrets needed for normal
|
||||
Lemur operation. This is especially useful for storing secrets needed for communication
|
||||
with third parties (e.g. external certificate authorities).
|
||||
|
||||
Lemur does not assume anything about the contents of the directory and will attempt to
|
||||
encrypt all files contained within. Currently this has only been tested against plain
|
||||
text files.
|
||||
|
||||
:param password:
|
||||
"""
|
||||
dest_dir = os.path.join(current_app.config.get("KEY_PATH"), "encrypted")
|
||||
|
||||
if not os.path.exists(dest_dir):
|
||||
current_app.logger.debug("Creating encryption directory: {0}".format(dest_dir))
|
||||
os.makedirs(dest_dir)
|
||||
|
||||
for root, dirs, files in os.walk(os.path.join(current_app.config.get("KEY_PATH"), 'decrypted')):
|
||||
for f in files:
|
||||
source = os.path.join(root, f)
|
||||
dest = os.path.join(dest_dir, f + ".enc")
|
||||
with open(source, 'rb') as in_file, open(dest, 'wb') as out_file:
|
||||
encrypt(in_file, out_file, password)
|
||||
|
||||
|
||||
def unlock(password):
|
||||
"""
|
||||
Decrypts Lemur's KEY_PATH, allowing lemur to use the secrets within.
|
||||
|
||||
This reverses the :func:`lock` function.
|
||||
|
||||
:param password:
|
||||
"""
|
||||
dest_dir = os.path.join(current_app.config.get("KEY_PATH"), "decrypted")
|
||||
source_dir = os.path.join(current_app.config.get("KEY_PATH"), "encrypted")
|
||||
|
||||
if not os.path.exists(dest_dir):
|
||||
current_app.logger.debug("Creating decryption directory: {0}".format(dest_dir))
|
||||
os.makedirs(dest_dir)
|
||||
|
||||
for root, dirs, files in os.walk(source_dir):
|
||||
for f in files:
|
||||
source = os.path.join(source_dir, f)
|
||||
dest = os.path.join(dest_dir, ".".join(f.split(".")[:-1]))
|
||||
with open(source, 'rb') as in_file, open(dest, 'wb') as out_file:
|
||||
current_app.logger.debug("Writing file: {0} Source: {1}".format(dest, source))
|
||||
decrypt(in_file, out_file, password)
|
||||
|
15
lemur/common/health.py
Normal file
15
lemur/common/health.py
Normal file
@ -0,0 +1,15 @@
|
||||
"""
|
||||
.. module: lemur.common.health
|
||||
:platform: Unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
from flask import Blueprint
|
||||
|
||||
mod = Blueprint('healthCheck', __name__)
|
||||
|
||||
@mod.route('/healthcheck')
|
||||
def health():
|
||||
return 'ok'
|
0
lemur/common/services/__init__.py
Normal file
0
lemur/common/services/__init__.py
Normal file
0
lemur/common/services/aws/__init__.py
Normal file
0
lemur/common/services/aws/__init__.py
Normal file
140
lemur/common/services/aws/elb.py
Normal file
140
lemur/common/services/aws/elb.py
Normal file
@ -0,0 +1,140 @@
|
||||
"""
|
||||
.. module:: elb
|
||||
:synopsis: Module contains some often used and helpful classes that
|
||||
are used to deal with ELBs
|
||||
|
||||
.. moduleauthor:: Kevin Glisson (kglisson@netflix.com)
|
||||
"""
|
||||
import boto.ec2
|
||||
|
||||
from flask import current_app
|
||||
|
||||
from lemur.exceptions import InvalidListener
|
||||
from lemur.common.services.aws.sts import assume_service
|
||||
|
||||
|
||||
def is_valid(listener_tuple):
|
||||
"""
|
||||
There are a few rules that aws has when creating listeners,
|
||||
this function ensures those rules are met before we try and create
|
||||
or update a listener.
|
||||
|
||||
While these could be caught with boto exception handling, I would
|
||||
rather be nice and catch these early before we sent them out to aws.
|
||||
It also gives us an opportunity to create nice user warnings.
|
||||
|
||||
This validity check should also be checked in the frontend
|
||||
but must also be enforced by server.
|
||||
|
||||
:param listener_tuple:
|
||||
"""
|
||||
|
||||
current_app.logger.debug(listener_tuple)
|
||||
lb_port, i_port, lb_protocol, arn = listener_tuple
|
||||
current_app.logger.debug(lb_protocol)
|
||||
if lb_protocol.lower() in ['ssl', 'https']:
|
||||
if not arn:
|
||||
raise InvalidListener
|
||||
|
||||
return listener_tuple
|
||||
|
||||
def get_all_regions():
|
||||
"""
|
||||
Retrieves all current EC2 regions.
|
||||
|
||||
:return:
|
||||
"""
|
||||
regions = []
|
||||
for r in boto.ec2.regions():
|
||||
regions.append(r.name)
|
||||
return regions
|
||||
|
||||
def get_all_elbs(account_number, region):
|
||||
"""
|
||||
Fetches all elb objects for a given account and region.
|
||||
|
||||
:param account_number:
|
||||
:param region:
|
||||
"""
|
||||
marker = None
|
||||
elbs = []
|
||||
return assume_service(account_number, 'elb', region).get_all_load_balancers()
|
||||
# TODO create pull request for boto to include elb marker support
|
||||
# while True:
|
||||
# app.logger.debug(response.__dict__)
|
||||
# raise Exception
|
||||
# result = response['list_server_certificates_response']['list_server_certificates_result']
|
||||
#
|
||||
# for elb in result['server_certificate_metadata_list']:
|
||||
# elbs.append(elb)
|
||||
#
|
||||
# if result['is_truncated'] == 'true':
|
||||
# marker = result['marker']
|
||||
# else:
|
||||
# return elbs
|
||||
|
||||
|
||||
|
||||
def attach_certificate(account_number, region, name, port, certificate_id):
|
||||
"""
|
||||
Attaches a certificate to a listener, throws exception
|
||||
if certificate specified does not exist in a particular account.
|
||||
|
||||
:param account_number:
|
||||
:param region:
|
||||
:param name:
|
||||
:param port:
|
||||
:param certificate_id:
|
||||
"""
|
||||
return assume_service(account_number, 'elb', region).set_lb_listener_SSL_certificate(name, port, certificate_id)
|
||||
|
||||
|
||||
def create_new_listeners(account_number, region, name, listeners=None):
|
||||
"""
|
||||
Creates a new listener and attaches it to the ELB.
|
||||
|
||||
:param account_number:
|
||||
:param region:
|
||||
:param name:
|
||||
:param listeners:
|
||||
:return:
|
||||
"""
|
||||
listeners = [is_valid(x) for x in listeners]
|
||||
return assume_service(account_number, 'elb', region).create_load_balancer_listeners(name, listeners=listeners)
|
||||
|
||||
|
||||
def update_listeners(account_number, region, name, listeners, ports):
|
||||
"""
|
||||
We assume that a listener with a specified port already exists. We can then
|
||||
delete the old listener on the port and create a new one in it's place.
|
||||
|
||||
If however we are replacing a listener e.g. changing a port from 80 to 443 we need
|
||||
to make sure we kept track of which ports we needed to delete so that we don't create
|
||||
two listeners (one 80 and one 443)
|
||||
|
||||
:param account_number:
|
||||
:param region:
|
||||
:param name:
|
||||
:param listeners:
|
||||
:param ports:
|
||||
"""
|
||||
# you cannot update a listeners port/protocol instead we remove the only one and
|
||||
# create a new one in it's place
|
||||
listeners = [is_valid(x) for x in listeners]
|
||||
|
||||
assume_service(account_number, 'elb', region).delete_load_balancer_listeners(name, ports)
|
||||
return create_new_listeners(account_number, region, name, listeners=listeners)
|
||||
|
||||
|
||||
def delete_listeners(account_number, region, name, ports):
|
||||
"""
|
||||
Deletes a listener from an ELB.
|
||||
|
||||
:param account_number:
|
||||
:param region:
|
||||
:param name:
|
||||
:param ports:
|
||||
:return:
|
||||
"""
|
||||
return assume_service(account_number, 'elb', region).delete_load_balancer_listeners(name, ports)
|
||||
|
104
lemur/common/services/aws/iam.py
Normal file
104
lemur/common/services/aws/iam.py
Normal file
@ -0,0 +1,104 @@
|
||||
"""
|
||||
.. module: lemur.common.services.aws.iam
|
||||
:platform: Unix
|
||||
:synopsis: Contains helper functions for interactive with AWS IAM Apis.
|
||||
: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 current_app
|
||||
from lemur.common.services.aws.sts import assume_service
|
||||
|
||||
|
||||
def ssl_split(param_string):
|
||||
"""
|
||||
|
||||
:param param_string:
|
||||
:return:
|
||||
"""
|
||||
output = {}
|
||||
parts = str(param_string).split("/")
|
||||
for part in parts:
|
||||
if "=" in part:
|
||||
key, value = part.split("=", 1)
|
||||
output[key] = value
|
||||
return output
|
||||
|
||||
|
||||
def upload_cert(account_number, cert, private_key, cert_chain=None):
|
||||
"""
|
||||
Upload a certificate to AWS
|
||||
|
||||
:param account_number:
|
||||
:param cert:
|
||||
:param private_key:
|
||||
:param cert_chain:
|
||||
:return:
|
||||
"""
|
||||
return assume_service(account_number, 'iam').upload_server_cert(cert.name, str(cert.body), str(private_key), cert_chain=str(cert_chain))
|
||||
|
||||
|
||||
def delete_cert(account_number, cert):
|
||||
"""
|
||||
Delete a certificate from AWS
|
||||
|
||||
:param account_number:
|
||||
:param cert:
|
||||
:return:
|
||||
"""
|
||||
return assume_service(account_number, 'iam').delete_server_cert(cert.name)
|
||||
|
||||
|
||||
def get_all_server_certs(account_number):
|
||||
"""
|
||||
Use STS to fetch all of the SSL certificates from a given account
|
||||
|
||||
:param account_number:
|
||||
"""
|
||||
marker = None
|
||||
certs = []
|
||||
while True:
|
||||
response = assume_service(account_number, 'iam').get_all_server_certs(marker=marker)
|
||||
result = response['list_server_certificates_response']['list_server_certificates_result']
|
||||
|
||||
for cert in result['server_certificate_metadata_list']:
|
||||
certs.append(cert)
|
||||
|
||||
if result['is_truncated'] == 'true':
|
||||
marker = result['marker']
|
||||
else:
|
||||
return certs
|
||||
|
||||
|
||||
def get_cert_from_arn(arn):
|
||||
"""
|
||||
Retrieves an SSL certificate from a given ARN.
|
||||
|
||||
:param arn:
|
||||
:return:
|
||||
"""
|
||||
name = arn.split("/", 1)[1]
|
||||
account_number = arn.split(":")[4]
|
||||
name = name.split("/")[-1]
|
||||
|
||||
response = assume_service(account_number, 'iam').get_server_certificate(name.strip())
|
||||
return digest_aws_cert_response(response)
|
||||
|
||||
|
||||
def digest_aws_cert_response(response):
|
||||
"""
|
||||
Processes an AWS certifcate response and retrieves the certificate body and chain.
|
||||
|
||||
:param response:
|
||||
:return:
|
||||
"""
|
||||
chain = None
|
||||
cert = response['get_server_certificate_response']['get_server_certificate_result']['server_certificate']
|
||||
body = cert['certificate_body']
|
||||
|
||||
if 'certificate_chain' in cert:
|
||||
chain = cert['certificate_chain']
|
||||
|
||||
return str(body), str(chain),
|
||||
|
||||
|
29
lemur/common/services/aws/ses.py
Normal file
29
lemur/common/services/aws/ses.py
Normal file
@ -0,0 +1,29 @@
|
||||
"""
|
||||
.. module: lemur.common.services.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 flask import current_app
|
||||
import boto.ses
|
||||
|
||||
from lemur.templates.config import env
|
||||
|
||||
|
||||
def send(subject, data, email_type, recipients):
|
||||
"""
|
||||
Configures all Lemur email messaging
|
||||
|
||||
:param subject:
|
||||
:param data:
|
||||
:param email_type:
|
||||
:param recipients:
|
||||
"""
|
||||
conn = boto.connect_ses()
|
||||
#jinja template depending on type
|
||||
template = env.get_template('{}.html'.format(email_type))
|
||||
body = template.render(**data)
|
||||
conn.send_email(current_app.config.get("LEMUR_EMAIL"), subject, body, recipients, format='html')
|
||||
|
41
lemur/common/services/aws/sts.py
Normal file
41
lemur/common/services/aws/sts.py
Normal file
@ -0,0 +1,41 @@
|
||||
"""
|
||||
.. module: lemur.common.services.aws.sts
|
||||
: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 boto
|
||||
import boto.ec2.elb
|
||||
|
||||
from flask import current_app
|
||||
|
||||
|
||||
def assume_service(account_number, service, region=None):
|
||||
conn = boto.connect_sts()
|
||||
|
||||
role = conn.assume_role('arn:aws:iam::{0}:role/{1}'.format(
|
||||
account_number, current_app.config.get('LEMUR_INSTANCE_PROFILE', 'Lemur')), 'blah')
|
||||
|
||||
if service in 'iam':
|
||||
return boto.connect_iam(
|
||||
aws_access_key_id=role.credentials.access_key,
|
||||
aws_secret_access_key=role.credentials.secret_key,
|
||||
security_token=role.credentials.session_token)
|
||||
|
||||
elif service in 'elb':
|
||||
return boto.ec2.elb.connect_to_region(
|
||||
region,
|
||||
aws_access_key_id=role.credentials.access_key,
|
||||
aws_secret_access_key=role.credentials.secret_key,
|
||||
security_token=role.credentials.session_token)
|
||||
|
||||
elif service in 'vpc':
|
||||
return boto.connect_vpc(
|
||||
aws_access_key_id=role.credentials.access_key,
|
||||
aws_secret_access_key=role.credentials.secret_key,
|
||||
security_token=role.credentials.session_token)
|
||||
|
||||
|
||||
|
||||
|
0
lemur/common/services/issuers/__init__.py
Normal file
0
lemur/common/services/issuers/__init__.py
Normal file
32
lemur/common/services/issuers/issuer.py
Normal file
32
lemur/common/services/issuers/issuer.py
Normal file
@ -0,0 +1,32 @@
|
||||
"""
|
||||
.. module: authority
|
||||
:platform: Unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
from flask import current_app
|
||||
|
||||
|
||||
class Issuer(object):
|
||||
"""
|
||||
This is the base class from which all of the supported
|
||||
issuers will inherit from.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.dry_run = current_app.config.get('DRY_RUN')
|
||||
|
||||
def create_certificate(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def create_authority(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def get_authorities(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def get_csr_config(self):
|
||||
raise NotImplementedError
|
||||
|
37
lemur/common/services/issuers/manager.py
Normal file
37
lemur/common/services/issuers/manager.py
Normal file
@ -0,0 +1,37 @@
|
||||
"""
|
||||
.. module: lemur.common.services.issuers.manager
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
|
||||
.. moduleauthor:: Kevin Glisson (kglisson@netflix.com)
|
||||
"""
|
||||
import pkgutil
|
||||
from importlib import import_module
|
||||
|
||||
from flask import current_app
|
||||
|
||||
from lemur.common.services.issuers import plugins
|
||||
|
||||
# TODO make the plugin dir configurable
|
||||
def get_plugin_by_name(plugin_name):
|
||||
"""
|
||||
Fetches a given plugin by it's name. We use a known location for issuer plugins and attempt
|
||||
to load it such that it can be used for issuing certificates.
|
||||
|
||||
:param plugin_name:
|
||||
:return: a plugin `class` :raise Exception: Generic error whenever the plugin specified can not be found.
|
||||
"""
|
||||
for importer, modname, ispkg in pkgutil.iter_modules(plugins.__path__):
|
||||
try:
|
||||
issuer = import_module('lemur.common.services.issuers.plugins.{0}.{0}'.format(modname))
|
||||
if issuer.__name__ == plugin_name:
|
||||
# we shouldn't return bad issuers
|
||||
issuer_obj = issuer.init()
|
||||
return issuer_obj
|
||||
except Exception as e:
|
||||
current_app.logger.warn("Issuer {0} was unable to be imported: {1}".format(modname, e))
|
||||
|
||||
else:
|
||||
raise Exception("Could not find the specified plugin: {0}".format(plugin_name))
|
||||
|
||||
|
0
lemur/common/services/issuers/plugins/__init__.py
Normal file
0
lemur/common/services/issuers/plugins/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
try:
|
||||
VERSION = __import__('pkg_resources') \
|
||||
.get_distribution(__name__).version
|
||||
except Exception, e:
|
||||
VERSION = 'unknown'
|
346
lemur/common/services/issuers/plugins/cloudca/cloudca.py
Normal file
346
lemur/common/services/issuers/plugins/cloudca/cloudca.py
Normal file
@ -0,0 +1,346 @@
|
||||
"""
|
||||
.. module: lemur.common.services.issuers.plugins.cloudca
|
||||
: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 ssl
|
||||
import base64
|
||||
from json import dumps
|
||||
|
||||
import arrow
|
||||
import requests
|
||||
from requests.adapters import HTTPAdapter
|
||||
|
||||
from flask import current_app
|
||||
|
||||
from lemur.exceptions import LemurException
|
||||
from lemur.common.services.issuers.issuer import Issuer
|
||||
|
||||
from lemur.common.services.issuers.plugins import cloudca
|
||||
|
||||
|
||||
from lemur.authorities import service as authority_service
|
||||
|
||||
API_ENDPOINT = '/v1/ca/netflix'
|
||||
|
||||
|
||||
class CloudCAException(LemurException):
|
||||
def __init__(self, message):
|
||||
self.message = message
|
||||
current_app.logger.error(self)
|
||||
|
||||
def __str__(self):
|
||||
return repr("CloudCA request failed: {0}".format(self.message))
|
||||
|
||||
|
||||
class CloudCAHostNameCheckingAdapter(HTTPAdapter):
|
||||
def cert_verify(self, conn, url, verify, cert):
|
||||
super(CloudCAHostNameCheckingAdapter, self).cert_verify(conn, url, verify, cert)
|
||||
conn.assert_hostname = False
|
||||
|
||||
|
||||
def remove_none(options):
|
||||
"""
|
||||
Simple function that traverse the options and removed any None items
|
||||
CloudCA really dislikes null values.
|
||||
|
||||
:param options:
|
||||
:return:
|
||||
"""
|
||||
new_dict = {}
|
||||
for k, v in options.items():
|
||||
if v:
|
||||
new_dict[k] = v
|
||||
|
||||
# this is super hacky and gross, cloudca doesn't like null values
|
||||
if new_dict.get('extensions'):
|
||||
if len(new_dict['extensions']['subAltNames']['names']) == 0:
|
||||
del new_dict['extensions']['subAltNames']
|
||||
|
||||
return new_dict
|
||||
|
||||
|
||||
def get_default_issuance(options):
|
||||
"""
|
||||
Gets the default time range for certificates
|
||||
|
||||
:param options:
|
||||
:return:
|
||||
"""
|
||||
if not options.get('validityStart') and not options.get('validityEnd'):
|
||||
start = arrow.utcnow()
|
||||
options['validityStart'] = start.floor('second').isoformat()
|
||||
options['validityEnd'] = start.replace(years=current_app.config.get('CLOUDCA_DEFAULT_VALIDITY')).ceil('second').isoformat()
|
||||
return options
|
||||
|
||||
|
||||
def convert_to_pem(der):
|
||||
"""
|
||||
Converts DER to PEM Lemur uses PEM internally
|
||||
|
||||
:param der:
|
||||
:return:
|
||||
"""
|
||||
decoded = base64.b64decode(der)
|
||||
return ssl.DER_cert_to_PEM_cert(decoded)
|
||||
|
||||
|
||||
def convert_date_to_utc_time(date):
|
||||
"""
|
||||
Converts a python `datetime` object to the current date + current time in UTC.
|
||||
|
||||
:param date:
|
||||
:return:
|
||||
"""
|
||||
d = arrow.get(date)
|
||||
return arrow.utcnow().replace(day=d.naive.day).replace(month=d.naive.month).replace(year=d.naive.year).replace(microsecond=0)
|
||||
|
||||
|
||||
def process_response(response):
|
||||
"""
|
||||
Helper function that processes responses from CloudCA.
|
||||
|
||||
:param response:
|
||||
:return: :raise CloudCAException:
|
||||
"""
|
||||
if response.status_code == 200:
|
||||
res = response.json()
|
||||
if res['returnValue'] != 'success':
|
||||
current_app.logger.debug(res)
|
||||
if res.get('data'):
|
||||
raise CloudCAException(" ".join([res['returnMessage'], res['data']['dryRunResultMessage']]))
|
||||
else:
|
||||
raise CloudCAException(res['returnMessage'])
|
||||
else:
|
||||
raise CloudCAException("There was an error with your request: {0}".format(response.status_code))
|
||||
|
||||
return response.json()
|
||||
|
||||
|
||||
def get_auth_data(ca_name):
|
||||
"""
|
||||
Creates the authentication record needed to authenticate a user request to CloudCA.
|
||||
|
||||
:param ca_name:
|
||||
:return: :raise CloudCAException:
|
||||
"""
|
||||
role = authority_service.get_authority_role(ca_name)
|
||||
if role:
|
||||
return {
|
||||
"authInfo": {
|
||||
"credType": "password",
|
||||
"credentials": {
|
||||
"username": role.username,
|
||||
"password": role.password # we only decrypt when we need to
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
raise CloudCAException("You do not have the required role to issue certificates from {0}".format(ca_name))
|
||||
|
||||
|
||||
class CloudCA(Issuer):
|
||||
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):
|
||||
self.session = requests.Session()
|
||||
self.session.mount('https://', CloudCAHostNameCheckingAdapter())
|
||||
self.url = current_app.config.get('CLOUDCA_URL')
|
||||
|
||||
if current_app.config.get('CLOUDCA_PEM_PATH') and current_app.config.get('CLOUDCA_BUNDLE'):
|
||||
self.session.cert = current_app.config.get('CLOUDCA_PEM_PATH')
|
||||
self.ca_bundle = current_app.config.get('CLOUDCA_BUNDLE')
|
||||
else:
|
||||
current_app.logger.warning("No CLOUDCA credentials found, lemur will be unable to request certificates from CLOUDCA")
|
||||
|
||||
super(CloudCA, self).__init__(*args, **kwargs)
|
||||
|
||||
def create_authority(self, options):
|
||||
"""
|
||||
Creates a new certificate authority
|
||||
|
||||
:param options:
|
||||
:return:
|
||||
"""
|
||||
# this is weird and I don't like it
|
||||
endpoint = '{0}/createCA'.format(API_ENDPOINT)
|
||||
options['caDN']['email'] = options['ownerEmail']
|
||||
|
||||
if options['caType'] == 'subca':
|
||||
options = dict(options.items() + self.auth_data(options['caParent']).items())
|
||||
|
||||
options['validityStart'] = convert_date_to_utc_time(options['validityStart']).isoformat()
|
||||
options['validityEnd'] = convert_date_to_utc_time(options['validityEnd']).isoformat()
|
||||
|
||||
response = self.session.post(self.url + endpoint, data=dumps(remove_none(options)), timeout=10, verify=self.ca_bundle)
|
||||
|
||||
json = process_response(response)
|
||||
roles = []
|
||||
|
||||
for cred in json['data']['authInfo']:
|
||||
role = {
|
||||
'username': cred['credentials']['username'],
|
||||
'password': cred['credentials']['password'],
|
||||
'name': "_".join([options['caName'], cred['credentials']['username']])
|
||||
}
|
||||
roles.append(role)
|
||||
|
||||
if options['caType'] == 'subca':
|
||||
cert = convert_to_pem(json['data']['certificate'])
|
||||
else:
|
||||
cert = convert_to_pem(json['data']['rootCertificate'])
|
||||
|
||||
intermediates = []
|
||||
for i in json['data']['intermediateCertificates']:
|
||||
intermediates.append(convert_to_pem(i))
|
||||
|
||||
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):
|
||||
"""
|
||||
Creates a new certificate from cloudca
|
||||
|
||||
If no start and end date are specified the default issue range
|
||||
will be used.
|
||||
|
||||
:param csr:
|
||||
:param options:
|
||||
"""
|
||||
endpoint = '{0}/enroll'.format(API_ENDPOINT)
|
||||
# lets default to two years if it's not specified
|
||||
# we do some last minute data massaging
|
||||
options = get_default_issuance(options)
|
||||
|
||||
cloudca_options = {
|
||||
'extensions': options['extensions'],
|
||||
'validityStart': convert_date_to_utc_time(options['validityStart']).isoformat(),
|
||||
'validityEnd': convert_date_to_utc_time(options['validityEnd']).isoformat(),
|
||||
'creator': options['creator'],
|
||||
'ownerEmail': options['owner'],
|
||||
'caName': options['authority'].name,
|
||||
'csr': csr,
|
||||
'comment': options['description']
|
||||
}
|
||||
|
||||
response = self.post(endpoint, remove_none(cloudca_options))
|
||||
|
||||
# we return a concatenated list of intermediate because that is what aws
|
||||
# expects
|
||||
cert = convert_to_pem(response['data']['certificate'])
|
||||
|
||||
intermediates = [convert_to_pem(response['data']['rootCertificate'])]
|
||||
for i in response['data']['intermediateCertificates']:
|
||||
intermediates.append(convert_to_pem(i))
|
||||
|
||||
return cert, "".join(intermediates),
|
||||
|
||||
def get_csr_config(self, issuer_options):
|
||||
"""
|
||||
Get a valid CSR for use with CloudCA
|
||||
|
||||
:param issuer_options:
|
||||
:return:
|
||||
"""
|
||||
return cloudca.constants.CSR_CONFIG.format(**issuer_options)
|
||||
|
||||
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_cert(self, ca_name=None, cert_handle=None):
|
||||
"""
|
||||
Returns a given cert from CloudCA.
|
||||
|
||||
:param ca_name:
|
||||
:param cert_handle:
|
||||
:return:
|
||||
"""
|
||||
endpoint = '{0}/getCert'.format(API_ENDPOINT)
|
||||
response = self.session.post(self.url + endpoint, data=dumps({'caName': ca_name}), timeout=10, verify=self.ca_bundle)
|
||||
raw = process_response(response)
|
||||
|
||||
certs = []
|
||||
for c in raw['data']['certList']:
|
||||
cert = convert_to_pem(c['certValue'])
|
||||
|
||||
intermediates = []
|
||||
for i in c['intermediateCertificates']:
|
||||
intermediates.append(convert_to_pem(i))
|
||||
|
||||
certs.append({
|
||||
'public_certificate': cert,
|
||||
'intermediate_cert': "\n".join(intermediates),
|
||||
'owner': c['ownerEmail']
|
||||
})
|
||||
|
||||
return certs
|
||||
|
||||
def post(self, endpoint, data):
|
||||
"""
|
||||
HTTP POST to CloudCA
|
||||
|
||||
:param endpoint:
|
||||
:param data:
|
||||
:return:
|
||||
"""
|
||||
if self.dry_run:
|
||||
endpoint += '?dry_run=1'
|
||||
|
||||
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:
|
||||
"""
|
||||
if self.dry_run:
|
||||
endpoint += '?dry_run=1'
|
||||
|
||||
response = self.session.get(self.url + endpoint, timeout=10, verify=self.ca_bundle)
|
||||
return process_response(response)
|
||||
|
||||
|
||||
def init():
|
||||
return CloudCA()
|
||||
|
27
lemur/common/services/issuers/plugins/cloudca/constants.py
Normal file
27
lemur/common/services/issuers/plugins/cloudca/constants.py
Normal file
@ -0,0 +1,27 @@
|
||||
CSR_CONFIG = """
|
||||
# Configuration for standard CSR generation for Netflix
|
||||
# Used for procuring CloudCA certificates
|
||||
# Author: kglisson
|
||||
# Contact: secops@netflix.com
|
||||
|
||||
[ req ]
|
||||
# Use a 2048 bit private key
|
||||
default_bits = 2048
|
||||
default_keyfile = key.pem
|
||||
prompt = no
|
||||
encrypt_key = no
|
||||
|
||||
# base request
|
||||
distinguished_name = req_distinguished_name
|
||||
|
||||
# distinguished_name
|
||||
[ req_distinguished_name ]
|
||||
countryName = "{country}" # C=
|
||||
stateOrProvinceName = "{state}" # ST=
|
||||
localityName = "{location}" # L=
|
||||
organizationName = "{organization}" # O=
|
||||
organizationalUnitName = "{organizationalUnit}" # OU=
|
||||
# This is the hostname/subject name on the certificate
|
||||
commonName = "{commonName}" # CN=
|
||||
"""
|
||||
|
@ -0,0 +1,5 @@
|
||||
try:
|
||||
VERSION = __import__('pkg_resources') \
|
||||
.get_distribution(__name__).version
|
||||
except Exception, e:
|
||||
VERSION = 'unknown'
|
159
lemur/common/services/issuers/plugins/verisign/constants.py
Normal file
159
lemur/common/services/issuers/plugins/verisign/constants.py
Normal file
@ -0,0 +1,159 @@
|
||||
CSR_CONFIG = """
|
||||
# Configuration for standard CSR generation for Netflix
|
||||
# Used for procuring VeriSign certificates
|
||||
# Author: jachan
|
||||
# Contact: cloudsecurity@netflix.com
|
||||
|
||||
[ req ]
|
||||
# Use a 2048 bit private key
|
||||
default_bits = 2048
|
||||
default_keyfile = key.pem
|
||||
prompt = no
|
||||
encrypt_key = no
|
||||
|
||||
# base request
|
||||
distinguished_name = req_distinguished_name
|
||||
|
||||
# extensions
|
||||
# Uncomment the following line if you are requesting a SAN cert
|
||||
{is_san_comment}req_extensions = req_ext
|
||||
|
||||
# distinguished_name
|
||||
[ req_distinguished_name ]
|
||||
countryName = "US" # C=
|
||||
stateOrProvinceName = "CALIFORNIA" # ST=
|
||||
localityName = "Los Gatos" # L=
|
||||
organizationName = "Netflix, Inc." # O=
|
||||
organizationalUnitName = "{OU}" # OU=
|
||||
# This is the hostname/subject name on the certificate
|
||||
commonName = "{DNS[0]}" # CN=
|
||||
|
||||
[ req_ext ]
|
||||
# Uncomment the following line if you are requesting a SAN cert
|
||||
{is_san_comment}subjectAltName = @alt_names
|
||||
|
||||
[alt_names]
|
||||
# Put your SANs here
|
||||
{DNS_LINES}
|
||||
"""
|
||||
|
||||
VERISIGN_INTERMEDIATE = """
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIFFTCCA/2gAwIBAgIQKC4nkXkzkuQo8iGnTsk3rjANBgkqhkiG9w0BAQsFADCB
|
||||
yjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL
|
||||
ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMTk5OSBWZXJp
|
||||
U2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxW
|
||||
ZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0
|
||||
aG9yaXR5IC0gRzMwHhcNMTMxMDMxMDAwMDAwWhcNMjMxMDMwMjM1OTU5WjB+MQsw
|
||||
CQYDVQQGEwJVUzEdMBsGA1UEChMUU3ltYW50ZWMgQ29ycG9yYXRpb24xHzAdBgNV
|
||||
BAsTFlN5bWFudGVjIFRydXN0IE5ldHdvcmsxLzAtBgNVBAMTJlN5bWFudGVjIENs
|
||||
YXNzIDMgU2VjdXJlIFNlcnZlciBDQSAtIEc0MIIBIjANBgkqhkiG9w0BAQEFAAOC
|
||||
AQ8AMIIBCgKCAQEAstgFyhx0LbUXVjnFSlIJluhL2AzxaJ+aQihiw6UwU35VEYJb
|
||||
A3oNL+F5BMm0lncZgQGUWfm893qZJ4Itt4PdWid/sgN6nFMl6UgfRk/InSn4vnlW
|
||||
9vf92Tpo2otLgjNBEsPIPMzWlnqEIRoiBAMnF4scaGGTDw5RgDMdtLXO637QYqzu
|
||||
s3sBdO9pNevK1T2p7peYyo2qRA4lmUoVlqTObQJUHypqJuIGOmNIrLRM0XWTUP8T
|
||||
L9ba4cYY9Z/JJV3zADreJk20KQnNDz0jbxZKgRb78oMQw7jW2FUyPfG9D72MUpVK
|
||||
Fpd6UiFjdS8W+cRmvvW1Cdj/JwDNRHxvSz+w9wIDAQABo4IBQDCCATwwHQYDVR0O
|
||||
BBYEFF9gz2GQVd+EQxSKYCqy9Xr0QxjvMBIGA1UdEwEB/wQIMAYBAf8CAQAwawYD
|
||||
VR0gBGQwYjBgBgpghkgBhvhFAQc2MFIwJgYIKwYBBQUHAgEWGmh0dHA6Ly93d3cu
|
||||
c3ltYXV0aC5jb20vY3BzMCgGCCsGAQUFBwICMBwaGmh0dHA6Ly93d3cuc3ltYXV0
|
||||
aC5jb20vcnBhMC8GA1UdHwQoMCYwJKAioCCGHmh0dHA6Ly9zLnN5bWNiLmNvbS9w
|
||||
Y2EzLWczLmNybDAOBgNVHQ8BAf8EBAMCAQYwKQYDVR0RBCIwIKQeMBwxGjAYBgNV
|
||||
BAMTEVN5bWFudGVjUEtJLTEtNTM0MC4GCCsGAQUFBwEBBCIwIDAeBggrBgEFBQcw
|
||||
AYYSaHR0cDovL3Muc3ltY2QuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQBbF1K+1lZ7
|
||||
9Pc0CUuWysf2IdBpgO/nmhnoJOJ/2S9h3RPrWmXk4WqQy04q6YoW51KN9kMbRwUN
|
||||
gKOomv4p07wdKNWlStRxPA91xQtzPwBIZXkNq2oeJQzAAt5mrL1LBmuaV4oqgX5n
|
||||
m7pSYHPEFfe7wVDJCKW6V0o6GxBzHOF7tpQDS65RsIJAOloknO4NWF2uuil6yjOe
|
||||
soHCL47BJ89A8AShP/U3wsr8rFNtqVNpT+F2ZAwlgak3A/I5czTSwXx4GByoaxbn
|
||||
5+CdKa/Y5Gk5eZVpuXtcXQGc1PfzSEUTZJXXCm5y2kMiJG8+WnDcwJLgLeVX+OQr
|
||||
J+71/xuzAYN6
|
||||
-----END CERTIFICATE-----
|
||||
"""
|
||||
|
||||
|
||||
VERISIGN_ROOT = """
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEGjCCAwICEQCbfgZJoz5iudXukEhxKe9XMA0GCSqGSIb3DQEBBQUAMIHKMQsw
|
||||
CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl
|
||||
cmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu
|
||||
LCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlT
|
||||
aWduIENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3Jp
|
||||
dHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQswCQYD
|
||||
VQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlT
|
||||
aWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJ
|
||||
bmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWdu
|
||||
IENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg
|
||||
LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMu6nFL8eB8aHm8b
|
||||
N3O9+MlrlBIwT/A2R/XQkQr1F8ilYcEWQE37imGQ5XYgwREGfassbqb1EUGO+i2t
|
||||
KmFZpGcmTNDovFJbcCAEWNF6yaRpvIMXZK0Fi7zQWM6NjPXr8EJJC52XJ2cybuGu
|
||||
kxUccLwgTS8Y3pKI6GyFVxEa6X7jJhFUokWWVYPKMIno3Nij7SqAP395ZVc+FSBm
|
||||
CC+Vk7+qRy+oRpfwEuL+wgorUeZ25rdGt+INpsyow0xZVYnm6FNcHOqd8GIWC6fJ
|
||||
Xwzw3sJ2zq/3avL6QaaiMxTJ5Xpj055iN9WFZZ4O5lMkdBteHRJTW8cs54NJOxWu
|
||||
imi5V5cCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAERSWwauSCPc/L8my/uRan2Te
|
||||
2yFPhpk0djZX3dAVL8WtfxUfN2JzPtTnX84XA9s1+ivbrmAJXx5fj267Cz3qWhMe
|
||||
DGBvtcC1IyIuBwvLqXTLR7sdwdela8wv0kL9Sd2nic9TutoAWii/gt/4uhMdUIaC
|
||||
/Y4wjylGsB49Ndo4YhYYSq3mtlFs3q9i6wHQHiT+eo8SGhJouPtmmRQURVyu565p
|
||||
F4ErWjfJXir0xuKhXFSbplQAz/DxwceYMBo7Nhbbo27q/a2ywtrvAkcTisDxszGt
|
||||
TxzhT5yvDwyd93gN2PQ1VoDat20Xj50egWTh/sVFuq1ruQp6Tk9LhO5L8X3dEQ==
|
||||
-----END CERTIFICATE-----
|
||||
"""
|
||||
|
||||
OLD_VERISIGN_INTERMEDIATE = """
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIFlTCCBH2gAwIBAgIQLP62CQ7ireLp/CI3JPG2vzANBgkqhkiG9w0BAQUFADCB
|
||||
yjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL
|
||||
ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMTk5OSBWZXJp
|
||||
U2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxW
|
||||
ZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0
|
||||
aG9yaXR5IC0gRzMwHhcNMTAwMjA4MDAwMDAwWhcNMjAwMjA3MjM1OTU5WjCBtTEL
|
||||
MAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZW
|
||||
ZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTswOQYDVQQLEzJUZXJtcyBvZiB1c2UgYXQg
|
||||
aHR0cHM6Ly93d3cudmVyaXNpZ24uY29tL3JwYSAoYykxMDEvMC0GA1UEAxMmVmVy
|
||||
aVNpZ24gQ2xhc3MgMyBTZWN1cmUgU2VydmVyIENBIC0gRzMwggEiMA0GCSqGSIb3
|
||||
DQEBAQUAA4IBDwAwggEKAoIBAQCxh4QfwgxF9byrJZenraI+nLr2wTm4i8rCrFbG
|
||||
5btljkRPTc5v7QlK1K9OEJxoiy6Ve4mbE8riNDTB81vzSXtig0iBdNGIeGwCU/m8
|
||||
f0MmV1gzgzszChew0E6RJK2GfWQS3HRKNKEdCuqWHQsV/KNLO85jiND4LQyUhhDK
|
||||
tpo9yus3nABINYYpUHjoRWPNGUFP9ZXse5jUxHGzUL4os4+guVOc9cosI6n9FAbo
|
||||
GLSa6Dxugf3kzTU2s1HTaewSulZub5tXxYsU5w7HnO1KVGrJTcW/EbGuHGeBy0RV
|
||||
M5l/JJs/U0V/hhrzPPptf4H1uErT9YU3HLWm0AnkGHs4TvoPAgMBAAGjggGIMIIB
|
||||
hDASBgNVHRMBAf8ECDAGAQH/AgEAMHAGA1UdIARpMGcwZQYLYIZIAYb4RQEHFwMw
|
||||
VjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cudmVyaXNpZ24uY29tL2NwczAqBggr
|
||||
BgEFBQcCAjAeGhxodHRwczovL3d3dy52ZXJpc2lnbi5jb20vcnBhMA4GA1UdDwEB
|
||||
/wQEAwIBBjBtBggrBgEFBQcBDARhMF+hXaBbMFkwVzBVFglpbWFnZS9naWYwITAf
|
||||
MAcGBSsOAwIaBBSP5dMahqyNjmvDz4Bq1EgYLHsZLjAlFiNodHRwOi8vbG9nby52
|
||||
ZXJpc2lnbi5jb20vdnNsb2dvLmdpZjAoBgNVHREEITAfpB0wGzEZMBcGA1UEAxMQ
|
||||
VmVyaVNpZ25NUEtJLTItNjAdBgNVHQ4EFgQUDURcFlNEwYJ+HSCrJfQBY9i+eaUw
|
||||
NAYDVR0fBC0wKzApoCegJYYjaHR0cDovL2NybC52ZXJpc2lnbi5jb20vcGNhMy1n
|
||||
My5jcmwwDQYJKoZIhvcNAQEFBQADggEBAHREFQzFWA4YY+3z8CjDeuuSSG/ghSBJ
|
||||
olwwlpIX4IjoeYuzT864Hzk2tTeEeODf4YFIVsSxah8nUsGdpgVTUGPPoUJOMXvn
|
||||
8wJeBSlUDXBwv3td5XbPIPXHy6vmIS6phYRetZUgq1CDTI/pvtWZKXTGM/eYXlLF
|
||||
6QDvXevUHQjfb3cqQvfLljws85xLxbNFmz7cy9YmiLOd5n+gFC6X5hzSDO7+DDMi
|
||||
o//+4Q/nk/UId1UCsobqYWVmqs017AmyiAPO/v3sGncYYQY2BMYgla74dZfeDNu4
|
||||
MXA68Mb6ZdlkhGEmZYVBcOmkaKs+P+SggTofsK27BlpugAtNWjEy5JY=
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEOzCCA6SgAwIBAgIQSsnqCI7m94zHpfn6OaSTljANBgkqhkiG9w0BAQUFADBf
|
||||
MQswCQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xNzA1BgNVBAsT
|
||||
LkNsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkw
|
||||
HhcNMTEwNjA5MDAwMDAwWhcNMjExMTA3MjM1OTU5WjCByjELMAkGA1UEBhMCVVMx
|
||||
FzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJpU2lnbiBUcnVz
|
||||
dCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMTk5OSBWZXJpU2lnbiwgSW5jLiAtIEZv
|
||||
ciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJpU2lnbiBDbGFzcyAz
|
||||
IFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzMwggEi
|
||||
MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDLupxS/HgfGh5vGzdzvfjJa5QS
|
||||
ME/wNkf10JEK9RfIpWHBFkBN+4phkOV2IMERBn2rLG6m9RFBjvotrSphWaRnJkzQ
|
||||
6LxSW3AgBFjResmkabyDF2StBYu80FjOjYz16/BCSQudlydnMm7hrpMVHHC8IE0v
|
||||
GN6SiOhshVcRGul+4yYRVKJFllWDyjCJ6NzYo+0qgD9/eWVXPhUgZggvlZO/qkcv
|
||||
qEaX8BLi/sIKK1Hmdua3RrfiDabMqMNMWVWJ5uhTXBzqnfBiFgunyV8M8N7Cds6v
|
||||
92ry+kGmojMUyeV6Y9OeYjfVhWWeDuZTJHQbXh0SU1vHLOeDSTsVropouVeXAgMB
|
||||
AAGjggEGMIIBAjAPBgNVHRMBAf8EBTADAQH/MD0GA1UdIAQ2MDQwMgYEVR0gADAq
|
||||
MCgGCCsGAQUFBwIBFhxodHRwczovL3d3dy52ZXJpc2lnbi5jb20vY3BzMDEGA1Ud
|
||||
HwQqMCgwJqAkoCKGIGh0dHA6Ly9jcmwudmVyaXNpZ24uY29tL3BjYTMuY3JsMA4G
|
||||
A1UdDwEB/wQEAwIBBjBtBggrBgEFBQcBDARhMF+hXaBbMFkwVzBVFglpbWFnZS9n
|
||||
aWYwITAfMAcGBSsOAwIaBBSP5dMahqyNjmvDz4Bq1EgYLHsZLjAlFiNodHRwOi8v
|
||||
bG9nby52ZXJpc2lnbi5jb20vdnNsb2dvLmdpZjANBgkqhkiG9w0BAQUFAAOBgQBl
|
||||
2Sr58sJgybnqQQfKNrcYL2iu/gMk5mdU7nTDLNn1M8Fetw6Tz3iejrImFBFT0cjC
|
||||
EiG0PXsq2BzUS2TsiU+/lYeH3pVk9HPGF9+9GZCX6GmBEmlmStMkQA5ZdRWwRHQX
|
||||
op4GYNOwg7jdL+afe2dcFqFH284ueQXZ8fT4PuJKoQ==
|
||||
-----END CERTIFICATE-----
|
||||
"""
|
194
lemur/common/services/issuers/plugins/verisign/verisign.py
Normal file
194
lemur/common/services/issuers/plugins/verisign/verisign.py
Normal file
@ -0,0 +1,194 @@
|
||||
"""
|
||||
.. module: lemur.common.services.issuers.plugins.verisign.verisign
|
||||
:platform: Unix
|
||||
:synopsis: This module is responsible for communicating with the VeriSign VICE 2.0 API.
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
import arrow
|
||||
import requests
|
||||
import xmltodict
|
||||
|
||||
from flask import current_app
|
||||
|
||||
from lemur.common.services.issuers.issuer import Issuer
|
||||
from lemur.common.services.issuers.plugins import verisign
|
||||
|
||||
from lemur.certificates.exceptions import InsufficientDomains
|
||||
|
||||
|
||||
# https://support.venafi.com/entries/66445046-Info-VeriSign-Error-Codes
|
||||
VERISIGN_ERRORS = {
|
||||
"0x30c5": "Domain Mismatch when enrolling for an SSL certificate, a domain in your request has not been added to verisign",
|
||||
"0x482d": "Cannot issue SHA1 certificates expiring after 31/12/2016",
|
||||
"0x3a10": "Invalid X509 certificate format.: an unsupported certificate format was submitted",
|
||||
"0x4002": "Internal QM Error. : Internal Database connection error.",
|
||||
"0x3301": "Bad transaction id or parent cert not renewable.: User try to renew a certificate that is not yet ready for renew or the transaction id is wrong",
|
||||
"0x3069": "Challenge phrase mismatch: The challenge phrase submitted does not match the original one",
|
||||
"0x3111": "Unsupported Product: User submitted a wrong product or requested cipher is not supported",
|
||||
"0x30e8": "CN or org does not match the original one.: the submitted CSR contains a common name or org that does not match the original one",
|
||||
"0x1005": "Duplicate certificate: a certificate with the same common name exists already",
|
||||
"0x0194": "Incorrect Signature Algorithm: The requested signature algorithm is not supported for the key type. i.e. an ECDSA is submitted for an RSA key",
|
||||
"0x6000": "parameter missing or incorrect: This is a general error code for missing or incorrect parameters. The reason will be in the response message. i.e. 'CSR is missing, 'Unsupported serverType' when no supported serverType could be found., 'invalid transaction id'",
|
||||
"0x3063": "Certificate not allowed: trying to issue a certificate that is not configured for the account",
|
||||
"0x23df": "No MDS Data Returned: internal connection lost or server not responding. this should be rare",
|
||||
"0x3004": "Invalid Account: The users mpki account associated with the certificate is not valid or not yet active",
|
||||
"0x4101": "Internal Error: internal server error, user should try again later. (Also check that State is spelled out",
|
||||
"0x3101": "Missing admin role: Your account does not have the admin role required to access the webservice API",
|
||||
"0x3085": "Account does not have webservice feature.: Your account does not the the webservice role required to access the webservice API",
|
||||
"0x9511": "Corrupted CSR : the submitted CSR was mal-formed",
|
||||
"0xa001": "Public key format does not match.: The public key format does not match the original cert at certificate renewal or replacement. E.g. if you try to renew or replace an RSA cert with a DSA or ECC key based CSR",
|
||||
"0x0143": "Certificate End Date Error: You are trying to replace a certificate with validity end date exceeding the original cert. or the certificate end date is not valid",
|
||||
"0x482d": "SHA1 validity check error: What error code do we get when we submit the SHA1 SSL requests with the validity more than 12/31/2016?",
|
||||
"0x482e": "What error code do we get when we cannot complete the re-authentication for domains with a newly-approved gTLD 30 days after the gTLD approval",
|
||||
"0x4824": "Per CA/B Forum baseline requirements, non-FQDN certs cannot exceed 11/1/2015. Examples: hostname, foo.cba (.cba is a pending gTLD)",
|
||||
"eE0x48": "Currently the maximum cert validity is 4-years",
|
||||
"0x4826": "OU misleading. See comments",
|
||||
"0x4827": "Org re-auth past due. EV org has to go through re-authentication every 13 months; OV org has to go through re-authentication every 39 months",
|
||||
"0x482a": "Domain re-auth past due. EV domain has to go through re-authentication every 13 months; OV domain has to go through re-authentication every 39 months.",
|
||||
"0x482b": "No org address was set to default, should not happen",
|
||||
"0x482c": "signature algorithm does not match intended key type in the CSR (e.g. CSR has an ECC key, but the signature algorithm is sha1WithRSAEncryption)",
|
||||
"0x600E": "only supports ECC keys with the named curve NIST P-256, aka secp256r1 or prime256v1, other ECC key sizes will get this error ",
|
||||
"0x6013": "only supports DSA keys with (2048, 256) as the bit lengths of the prime parameter pair (p, q), other DSA key sizes will get this error",
|
||||
"0x600d": "RSA key size < 2A048",
|
||||
"0x4828": "Verisign certificates can be at most two years in length",
|
||||
"0x3043": "Certificates must have a validity of at least 1 day"
|
||||
}
|
||||
|
||||
|
||||
class Verisign(Issuer):
|
||||
title = 'VeriSign'
|
||||
slug = 'verisign'
|
||||
description = 'Enables the creation of certificates by the VICE2.0 verisign API.'
|
||||
version = verisign.VERSION
|
||||
|
||||
author = 'Kevin Glisson'
|
||||
author_url = 'https://github.com/netflix/lemur'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.session = requests.Session()
|
||||
self.session.cert = current_app.config.get('VERISIGN_PEM_PATH')
|
||||
super(Verisign, self).__init__(*args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def handle_response(content):
|
||||
"""
|
||||
Helper function that helps with parsing responses from the Verisign API.
|
||||
:param content:
|
||||
:return: :raise Exception:
|
||||
"""
|
||||
d = xmltodict.parse(content)
|
||||
global VERISIGN_ERRORS
|
||||
if d.get('Error'):
|
||||
status_code = d['Error']['StatusCode']
|
||||
elif d.get('Response'):
|
||||
status_code = d['Response']['StatusCode']
|
||||
if status_code in VERISIGN_ERRORS.keys():
|
||||
raise Exception(VERISIGN_ERRORS[status_code])
|
||||
return d
|
||||
|
||||
def create_certificate(self, csr, issuer_options):
|
||||
"""
|
||||
Creates a Verisign certificate.
|
||||
|
||||
:param csr:
|
||||
:param issuer_options:
|
||||
:return: :raise Exception:
|
||||
"""
|
||||
url = current_app.config.get("VERISIGN_URL") + '/enroll'
|
||||
|
||||
data = {
|
||||
'csr': csr,
|
||||
'challenge': issuer_options['challenge'],
|
||||
'serverType': 'Apache',
|
||||
'certProductType': 'Server',
|
||||
'firstName': current_app.config.get("VERISIGN_FIRST_NAME"),
|
||||
'lastName': current_app.config.get("VERISIGN_LAST_NAME"),
|
||||
'signatureAlgorithm': 'sha256WithRSAEncryption',
|
||||
'email': current_app.config.get("VERISIGN_EMAIL")
|
||||
}
|
||||
|
||||
if issuer_options.get('validityEnd'):
|
||||
data['specificEndDate'] = arrow.get(issuer_options['validityEnd']).replace(days=-1).format("MM/DD/YYYY")
|
||||
|
||||
now = arrow.utcnow()
|
||||
then = arrow.get(issuer_options['validityEnd'])
|
||||
|
||||
if then < now.replace(years=+1):
|
||||
data['validityPeriod'] = '1Y'
|
||||
elif then < now.replace(years=+2):
|
||||
data['validityPeriod'] = '2Y'
|
||||
else:
|
||||
raise Exception("Verisign issued certificates cannot exceed two years in validity")
|
||||
|
||||
current_app.logger.info("Requesting a new verisign certificate: {0}".format(data))
|
||||
|
||||
response = self.session.post(url, data=data)
|
||||
cert = self.handle_response(response.content)['Response']['Certificate']
|
||||
return cert, verisign.constants.VERISIGN_INTERMEDIATE,
|
||||
|
||||
def get_csr_config(self, issuer_options):
|
||||
"""
|
||||
Used to generate a valid CSR for the given Certificate Authority.
|
||||
|
||||
:param issuer_options:
|
||||
:return: :raise InsufficientDomains:
|
||||
"""
|
||||
domains = []
|
||||
|
||||
if issuer_options.get('commonName'):
|
||||
domains.append(issuer_options.get('commonName'))
|
||||
|
||||
if issuer_options.get('extensions'):
|
||||
for n in issuer_options['extensions']['subAltNames']['names']:
|
||||
if n['value']:
|
||||
domains.append(n['value'])
|
||||
|
||||
is_san_comment = "#"
|
||||
|
||||
dns_lines = []
|
||||
if len(domains) < 1:
|
||||
raise InsufficientDomains
|
||||
|
||||
elif len(domains) > 1:
|
||||
is_san_comment = ""
|
||||
for domain_line in list(set(domains)):
|
||||
dns_lines.append("DNS.{} = {}".format(len(dns_lines) + 1, domain_line))
|
||||
|
||||
return verisign.constants.CSR_CONFIG.format(
|
||||
is_san_comment=is_san_comment,
|
||||
OU=issuer_options.get('organizationalUnit', 'Operations'),
|
||||
DNS=domains,
|
||||
DNS_LINES="\n".join(dns_lines))
|
||||
|
||||
@staticmethod
|
||||
def create_authority(options):
|
||||
"""
|
||||
Creates an authority, this authority is then used by Lemur to allow a user
|
||||
to specify which Certificate Authority they want to sign their certificate.
|
||||
|
||||
:param options:
|
||||
:return:
|
||||
"""
|
||||
role = {'username': '', 'password': '', 'name': 'verisign'}
|
||||
return verisign.constants.VERISIGN_ROOT, "", [role]
|
||||
|
||||
def get_available_units(self):
|
||||
"""
|
||||
Uses the Verisign to fetch the number of available unit's left. This can be used to get tabs
|
||||
on the number of certificates that can be issued.
|
||||
|
||||
:return:
|
||||
"""
|
||||
url = current_app.config.get("VERISIGN_URL") + '/getTokens'
|
||||
response = self.session.post(url, headers={'content-type': 'application/x-www-form-urlencoded'})
|
||||
return self.handle_response(response.content)['Response']['Order']
|
||||
|
||||
def get_authorities(self):
|
||||
pass
|
||||
|
||||
|
||||
def init():
|
||||
return Verisign()
|
63
lemur/common/utils.py
Normal file
63
lemur/common/utils.py
Normal file
@ -0,0 +1,63 @@
|
||||
"""
|
||||
.. module: lemur.common.utils
|
||||
: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 functools import wraps
|
||||
|
||||
from flask import current_app
|
||||
|
||||
from flask.ext.restful import marshal
|
||||
from flask.ext.restful.reqparse import RequestParser
|
||||
|
||||
from flask.ext.sqlalchemy import Pagination
|
||||
|
||||
|
||||
class marshal_items(object):
|
||||
def __init__(self, fields, envelope=None):
|
||||
self.fields = fields
|
||||
self.envelop = envelope
|
||||
|
||||
def __call__(self, f):
|
||||
def _filter_items(items):
|
||||
filtered_items = []
|
||||
for item in items:
|
||||
filtered_items.append(marshal(item, self.fields))
|
||||
return filtered_items
|
||||
|
||||
@wraps(f)
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
resp = f(*args, **kwargs)
|
||||
|
||||
# this is a bit weird way to handle non standard error codes returned from the marshaled function
|
||||
if isinstance(resp, tuple):
|
||||
return resp[0], resp[1]
|
||||
|
||||
if isinstance(resp, Pagination):
|
||||
return {'items': _filter_items(resp.items), 'total': resp.total}
|
||||
|
||||
if isinstance(resp, list):
|
||||
return _filter_items(resp)
|
||||
|
||||
return marshal(resp, self.fields)
|
||||
except Exception as e:
|
||||
# this is a little weird hack to respect flask restful parsing errors on marshaled functions
|
||||
if hasattr(e, 'code'):
|
||||
return {'message': e.data['message']}, 400
|
||||
else:
|
||||
current_app.logger.exception(e)
|
||||
return {'message': e.message}, 400
|
||||
return wrapper
|
||||
|
||||
|
||||
paginated_parser = RequestParser()
|
||||
|
||||
paginated_parser.add_argument('count', type=int, default=10, location='args')
|
||||
paginated_parser.add_argument('page', type=int, default=1, location='args')
|
||||
paginated_parser.add_argument('sortDir', type=str, dest='sort_dir', location='args')
|
||||
paginated_parser.add_argument('sortBy', type=str, dest='sort_by', location='args')
|
||||
paginated_parser.add_argument('filter', type=str, location='args')
|
10
lemur/constants.py
Normal file
10
lemur/constants.py
Normal file
@ -0,0 +1,10 @@
|
||||
"""
|
||||
.. module: lemur.constants
|
||||
:copyright: (c) 2015 by Netflix Inc.
|
||||
:license: Apache, see LICENSE for more details.
|
||||
"""
|
||||
SAN_NAMING_TEMPLATE = "SAN-{subject}-{issuer}-{not_before}-{not_after}"
|
||||
DEFAULT_NAMING_TEMPLATE = "{subject}-{issuer}-{not_before}-{not_after}"
|
||||
NONSTANDARD_NAMING_TEMPLATE = "{issuer}-{not_before}-{not_after}"
|
||||
|
||||
|
278
lemur/database.py
Normal file
278
lemur/database.py
Normal file
@ -0,0 +1,278 @@
|
||||
"""
|
||||
.. module: lemur.database
|
||||
:platform: Unix
|
||||
:synopsis: This module contains all of the database related methods
|
||||
needed for lemur to interact with a datastore
|
||||
|
||||
: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 current_app
|
||||
|
||||
from sqlalchemy import exc
|
||||
from sqlalchemy.sql import and_, or_
|
||||
|
||||
from lemur.extensions import db
|
||||
from lemur.exceptions import AttrNotFound, IntegrityError
|
||||
|
||||
|
||||
def filter_none(kwargs):
|
||||
"""
|
||||
Remove all `None` values froma given dict. SQLAlchemy does not
|
||||
like to have values that are None passed to it.
|
||||
|
||||
:param kwargs: Dict to filter
|
||||
:return: Dict without any 'None' values
|
||||
"""
|
||||
n_kwargs = {}
|
||||
for k, v in kwargs.items():
|
||||
if v:
|
||||
n_kwargs[k] = v
|
||||
return n_kwargs
|
||||
|
||||
|
||||
def session_query(model):
|
||||
"""
|
||||
Returns a SQLAlchemy query object for the specified `model`.
|
||||
|
||||
If `model` has a ``query`` attribute already, that object will be returned.
|
||||
Otherwise a query will be created and returned based on `session`.
|
||||
|
||||
:param model: sqlalchemy model
|
||||
:return: query object for model
|
||||
"""
|
||||
return model.query if hasattr(model, 'query') else db.session.query(model)
|
||||
|
||||
|
||||
def create_query(model, kwargs):
|
||||
"""
|
||||
Returns a SQLAlchemy query object for specified `model`. Model
|
||||
filtered by the kwargs passed.
|
||||
|
||||
:param model:
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
s = session_query(model)
|
||||
return s.filter_by(**kwargs)
|
||||
|
||||
|
||||
def commit():
|
||||
"""
|
||||
Helper to commit the current session.
|
||||
"""
|
||||
db.session.commit()
|
||||
|
||||
|
||||
def add(model):
|
||||
"""
|
||||
Helper to add a `model` to the current session.
|
||||
|
||||
:param model:
|
||||
:return:
|
||||
"""
|
||||
db.session.add(model)
|
||||
|
||||
|
||||
def find_all(query, model, kwargs):
|
||||
"""
|
||||
Returns a query object that ensures that all kwargs
|
||||
are present.
|
||||
|
||||
:param query:
|
||||
:param model:
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
conditions = []
|
||||
kwargs = filter_none(kwargs)
|
||||
for attr, value in kwargs.items():
|
||||
if not isinstance(value, list):
|
||||
value = value.split(',')
|
||||
|
||||
conditions.append(getattr(model, attr).in_(value))
|
||||
|
||||
return query.filter(and_(*conditions))
|
||||
|
||||
|
||||
def find_any(query, model, kwargs):
|
||||
"""
|
||||
Returns a query object that allows any kwarg
|
||||
to be present.
|
||||
|
||||
:param query:
|
||||
:param model:
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
or_args = []
|
||||
for attr, value in kwargs.items():
|
||||
or_args.append(or_(getattr(model, attr) == value))
|
||||
exprs = or_(*or_args)
|
||||
return query.filter(exprs)
|
||||
|
||||
|
||||
def get(model, value, field="id"):
|
||||
"""
|
||||
Returns one object filtered by the field and value.
|
||||
|
||||
:param model:
|
||||
:param value:
|
||||
:param field:
|
||||
:return:
|
||||
"""
|
||||
query = session_query(model)
|
||||
try:
|
||||
return query.filter(getattr(model, field) == value).one()
|
||||
except:
|
||||
return
|
||||
|
||||
|
||||
def get_all(model, value, field="id"):
|
||||
"""
|
||||
Returns query object with the fields and value filtered.
|
||||
|
||||
:param model:
|
||||
:param value:
|
||||
:param field:
|
||||
:return:
|
||||
"""
|
||||
query = session_query(model)
|
||||
return query.filter(getattr(model, field) == value)
|
||||
|
||||
|
||||
def create(model):
|
||||
"""
|
||||
Helper that attempts to create a new instance of an object.
|
||||
|
||||
:param model:
|
||||
:return: :raise IntegrityError:
|
||||
"""
|
||||
try:
|
||||
db.session.add(model)
|
||||
commit()
|
||||
db.session.refresh(model)
|
||||
except exc.IntegrityError as e:
|
||||
raise IntegrityError(e.orig.diag.message_detail)
|
||||
return model
|
||||
|
||||
|
||||
def update(model):
|
||||
"""
|
||||
Helper that attempts to update a model.
|
||||
|
||||
:param model:
|
||||
:return:
|
||||
"""
|
||||
commit()
|
||||
db.session.refresh(model)
|
||||
return model
|
||||
|
||||
|
||||
def delete(model):
|
||||
"""
|
||||
Helper that attempts to delete a model.
|
||||
|
||||
:param model:
|
||||
"""
|
||||
db.session.delete(model)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
def filter(query, model, terms):
|
||||
"""
|
||||
Helper that searched for 'like' strings in column values.
|
||||
|
||||
:param query:
|
||||
:param model:
|
||||
:param terms:
|
||||
:return:
|
||||
"""
|
||||
return query.filter(getattr(model, terms[0]).ilike('%{}%'.format(terms[1])))
|
||||
|
||||
|
||||
def sort(query, model, field, direction):
|
||||
"""
|
||||
Returns objects of the specified `model` in the field and direction
|
||||
given
|
||||
|
||||
:param query:
|
||||
:param model:
|
||||
:param field:
|
||||
:param direction:
|
||||
"""
|
||||
try:
|
||||
field = getattr(model, field)
|
||||
direction = getattr(field, direction)
|
||||
query = query.order_by(direction())
|
||||
return query
|
||||
except AttributeError as e:
|
||||
raise AttrNotFound(field)
|
||||
|
||||
|
||||
def paginate(query, page, count):
|
||||
"""
|
||||
Returns the items given the count and page specified
|
||||
|
||||
:param query:
|
||||
:param page:
|
||||
:param count:
|
||||
"""
|
||||
return query.paginate(page, count)
|
||||
|
||||
|
||||
def update_list(model, model_attr, item_model, items):
|
||||
"""
|
||||
Helper that correctly updates a models items
|
||||
depending on what has changed
|
||||
|
||||
:param model_attr:
|
||||
:param item_model:
|
||||
:param items:
|
||||
:param model:
|
||||
:return:
|
||||
"""
|
||||
ids = []
|
||||
|
||||
for i in items:
|
||||
ids.append(i['id'])
|
||||
|
||||
for i in getattr(model, model_attr):
|
||||
if i.id not in ids:
|
||||
getattr(model, model_attr).remove(i)
|
||||
|
||||
for i in items:
|
||||
for item in getattr(model, model_attr):
|
||||
if item.id == i['id']:
|
||||
break
|
||||
else:
|
||||
getattr(model, model_attr).append(get(item_model, i['id']))
|
||||
|
||||
return model
|
||||
|
||||
|
||||
def sort_and_page(query, model, args):
|
||||
"""
|
||||
Helper that allows us to combine sorting and paging
|
||||
|
||||
:param query:
|
||||
:param model:
|
||||
:param args:
|
||||
:return:
|
||||
"""
|
||||
sort_by = args.pop('sort_by')
|
||||
sort_dir = args.pop('sort_dir')
|
||||
page = args.pop('page')
|
||||
count = args.pop('count')
|
||||
|
||||
query = find_all(query, model, args)
|
||||
|
||||
if sort_by and sort_dir:
|
||||
query = sort(query, model, sort_by, sort_dir)
|
||||
|
||||
return paginate(query, page, count)
|
||||
|
||||
|
||||
|
55
lemur/decorators.py
Normal file
55
lemur/decorators.py
Normal file
@ -0,0 +1,55 @@
|
||||
"""
|
||||
.. module: lemur.decorators
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
"""
|
||||
from datetime import timedelta
|
||||
from flask import make_response, request, current_app
|
||||
|
||||
from functools import update_wrapper
|
||||
|
||||
|
||||
def crossdomain(origin=None, methods=None, headers=None,
|
||||
max_age=21600, attach_to_all=True,
|
||||
automatic_options=True):
|
||||
if methods is not None:
|
||||
methods = ', '.join(sorted(x.upper() for x in methods))
|
||||
|
||||
if headers is not None and not isinstance(headers, basestring):
|
||||
headers = ', '.join(x.upper() for x in headers)
|
||||
|
||||
if not isinstance(origin, basestring):
|
||||
origin = ', '.join(origin)
|
||||
|
||||
if isinstance(max_age, timedelta):
|
||||
max_age = max_age.total_seconds()
|
||||
|
||||
def get_methods():
|
||||
if methods is not None:
|
||||
return methods
|
||||
|
||||
options_resp = current_app.make_default_options_response()
|
||||
return options_resp.headers['allow']
|
||||
|
||||
def decorator(f):
|
||||
def wrapped_function(*args, **kwargs):
|
||||
if automatic_options and request.method == 'OPTIONS':
|
||||
resp = current_app.make_default_options_response()
|
||||
else:
|
||||
resp = make_response(f(*args, **kwargs))
|
||||
if not attach_to_all and request.method != 'OPTIONS':
|
||||
return resp
|
||||
|
||||
h = resp.headers
|
||||
h['Access-Control-Allow-Origin'] = origin
|
||||
h['Access-Control-Allow-Methods'] = get_methods()
|
||||
h['Access-Control-Max-Age'] = str(max_age)
|
||||
#if headers is not None:
|
||||
h['Access-Control-Allow-Headers'] = "Origin, X-Requested-With, Content-Type, Accept, Authorization " # headers
|
||||
h['Access-Control-Allow-Credentials'] = 'true'
|
||||
return resp
|
||||
|
||||
f.provide_automatic_options = False
|
||||
return update_wrapper(wrapped_function, f)
|
||||
return decorator
|
||||
|
0
lemur/domains/__init__.py
Normal file
0
lemur/domains/__init__.py
Normal file
27
lemur/domains/models.py
Normal file
27
lemur/domains/models.py
Normal file
@ -0,0 +1,27 @@
|
||||
"""
|
||||
.. module: lemur.domains.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
|
||||
|
||||
from lemur.database import db
|
||||
|
||||
|
||||
class Domain(db.Model):
|
||||
__tablename__ = 'domains'
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String(256))
|
||||
|
||||
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['certificates'] = [x.id for x in self.certificate]
|
||||
return blob
|
||||
|
64
lemur/domains/service.py
Normal file
64
lemur/domains/service.py
Normal file
@ -0,0 +1,64 @@
|
||||
"""
|
||||
.. module: lemur.domains.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.domains.models import Domain
|
||||
from lemur.certificates.models import Certificate
|
||||
|
||||
from lemur import database
|
||||
|
||||
|
||||
def get(domain_id):
|
||||
"""
|
||||
Fetches one domain
|
||||
|
||||
:param domain_id:
|
||||
:return:
|
||||
"""
|
||||
return database.get(Domain, domain_id)
|
||||
|
||||
|
||||
def get_all():
|
||||
"""
|
||||
Fetches all domains
|
||||
|
||||
:return:
|
||||
"""
|
||||
query = database.session_query(Domain)
|
||||
return database.find_all(query, Domain, {}).all()
|
||||
|
||||
|
||||
def render(args):
|
||||
"""
|
||||
Helper to parse REST Api requests
|
||||
|
||||
:param args:
|
||||
:return:
|
||||
"""
|
||||
query = database.session_query(Domain).join(Certificate, Domain.certificate)
|
||||
|
||||
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 filt:
|
||||
terms = filt.split(';')
|
||||
query = database.filter(query, Domain, terms)
|
||||
|
||||
if certificate_id:
|
||||
query = query.filter(Certificate.id == certificate_id)
|
||||
|
||||
query = database.find_all(query, Domain, args)
|
||||
|
||||
if sort_by and sort_dir:
|
||||
query = database.sort(query, Domain, sort_by, sort_dir)
|
||||
|
||||
return database.paginate(query, page, count)
|
||||
|
182
lemur/domains/views.py
Normal file
182
lemur/domains/views.py
Normal file
@ -0,0 +1,182 @@
|
||||
"""
|
||||
.. module: lemur.domains.views
|
||||
:platform: Unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
|
||||
"""
|
||||
from flask import Blueprint
|
||||
from flask.ext.restful import reqparse, Api, fields
|
||||
|
||||
from lemur.domains import service
|
||||
from lemur.auth.service import AuthenticatedResource
|
||||
|
||||
from lemur.common.utils import paginated_parser, marshal_items
|
||||
|
||||
FIELDS = {
|
||||
'id': fields.Integer,
|
||||
'name': fields.String
|
||||
}
|
||||
|
||||
mod = Blueprint('domains', __name__)
|
||||
api = Api(mod)
|
||||
|
||||
|
||||
class DomainsList(AuthenticatedResource):
|
||||
""" Defines the 'domains' endpoint """
|
||||
def __init__(self):
|
||||
super(DomainsList, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
def get(self):
|
||||
"""
|
||||
.. http:get:: /domains
|
||||
|
||||
The current domain list
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /domains 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": 1,
|
||||
"name": "www.example.com",
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "www.example2.com",
|
||||
}
|
||||
]
|
||||
"total": 2
|
||||
}
|
||||
|
||||
:query sortBy: field to sort on
|
||||
:query sortDir: acs or desc
|
||||
:query page: int. default is 1
|
||||
:query filter: key value pair. format is k=v;
|
||||
:query limit: limit number. default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
"""
|
||||
parser = paginated_parser.copy()
|
||||
args = parser.parse_args()
|
||||
return service.render(args)
|
||||
|
||||
|
||||
class Domains(AuthenticatedResource):
|
||||
def __init__(self):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(Domains, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
def get(self, domain_id):
|
||||
"""
|
||||
.. http:get:: /domains/1
|
||||
|
||||
Fetch one domain
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /domains 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
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"name": "www.example.com",
|
||||
}
|
||||
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
"""
|
||||
return service.get(domain_id)
|
||||
|
||||
|
||||
class CertificateDomains(AuthenticatedResource):
|
||||
""" Defines the 'domains' endpoint """
|
||||
def __init__(self):
|
||||
super(CertificateDomains, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
def get(self, certificate_id):
|
||||
"""
|
||||
.. http:get:: /certificates/1/domains
|
||||
|
||||
The current domain list
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /domains 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": 1,
|
||||
"name": "www.example.com",
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "www.example2.com",
|
||||
}
|
||||
]
|
||||
"total": 2
|
||||
}
|
||||
|
||||
:query sortBy: field to sort on
|
||||
:query sortDir: acs or desc
|
||||
:query page: int. default is 1
|
||||
:query filter: key value pair. format is k=v;
|
||||
:query limit: limit number. default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
"""
|
||||
parser = paginated_parser.copy()
|
||||
args = parser.parse_args()
|
||||
args['certificate_id'] = certificate_id
|
||||
return service.render(args)
|
||||
|
||||
|
||||
api.add_resource(DomainsList, '/domains', endpoint='domains')
|
||||
api.add_resource(Domains, '/domains/<int:domain_id>', endpoint='domain')
|
||||
api.add_resource(CertificateDomains, '/certificates/<int:certificate_id>/domains', endpoint='certificateDomains')
|
0
lemur/elbs/__init__.py
Normal file
0
lemur/elbs/__init__.py
Normal file
44
lemur/elbs/models.py
Normal file
44
lemur/elbs/models.py
Normal file
@ -0,0 +1,44 @@
|
||||
"""
|
||||
.. module: lemur.elbs.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, BigInteger, String, ForeignKey, DateTime, PassiveDefault, func
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from lemur.database import db
|
||||
from lemur.listeners.models import Listener
|
||||
|
||||
|
||||
class ELB(db.Model):
|
||||
__tablename__ = 'elbs'
|
||||
id = Column(BigInteger, primary_key=True)
|
||||
account_id = Column(BigInteger, ForeignKey("accounts.id"), index=True)
|
||||
region = Column(String(32))
|
||||
name = Column(String(128))
|
||||
vpc_id = Column(String(128))
|
||||
scheme = Column(String(128))
|
||||
dns_name = Column(String(128))
|
||||
listeners = relationship("Listener", backref='elb', cascade="all, delete, delete-orphan")
|
||||
date_created = Column(DateTime, PassiveDefault(func.now()), nullable=False)
|
||||
|
||||
def __init__(self, elb_obj=None):
|
||||
if elb_obj:
|
||||
self.region = elb_obj.connection.region.name
|
||||
self.name = elb_obj.name
|
||||
self.vpc_id = elb_obj.vpc_id
|
||||
self.scheme = elb_obj.scheme
|
||||
self.dns_name = elb_obj.dns_name
|
||||
for listener in elb_obj.listeners:
|
||||
self.listeners.append(Listener(listener))
|
||||
|
||||
def as_dict(self):
|
||||
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
|
||||
|
||||
def serialize(self):
|
||||
blob = self.as_dict()
|
||||
del blob['date_created']
|
||||
return blob
|
125
lemur/elbs/service.py
Normal file
125
lemur/elbs/service.py
Normal file
@ -0,0 +1,125 @@
|
||||
"""
|
||||
.. module: lemur.elbs.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 sqlalchemy import func
|
||||
from sqlalchemy.sql import and_
|
||||
|
||||
from lemur import database
|
||||
from lemur.elbs.models import ELB
|
||||
from lemur.listeners.models import Listener
|
||||
|
||||
def get_all(account_id, elb_name):
|
||||
"""
|
||||
Retrieves all ELBs in a given account
|
||||
|
||||
:param account_id:
|
||||
:param elb_name:
|
||||
:rtype : Elb
|
||||
:return:
|
||||
"""
|
||||
query = database.session_query(ELB)
|
||||
return query.filter(and_(ELB.name == elb_name, ELB.account_id == account_id)).all()
|
||||
|
||||
|
||||
def get_by_region_and_account(region, account_id):
|
||||
query = database.session_query(ELB)
|
||||
return query.filter(and_(ELB.region == region, ELB.account_id == account_id)).all()
|
||||
|
||||
|
||||
def get_all_elbs():
|
||||
"""
|
||||
Get all ELBs that Lemur knows about
|
||||
|
||||
:rtype : list
|
||||
:return:
|
||||
"""
|
||||
return ELB.query.all()
|
||||
|
||||
|
||||
def get(elb_id):
|
||||
"""
|
||||
Retrieve an ELB with a give ID
|
||||
|
||||
:rtype : Elb
|
||||
:param elb_id:
|
||||
:return:
|
||||
"""
|
||||
return database.get(ELB, elb_id)
|
||||
|
||||
|
||||
def create(account, elb):
|
||||
"""
|
||||
Create a new ELB
|
||||
|
||||
:param account:
|
||||
:param elb:
|
||||
"""
|
||||
elb = ELB(elb)
|
||||
account.elbs.append(elb)
|
||||
database.create(elb)
|
||||
|
||||
|
||||
def delete(elb_id):
|
||||
"""
|
||||
Delete an ELB
|
||||
|
||||
:param elb_id:
|
||||
"""
|
||||
database.delete(get(elb_id))
|
||||
|
||||
|
||||
def render(args):
|
||||
query = database.session_query(ELB)
|
||||
|
||||
sort_by = args.pop('sort_by')
|
||||
sort_dir = args.pop('sort_dir')
|
||||
page = args.pop('page')
|
||||
count = args.pop('count')
|
||||
filt = args.pop('filter')
|
||||
active = args.pop('active')
|
||||
certificate_id = args.pop('certificate_id')
|
||||
|
||||
if certificate_id:
|
||||
query.filter(ELB.listeners.any(Listener.certificate_id == certificate_id))
|
||||
|
||||
if active == 'true':
|
||||
query = query.filter(ELB.listeners.any())
|
||||
|
||||
if filt:
|
||||
terms = filt.split(';')
|
||||
query = database.filter(query, ELB, terms)
|
||||
|
||||
query = database.find_all(query, ELB, args)
|
||||
|
||||
if sort_by and sort_dir:
|
||||
query = database.sort(query, ELB, sort_by, sort_dir)
|
||||
|
||||
return database.paginate(query, page, count)
|
||||
|
||||
|
||||
def stats(**kwargs):
|
||||
attr = getattr(ELB, kwargs.get('metric'))
|
||||
query = database.db.session.query(attr, func.count(attr))
|
||||
|
||||
if kwargs.get('account_id'):
|
||||
query = query.filter(ELB.account_id == kwargs.get('account_id'))
|
||||
|
||||
if kwargs.get('active') == 'true':
|
||||
query = query.join(ELB.listeners)
|
||||
query = query.filter(Listener.certificate_id != None)
|
||||
|
||||
items = query.group_by(attr).all()
|
||||
|
||||
results = []
|
||||
for key, count in items:
|
||||
if key:
|
||||
results.append({"key": key, "y": count})
|
||||
return results
|
||||
|
||||
|
72
lemur/elbs/sync.py
Normal file
72
lemur/elbs/sync.py
Normal file
@ -0,0 +1,72 @@
|
||||
|
||||
"""
|
||||
.. module: lemur.elbs.sync
|
||||
:platform: Unix
|
||||
:synopsis: This module attempts to sync with AWS and ensure that all elbs
|
||||
currently available in AWS are available in Lemur as well
|
||||
|
||||
: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 current_app
|
||||
from lemur.accounts import service as account_service
|
||||
from lemur.elbs import service as elb_service
|
||||
from lemur.common.services.aws.elb import get_all_elbs, get_all_regions
|
||||
|
||||
|
||||
def create_new(known, aws, account):
|
||||
new = 0
|
||||
for elb in aws:
|
||||
for n in known:
|
||||
if elb.name == n.name:
|
||||
break
|
||||
else:
|
||||
new += 1
|
||||
current_app.logger.debug("Creating {0}".format(elb.name))
|
||||
try:
|
||||
elb_service.create(account, elb)
|
||||
except AttributeError as e:
|
||||
current_app.logger.exception(e)
|
||||
return new
|
||||
|
||||
|
||||
def remove_missing(known, aws):
|
||||
deleted = 0
|
||||
for ke in known:
|
||||
for elb in aws:
|
||||
if elb.name == ke.name:
|
||||
break
|
||||
else:
|
||||
deleted += 1
|
||||
current_app.logger.debug("Deleting {0}".format(ke.name))
|
||||
elb_service.delete(ke.id)
|
||||
return deleted
|
||||
|
||||
|
||||
def sync_all_elbs():
|
||||
for account in account_service.get_all():
|
||||
regions = get_all_regions()
|
||||
for region in regions:
|
||||
current_app.logger.info("Importing ELBs from '{0}/{1}/{2}'... ".format(account.account_number, account.label, region))
|
||||
try:
|
||||
aws_elbs = get_all_elbs(account.account_number, region)
|
||||
except Exception as e:
|
||||
current_app.logger.error("Failed to get ELBS from '{0}/{1}/{2}' reason: {3}".format(
|
||||
account.label, account.account_number, region, e.message)
|
||||
)
|
||||
continue
|
||||
|
||||
known_elbs = elb_service.get_by_region_and_account(region, account.id)
|
||||
|
||||
new_elbs = create_new(known_elbs, aws_elbs, account)
|
||||
current_app.logger.info(
|
||||
"Created {0} new ELBs in '{1}/{2}/{3}'...".format(
|
||||
new_elbs, account.account_number, account.label, region))
|
||||
|
||||
deleted_elbs = remove_missing(known_elbs, aws_elbs)
|
||||
current_app.logger.info(
|
||||
"Deleted {0} missing ELBs from '{1}/{2}/{3}'...".format(
|
||||
deleted_elbs, account.account_number, account.label, region))
|
78
lemur/elbs/views.py
Normal file
78
lemur/elbs/views.py
Normal file
@ -0,0 +1,78 @@
|
||||
"""
|
||||
.. module: lemur.elbs.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 flask import Blueprint
|
||||
from flask.ext.restful import reqparse, Api, fields
|
||||
from lemur.elbs import service
|
||||
from lemur.auth.service import AuthenticatedResource
|
||||
|
||||
from lemur.common.utils import marshal_items, paginated_parser
|
||||
|
||||
|
||||
mod = Blueprint('elbs', __name__)
|
||||
api = Api(mod)
|
||||
|
||||
|
||||
FIELDS = {
|
||||
'name': fields.String,
|
||||
'id': fields.Integer,
|
||||
'region': fields.String,
|
||||
'scheme': fields.String,
|
||||
'accountId': fields.Integer(attribute='account_id'),
|
||||
'vpcId': fields.String(attribute='vpc_id')
|
||||
}
|
||||
|
||||
|
||||
class ELBsList(AuthenticatedResource):
|
||||
""" Defines the 'elbs' endpoint """
|
||||
def __init__(self):
|
||||
super(ELBsList, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
def get(self):
|
||||
parser = paginated_parser.copy()
|
||||
parser.add_argument('owner', type=str, location='args')
|
||||
parser.add_argument('id', type=str, location='args')
|
||||
parser.add_argument('accountId', type=str, dest='account_id', location='args')
|
||||
parser.add_argument('certificateId', type=str, dest='certificate_id', location='args')
|
||||
parser.add_argument('active', type=str, default='true', location='args')
|
||||
|
||||
args = parser.parse_args()
|
||||
return service.render(args)
|
||||
|
||||
|
||||
class ELBsStats(AuthenticatedResource):
|
||||
def __init__(self):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(ELBsStats, self).__init__()
|
||||
|
||||
def get(self):
|
||||
self.reqparse.add_argument('metric', type=str, location='args')
|
||||
self.reqparse.add_argument('accountId', dest='account_id', location='args')
|
||||
self.reqparse.add_argument('active', type=str, default='true', location='args')
|
||||
|
||||
args = self.reqparse.parse_args()
|
||||
|
||||
items = service.stats(**args)
|
||||
return {"items": items, "total": len(items)}
|
||||
|
||||
|
||||
class ELBs(AuthenticatedResource):
|
||||
def __init__(self):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(ELBs, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
def get(self, elb_id):
|
||||
return service.get(elb_id)
|
||||
|
||||
|
||||
api.add_resource(ELBsList, '/elbs', endpoint='elbs')
|
||||
api.add_resource(ELBs, '/elbs/<int:elb_id>', endpoint='elb')
|
||||
api.add_resource(ELBsStats, '/elbs/stats', endpoint='elbsStats')
|
61
lemur/exceptions.py
Normal file
61
lemur/exceptions.py
Normal file
@ -0,0 +1,61 @@
|
||||
"""
|
||||
.. module: lemur.exceptions
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
"""
|
||||
from flask import current_app
|
||||
|
||||
|
||||
class LemurException(Exception):
|
||||
def __init__(self):
|
||||
current_app.logger.error(self)
|
||||
|
||||
|
||||
class AuthenticationFailedException(LemurException):
|
||||
def __init__(self, remote_ip, user_agent):
|
||||
self.remote_ip = remote_ip
|
||||
self.user_agent = user_agent
|
||||
|
||||
def __str__(self):
|
||||
return repr("Failed login from: {} {}".format(self.remote_ip, self.user_agent))
|
||||
|
||||
|
||||
class IntegrityError(LemurException):
|
||||
def __init__(self, message):
|
||||
self.message = message
|
||||
|
||||
def __str__(self):
|
||||
return repr(self.message)
|
||||
|
||||
|
||||
class InvalidListener(LemurException):
|
||||
def __str__(self):
|
||||
return repr("Invalid listener, ensure you select a certificate if you are using a secure protocol")
|
||||
|
||||
|
||||
class CertificateUnavailable(LemurException):
|
||||
def __str__(self):
|
||||
return repr("The certificate requested is not available")
|
||||
|
||||
|
||||
class AttrNotFound(LemurException):
|
||||
def __init__(self, field):
|
||||
self.field = field
|
||||
|
||||
def __str__(self):
|
||||
return repr("The field '{0}' is not sortable".format(self.field))
|
||||
|
||||
|
||||
class NoPersistanceFound(Exception):
|
||||
def __str__(self):
|
||||
return repr("No peristence method found, Lemur cannot persist sensitive information")
|
||||
|
||||
|
||||
class NoEncryptionKeyFound(Exception):
|
||||
def __str__(self):
|
||||
return repr("Aborting... Lemur cannot locate db encryption key, is ENCRYPTION_KEY set?")
|
||||
|
||||
|
||||
class InvalidToken(Exception):
|
||||
def __str__(self):
|
||||
return repr("Invalid token")
|
18
lemur/extensions.py
Normal file
18
lemur/extensions.py
Normal file
@ -0,0 +1,18 @@
|
||||
"""
|
||||
.. module: lemur.extensions
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
from flask.ext.sqlalchemy import SQLAlchemy
|
||||
db = SQLAlchemy()
|
||||
|
||||
from flask.ext.migrate import Migrate
|
||||
migrate = Migrate()
|
||||
|
||||
from flask.ext.bcrypt import Bcrypt
|
||||
bcrypt = Bcrypt()
|
||||
|
||||
from flask.ext.principal import Principal
|
||||
principal = Principal()
|
||||
|
138
lemur/factory.py
Normal file
138
lemur/factory.py
Normal file
@ -0,0 +1,138 @@
|
||||
"""
|
||||
.. module: lemur.factory
|
||||
:platform: Unix
|
||||
:synopsis: This module contains all the needed functions to allow
|
||||
the factory app creation.
|
||||
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
|
||||
"""
|
||||
import os
|
||||
import imp
|
||||
import errno
|
||||
|
||||
from logging import Formatter
|
||||
from logging.handlers import RotatingFileHandler
|
||||
|
||||
from flask import Flask
|
||||
from lemur.common.health import mod as health
|
||||
from lemur.exceptions import NoEncryptionKeyFound
|
||||
from lemur.extensions import db, migrate, principal
|
||||
|
||||
|
||||
DEFAULT_BLUEPRINTS = (
|
||||
health,
|
||||
)
|
||||
|
||||
API_VERSION = 1
|
||||
|
||||
|
||||
def create_app(app_name=None, blueprints=None, config=None):
|
||||
"""
|
||||
Lemur application factory
|
||||
|
||||
:param config:
|
||||
:param app_name:
|
||||
:param blueprints:
|
||||
:return:
|
||||
"""
|
||||
if not blueprints:
|
||||
blueprints = DEFAULT_BLUEPRINTS
|
||||
else:
|
||||
blueprints = blueprints + DEFAULT_BLUEPRINTS
|
||||
|
||||
if not app_name:
|
||||
app_name = __name__
|
||||
|
||||
app = Flask(app_name)
|
||||
configure_app(app, config)
|
||||
configure_blueprints(app, blueprints)
|
||||
configure_extensions(app)
|
||||
configure_logging(app)
|
||||
return app
|
||||
|
||||
|
||||
def from_file(file_path, silent=False):
|
||||
"""
|
||||
Updates the values in the config from a Python file. This function
|
||||
behaves as if the file was imported as module with the
|
||||
|
||||
:param file_path:
|
||||
:param silent:
|
||||
"""
|
||||
d = imp.new_module('config')
|
||||
d.__file__ = file_path
|
||||
try:
|
||||
with open(file_path) as config_file:
|
||||
exec(compile(config_file.read(), file_path, 'exec'), d.__dict__)
|
||||
except IOError as e:
|
||||
if silent and e.errno in (errno.ENOENT, errno.EISDIR):
|
||||
return False
|
||||
e.strerror = 'Unable to load configuration file (%s)' % e.strerror
|
||||
raise
|
||||
return d
|
||||
|
||||
|
||||
def configure_app(app, config=None):
|
||||
"""
|
||||
Different ways of configuration
|
||||
|
||||
:param app:
|
||||
:param config:
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
app.config.from_envvar("LEMUR_SETTINGS")
|
||||
except RuntimeError:
|
||||
if config and config != 'None':
|
||||
app.config.from_object(from_file(config))
|
||||
else:
|
||||
app.config.from_object(from_file(os.path.expanduser("~/.lemur/lemur.conf.py")))
|
||||
|
||||
if not app.config.get('ENCRYPTION_KEY'):
|
||||
raise NoEncryptionKeyFound
|
||||
|
||||
|
||||
def configure_extensions(app):
|
||||
"""
|
||||
Attaches and configures any needed flask extensions
|
||||
to our app.
|
||||
|
||||
:param app:
|
||||
"""
|
||||
db.init_app(app)
|
||||
migrate.init_app(app, db)
|
||||
principal.init_app(app)
|
||||
|
||||
|
||||
def configure_blueprints(app, blueprints):
|
||||
"""
|
||||
We prefix our APIs with their given version so that we can support
|
||||
multiple concurrent API versions.
|
||||
|
||||
:param app:
|
||||
:param blueprints:
|
||||
"""
|
||||
for blueprint in blueprints:
|
||||
app.register_blueprint(blueprint, url_prefix="/api/{0}".format(API_VERSION))
|
||||
|
||||
|
||||
def configure_logging(app):
|
||||
"""
|
||||
Sets up application wide logging.
|
||||
|
||||
:param app:
|
||||
"""
|
||||
handler = RotatingFileHandler(app.config.get('LOG_FILE', 'lemur.log'), maxBytes=10000000, backupCount=100)
|
||||
|
||||
handler.setFormatter(Formatter(
|
||||
'%(asctime)s %(levelname)s: %(message)s '
|
||||
'[in %(pathname)s:%(lineno)d]'
|
||||
))
|
||||
|
||||
handler.setLevel(app.config.get('LOG_LEVEL', 'DEBUG'))
|
||||
app.logger.setLevel(app.config.get('LOG_LEVEL', 'DEBUG'))
|
||||
app.logger.addHandler(handler)
|
||||
|
0
lemur/listeners/__init__.py
Normal file
0
lemur/listeners/__init__.py
Normal file
43
lemur/listeners/models.py
Normal file
43
lemur/listeners/models.py
Normal file
@ -0,0 +1,43 @@
|
||||
"""
|
||||
.. module: lemur.elbs.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, BigInteger, String, ForeignKey, DateTime, PassiveDefault, func
|
||||
|
||||
from lemur.database import db
|
||||
from lemur.certificates import service as cert_service
|
||||
from lemur.certificates.models import Certificate, get_name_from_arn
|
||||
|
||||
|
||||
class Listener(db.Model):
|
||||
__tablename__ = 'listeners'
|
||||
id = Column(BigInteger, primary_key=True)
|
||||
certificate_id = Column(Integer, ForeignKey(Certificate.id), index=True)
|
||||
elb_id = Column(BigInteger, ForeignKey("elbs.id"), index=True)
|
||||
instance_port = Column(Integer)
|
||||
instance_protocol = Column(String(16))
|
||||
load_balancer_port = Column(Integer)
|
||||
load_balancer_protocol = Column(String(16))
|
||||
date_created = Column(DateTime, PassiveDefault(func.now()), nullable=False)
|
||||
|
||||
def __init__(self, listener):
|
||||
self.load_balancer_port = listener.load_balancer_port
|
||||
self.load_balancer_protocol = listener.protocol
|
||||
self.instance_port = listener.instance_port
|
||||
self.instance_protocol = listener.instance_protocol
|
||||
if listener.ssl_certificate_id not in ["Invalid-Certificate", None]:
|
||||
self.certificate_id = cert_service.get_by_name(get_name_from_arn(listener.ssl_certificate_id)).id
|
||||
|
||||
def as_dict(self):
|
||||
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
|
||||
|
||||
def serialize(self):
|
||||
blob = self.as_dict()
|
||||
del blob['date_created']
|
||||
return blob
|
||||
|
162
lemur/listeners/service.py
Normal file
162
lemur/listeners/service.py
Normal file
@ -0,0 +1,162 @@
|
||||
"""
|
||||
.. module: lemur.listeners.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 sqlalchemy import func
|
||||
|
||||
from lemur import database
|
||||
|
||||
from lemur.exceptions import CertificateUnavailable
|
||||
|
||||
from lemur.elbs.models import ELB
|
||||
from lemur.listeners.models import Listener
|
||||
from lemur.elbs import service as elb_service
|
||||
from lemur.certificates import service as certificate_service
|
||||
|
||||
from lemur.common.services.aws.elb import update_listeners, create_new_listeners, delete_listeners
|
||||
|
||||
|
||||
def verify_attachment(certificate_id, elb_account_number):
|
||||
"""
|
||||
Ensures that the certificate we want ot attach to our listener is
|
||||
in the same account as our listener.
|
||||
|
||||
:rtype : Certificate
|
||||
:param certificate_id:
|
||||
:param elb_account_number:
|
||||
:return: :raise CertificateUnavailable:
|
||||
"""
|
||||
cert = certificate_service.get(certificate_id)
|
||||
|
||||
# we need to ensure that the specified cert is in our account
|
||||
for account in cert.accounts:
|
||||
if account.account_number == elb_account_number:
|
||||
break
|
||||
else:
|
||||
raise CertificateUnavailable
|
||||
return cert
|
||||
|
||||
|
||||
def get(listener_id):
|
||||
return database.get(Listener, listener_id)
|
||||
|
||||
|
||||
def create(elb_id, instance_protocol, instance_port, load_balancer_port, load_balancer_protocol, certificate_id=None):
|
||||
listener = Listener(elb_id,
|
||||
instance_port,
|
||||
instance_protocol,
|
||||
load_balancer_port,
|
||||
load_balancer_protocol
|
||||
)
|
||||
|
||||
elb = elb_service.get(elb_id)
|
||||
elb.listeners.append(listener)
|
||||
account_number = elb.account.account_number
|
||||
|
||||
cert = verify_attachment(certificate_id, account_number)
|
||||
listener_tuple = (load_balancer_port, instance_port, load_balancer_protocol, cert.get_art(account_number),)
|
||||
create_new_listeners(account_number, elb.region, elb.name, [listener_tuple])
|
||||
|
||||
return {'message': 'Listener has been created'}
|
||||
|
||||
|
||||
def update(listener_id, **kwargs):
|
||||
listener = get(listener_id)
|
||||
|
||||
# if the lb_port has changed we need to make sure we are deleting
|
||||
# the listener on the old port to avoid listener duplication
|
||||
ports = []
|
||||
if listener.load_balancer_port != kwargs.get('load_balancer_port'):
|
||||
ports.append(listener.load_balancer_port)
|
||||
else:
|
||||
ports.append(kwargs.get('load_balancer_port'))
|
||||
|
||||
certificate_id = kwargs.get('certificate_id')
|
||||
|
||||
listener.instance_port = kwargs.get('instance_port')
|
||||
listener.instance_protocol = kwargs.get('instance_protocol')
|
||||
listener.load_balancer_port = kwargs.get('load_balancer_port')
|
||||
listener.load_balancer_protocol = kwargs.get('load_balancer_protocol')
|
||||
|
||||
elb = listener.elb
|
||||
account_number = listener.elb.account.account_number
|
||||
|
||||
arn = None
|
||||
if certificate_id:
|
||||
cert = verify_attachment(certificate_id, account_number)
|
||||
cert.elb_listeners.append(listener)
|
||||
arn = cert.get_arn(account_number)
|
||||
|
||||
# remove certificate that is no longer wanted
|
||||
if listener.certificate and not certificate_id:
|
||||
listener.certificate.remove()
|
||||
|
||||
database.update(listener)
|
||||
listener_tuple = (listener.load_balancer_port, listener.instance_port, listener.load_balancer_protocol, arn,)
|
||||
update_listeners(account_number, elb.region, elb.name, [listener_tuple], ports)
|
||||
|
||||
return {'message': 'Listener has been updated'}
|
||||
|
||||
|
||||
def delete(listener_id):
|
||||
# first try to delete the listener in aws
|
||||
listener = get(listener_id)
|
||||
delete_listeners(listener.elb.account.account_number, listener.elb.region, listener.elb.name, [listener.load_balancer_port])
|
||||
# cleanup operation in lemur
|
||||
database.delete(listener)
|
||||
|
||||
|
||||
def render(args):
|
||||
query = database.session_query(Listener)
|
||||
|
||||
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)
|
||||
elb_id = args.pop('elb_id', None)
|
||||
|
||||
if certificate_id:
|
||||
query = database.get_all(Listener, certificate_id, field='certificate_id')
|
||||
|
||||
if elb_id:
|
||||
query = query.filter(Listener.elb_id == elb_id)
|
||||
|
||||
if filt:
|
||||
terms = filt.split(';')
|
||||
query = database.filter(query, Listener, terms)
|
||||
|
||||
query = database.find_all(query, Listener, args)
|
||||
|
||||
if sort_by and sort_dir:
|
||||
query = database.sort(query, Listener, sort_by, sort_dir)
|
||||
|
||||
return database.paginate(query, page, count)
|
||||
|
||||
|
||||
def stats(**kwargs):
|
||||
attr = getattr(Listener, kwargs.get('metric'))
|
||||
query = database.db.session.query(attr, func.count(attr))
|
||||
query = query.join(Listener.elb)
|
||||
|
||||
if kwargs.get('account_id'):
|
||||
query = query.filter(ELB.account_id == kwargs.get('account_id'))
|
||||
|
||||
if kwargs.get('active') == 'true':
|
||||
query = query.filter(Listener.certificate_id != None)
|
||||
|
||||
items = query.group_by(attr).all()
|
||||
results = []
|
||||
for key, count in items:
|
||||
if key:
|
||||
results.append({"key": key, "y": count})
|
||||
return results
|
||||
|
||||
|
||||
|
128
lemur/listeners/views.py
Normal file
128
lemur/listeners/views.py
Normal file
@ -0,0 +1,128 @@
|
||||
"""
|
||||
.. module: lemur.listeners.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 flask import Blueprint
|
||||
from flask.ext.restful import reqparse, Api, fields
|
||||
|
||||
from lemur.listeners import service
|
||||
from lemur.auth.service import AuthenticatedResource
|
||||
from lemur.auth.permissions import admin_permission
|
||||
from lemur.common.utils import marshal_items, paginated_parser
|
||||
|
||||
|
||||
mod = Blueprint('listeners', __name__)
|
||||
api = Api(mod)
|
||||
|
||||
|
||||
FIELDS = {
|
||||
'id': fields.Integer,
|
||||
'elbId': fields.Integer(attribute="elb_id"),
|
||||
'certificateId': fields.Integer(attribute="certificate_id"),
|
||||
'instancePort': fields.Integer(attribute="instance_port"),
|
||||
'instanceProtocol': fields.String(attribute="instance_protocol"),
|
||||
'loadBalancerPort': fields.Integer(attribute="load_balancer_port"),
|
||||
'loadBalancerProtocol': fields.String(attribute="load_balancer_protocol")
|
||||
}
|
||||
|
||||
|
||||
class ListenersList(AuthenticatedResource):
|
||||
def __init__(self):
|
||||
super(ListenersList, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
def get(self):
|
||||
parser = paginated_parser.copy()
|
||||
parser.add_argument('certificateId', type=int, dest='certificate_id', location='args')
|
||||
args = parser.parse_args()
|
||||
return service.render(args)
|
||||
|
||||
|
||||
class ListenersCertificateList(AuthenticatedResource):
|
||||
def __init__(self):
|
||||
super(ListenersCertificateList, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
def get(self, certificate_id):
|
||||
parser = paginated_parser.copy()
|
||||
args = parser.parse_args()
|
||||
args['certificate_id'] = certificate_id
|
||||
return service.render(args)
|
||||
|
||||
|
||||
class ListenersELBList(AuthenticatedResource):
|
||||
def __init__(self):
|
||||
super(ListenersELBList, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
def get(self, elb_id):
|
||||
parser = paginated_parser.copy()
|
||||
args = parser.parse_args()
|
||||
args['elb_id'] = elb_id
|
||||
return service.render(args)
|
||||
|
||||
|
||||
class ListenersStats(AuthenticatedResource):
|
||||
def __init__(self):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(ListenersStats, self).__init__()
|
||||
|
||||
def get(self):
|
||||
self.reqparse.add_argument('metric', type=str, location='args')
|
||||
self.reqparse.add_argument('accountId', dest='account_id', location='args')
|
||||
self.reqparse.add_argument('active', type=str, default='true', location='args')
|
||||
|
||||
args = self.reqparse.parse_args()
|
||||
|
||||
items = service.stats(**args)
|
||||
return {"items": items, "total": len(items)}
|
||||
|
||||
|
||||
class Listeners(AuthenticatedResource):
|
||||
def __init__(self):
|
||||
super(Listeners, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
def get(self, listener_id):
|
||||
return service.get(listener_id)
|
||||
|
||||
@admin_permission.require(http_exception=403)
|
||||
@marshal_items(FIELDS)
|
||||
def post(self):
|
||||
self.reqparse.add_argument('elbId', type=str, dest='elb_id', required=True, location='json')
|
||||
self.reqparse.add_argument('instanceProtocol', type=str, dest='instance_protocol', required=True, location='json')
|
||||
self.reqparse.add_argument('instancePort', type=int, dest='instance_port', required=True, location='json')
|
||||
self.reqparse.add_argument('loadBalancerProtocol', type=str, dest='load_balancer_protocol', required=True, location='json')
|
||||
self.reqparse.add_argument('loadBalancerPort', type=int, dest='load_balancer_port', required=True, location='json')
|
||||
self.reqparse.add_argument('certificateId', type=int, dest='certificate_id', location='json')
|
||||
|
||||
args = self.reqparse.parse_args()
|
||||
return service.create(**args)
|
||||
|
||||
@admin_permission.require(http_exception=403)
|
||||
@marshal_items(FIELDS)
|
||||
def put(self, listener_id):
|
||||
self.reqparse.add_argument('instanceProtocol', type=str, dest='instance_protocol', required=True, location='json')
|
||||
self.reqparse.add_argument('instancePort', type=int, dest='instance_port', required=True, location='json')
|
||||
self.reqparse.add_argument('loadBalancerProtocol', type=str, dest='load_balancer_protocol', required=True, location='json')
|
||||
self.reqparse.add_argument('loadBalancerPort', type=int, dest='load_balancer_port', required=True, location='json')
|
||||
self.reqparse.add_argument('certificateId', type=int, dest='certificate_id', location='json')
|
||||
|
||||
args = self.reqparse.parse_args()
|
||||
return service.update(listener_id, **args)
|
||||
|
||||
@admin_permission.require(http_exception=403)
|
||||
def delete(self, listener_id):
|
||||
return service.delete(listener_id)
|
||||
|
||||
|
||||
api.add_resource(ListenersList, '/listeners', endpoint='listeners')
|
||||
api.add_resource(Listeners, '/listeners/<int:listener_id>', endpoint='listener')
|
||||
api.add_resource(ListenersStats, '/listeners/stats', endpoint='listenersStats')
|
||||
api.add_resource(ListenersCertificateList, '/certificates/<int:certificate_id>/listeners', endpoint='listenersCertificates')
|
||||
api.add_resource(ListenersELBList, '/elbs/<int:elb_id>/listeners', endpoint='elbListeners')
|
507
lemur/manage.py
Normal file
507
lemur/manage.py
Normal file
@ -0,0 +1,507 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
import sys
|
||||
import base64
|
||||
from gunicorn.config import make_settings
|
||||
|
||||
from flask import current_app
|
||||
from flask.ext.script import Manager, Command, Option, Group, prompt_pass
|
||||
from flask.ext.migrate import Migrate, MigrateCommand, stamp
|
||||
from flask_script.commands import ShowUrls, Clean, Server
|
||||
|
||||
from lemur import database
|
||||
from lemur.users import service as user_service
|
||||
from lemur.roles import service as role_service
|
||||
from lemur.accounts import service as account_service
|
||||
from lemur.certificates import service as cert_service
|
||||
|
||||
from lemur.certificates.verify import verify_string
|
||||
from lemur.certificates import sync
|
||||
from lemur.elbs.sync import sync_all_elbs
|
||||
|
||||
from lemur import create_app
|
||||
from lemur.common.crypto import encrypt, decrypt, lock, unlock
|
||||
|
||||
# Needed to be imported so that SQLAlchemy create_all can find our models
|
||||
from lemur.users.models import User
|
||||
from lemur.roles.models import Role
|
||||
from lemur.authorities.models import Authority
|
||||
from lemur.certificates.models import Certificate
|
||||
from lemur.accounts.models import Account
|
||||
from lemur.domains.models import Domain
|
||||
from lemur.elbs.models import ELB
|
||||
from lemur.listeners.models import Listener
|
||||
|
||||
manager = Manager(create_app)
|
||||
manager.add_option('-c', '--config', dest='config')
|
||||
|
||||
migrate = Migrate(create_app)
|
||||
|
||||
KEY_LENGTH = 40
|
||||
DEFAULT_CONFIG_PATH = '~/.lemur/lemur.conf.py'
|
||||
DEFAULT_SETTINGS = 'lemur.conf.server'
|
||||
SETTINGS_ENVVAR = 'LEMUR_CONF'
|
||||
|
||||
|
||||
CONFIG_TEMPLATE = """
|
||||
# This is just Python which means you can inherit and tweak settings
|
||||
|
||||
import os
|
||||
_basedir = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
ADMINS = frozenset([''])
|
||||
|
||||
THREADS_PER_PAGE = 8
|
||||
|
||||
#############
|
||||
## General ##
|
||||
#############
|
||||
|
||||
# These will need to be set to `True` if you are developing locally
|
||||
CORS = False
|
||||
debug = False
|
||||
|
||||
# You should consider storing these separately from your config
|
||||
LEMUR_SECRET_TOKEN = '{secret_token}'
|
||||
LEMUR_ENCRYPTION_KEY = '{encryption_key}'
|
||||
|
||||
# this is a list of domains as regexes that only admins can issue
|
||||
LEMUR_RESTRICTED_DOMAINS = []
|
||||
|
||||
#################
|
||||
## Mail Server ##
|
||||
#################
|
||||
|
||||
# Lemur currently only supports SES for sending email, this address
|
||||
# needs to be verified
|
||||
LEMUR_EMAIL = ''
|
||||
LEMUR_SECURITY_TEAM_EMAIL = []
|
||||
|
||||
#############
|
||||
## Logging ##
|
||||
#############
|
||||
|
||||
LOG_LEVEL = "DEBUG"
|
||||
LOG_FILE = "lemur.log"
|
||||
|
||||
|
||||
##############
|
||||
## Database ##
|
||||
##############
|
||||
|
||||
SQLALCHEMY_DATABASE_URI = ''
|
||||
|
||||
|
||||
#########
|
||||
## AWS ##
|
||||
#########
|
||||
|
||||
# Lemur will need STS assume role access to every account you want to monitor
|
||||
#AWS_ACCOUNT_MAPPINGS = {{
|
||||
# '1111111111': 'myawsacount'
|
||||
#}}
|
||||
|
||||
## This is useful if you know you only want to monitor one account
|
||||
#AWS_REGIONS = ['us-east-1']
|
||||
|
||||
#LEMUR_INSTANCE_PROFILE = 'Lemur'
|
||||
|
||||
#############
|
||||
## Issuers ##
|
||||
#############
|
||||
|
||||
# These will be dependent on which 3rd party that Lemur is
|
||||
# configured to use.
|
||||
|
||||
#CLOUDCA_URL = ''
|
||||
#CLOUDCA_PEM_PATH = ''
|
||||
#CLOUDCA_BUNDLE = ''
|
||||
|
||||
# number of years to issue if not specified
|
||||
#CLOUDCA_DEFAULT_VALIDITY = 2
|
||||
|
||||
#VERISIGN_URL = ''
|
||||
#VERISIGN_PEM_PATH = ''
|
||||
#VERISIGN_FIRST_NAME = ''
|
||||
#VERISIGN_LAST_NAME = ''
|
||||
#VERSIGN_EMAIL = ''
|
||||
"""
|
||||
|
||||
@MigrateCommand.command
|
||||
def create():
|
||||
database.db.create_all()
|
||||
stamp(revision='head')
|
||||
|
||||
|
||||
@manager.command
|
||||
def lock():
|
||||
"""
|
||||
Encrypts all of the files in the `keys` directory with the password
|
||||
given. This is a useful function to ensure that you do no check in
|
||||
your key files into source code in clear text.
|
||||
|
||||
:return:
|
||||
"""
|
||||
password = prompt_pass("Please enter the encryption password")
|
||||
lock(password)
|
||||
sys.stdout.write("[+] Lemur keys have been encrypted!\n")
|
||||
|
||||
|
||||
@manager.command
|
||||
def unlock():
|
||||
"""
|
||||
Decrypts all of the files in the `keys` directory with the password
|
||||
given. This is most commonly used during the startup sequence of Lemur
|
||||
allowing it to go from source code to something that can communicate
|
||||
with external services.
|
||||
|
||||
:return:
|
||||
"""
|
||||
password = prompt_pass("Please enter the encryption password")
|
||||
unlock(password)
|
||||
sys.stdout.write("[+] Lemur keys have been unencrypted!\n")
|
||||
|
||||
|
||||
@manager.command
|
||||
def encrypt_file(source):
|
||||
"""
|
||||
Utility to encrypt sensitive files, Lemur will decrypt these
|
||||
files when admin enters the correct password.
|
||||
|
||||
Uses AES-256-CBC encryption
|
||||
"""
|
||||
dest = source + ".encrypted"
|
||||
password = prompt_pass("Please enter the encryption password")
|
||||
password1 = prompt_pass("Please confirm the encryption password")
|
||||
if password != password1:
|
||||
sys.stdout.write("[!] Encryption passwords do not match!\n")
|
||||
return
|
||||
|
||||
with open(source, 'rb') as in_file, open(dest, 'wb') as out_file:
|
||||
encrypt(in_file, out_file, password)
|
||||
|
||||
sys.stdout.write("[+] Writing encryption files... {0}!\n".format(dest))
|
||||
|
||||
|
||||
@manager.command
|
||||
def decrypt_file(source):
|
||||
"""
|
||||
Utility to decrypt, Lemur will decrypt these
|
||||
files when admin enters the correct password.
|
||||
|
||||
Assumes AES-256-CBC encryption
|
||||
"""
|
||||
# cleanup extensions a bit
|
||||
if ".encrypted" in source:
|
||||
dest = ".".join(source.split(".")[:-1]) + ".decrypted"
|
||||
else:
|
||||
dest = source + ".decrypted"
|
||||
|
||||
password = prompt_pass("Please enter the encryption password")
|
||||
|
||||
with open(source, 'rb') as in_file, open(dest, 'wb') as out_file:
|
||||
decrypt(in_file, out_file, password)
|
||||
|
||||
sys.stdout.write("[+] Writing decrypted files... {0}!\n".format(dest))
|
||||
|
||||
|
||||
@manager.command
|
||||
def check_revoked():
|
||||
"""
|
||||
Function attempts to update Lemur's internal cache with revoked
|
||||
certificates. This is called periodically by Lemur. It checks both
|
||||
CRLs and OCSP to see if a certificate is revoked. If Lemur is unable
|
||||
encounters an issue with verification it marks the certificate status
|
||||
as `unknown`.
|
||||
"""
|
||||
for cert in cert_service.get_all_certs():
|
||||
if cert.chain:
|
||||
status = verify_string(cert.body, cert.chain)
|
||||
else:
|
||||
status = verify_string(cert.body, "")
|
||||
|
||||
cert.status = 'valid' if status else "invalid"
|
||||
database.update(cert)
|
||||
|
||||
|
||||
@manager.shell
|
||||
def make_shell_context():
|
||||
"""
|
||||
Creates a python REPL with several default imports
|
||||
in the context of the current_app
|
||||
|
||||
:return:
|
||||
"""
|
||||
return dict(current_app=current_app)
|
||||
|
||||
|
||||
def generate_settings():
|
||||
"""
|
||||
This command is run when ``default_path`` doesn't exist, or ``init`` is
|
||||
run and returns a string representing the default data to put into their
|
||||
settings file.
|
||||
"""
|
||||
output = CONFIG_TEMPLATE.format(
|
||||
encryption_key=base64.b64encode(os.urandom(KEY_LENGTH)),
|
||||
secret_token=base64.b64encode(os.urandom(KEY_LENGTH))
|
||||
)
|
||||
|
||||
return output
|
||||
|
||||
|
||||
class Sync(Command):
|
||||
"""
|
||||
Attempts to run several methods Certificate discovery. This is
|
||||
run on a periodic basis and updates the Lemur datastore with the
|
||||
information it discovers.
|
||||
"""
|
||||
option_list = [
|
||||
Group(
|
||||
Option('-a', '--all', action="store_true"),
|
||||
Option('-b', '--aws', action="store_true"),
|
||||
Option('-d', '--cloudca', action="store_true"),
|
||||
Option('-s', '--source', action="store_true"),
|
||||
exclusive=True, required=True
|
||||
)
|
||||
]
|
||||
|
||||
def run(self, all, aws, cloudca, source):
|
||||
sys.stdout.write("[!] Starting to sync with external sources!\n")
|
||||
|
||||
if all or aws:
|
||||
sys.stdout.write("[!] Starting to sync with AWS!\n")
|
||||
try:
|
||||
sync.aws()
|
||||
#sync_all_elbs()
|
||||
sys.stdout.write("[+] Finished syncing with AWS!\n")
|
||||
except Exception as e:
|
||||
sys.stdout.write("[-] Syncing with AWS failed!\n")
|
||||
|
||||
if all or cloudca:
|
||||
sys.stdout.write("[!] Starting to sync with CloudCA!\n")
|
||||
try:
|
||||
sync.cloudca()
|
||||
sys.stdout.write("[+] Finished syncing with CloudCA!\n")
|
||||
except Exception as e:
|
||||
sys.stdout.write("[-] Syncing with CloudCA failed!\n")
|
||||
|
||||
sys.stdout.write("[!] Starting to sync with Source Code!\n")
|
||||
|
||||
if all or source:
|
||||
try:
|
||||
sync.source()
|
||||
sys.stdout.write("[+] Finished syncing with Source Code!\n")
|
||||
except Exception as e:
|
||||
sys.stdout.write("[-] Syncing with Source Code failed!\n")
|
||||
|
||||
sys.stdout.write("[+] Finished syncing with external sources!\n")
|
||||
|
||||
|
||||
class InitializeApp(Command):
|
||||
"""
|
||||
This command will bootstrap our database with any accounts as
|
||||
specified by our config.
|
||||
|
||||
Additionally a Lemur user will be created as a default user
|
||||
and be used when certificates are discovered by Lemur.
|
||||
"""
|
||||
def run(self):
|
||||
create()
|
||||
user = user_service.get_by_username("lemur")
|
||||
|
||||
if not user:
|
||||
sys.stdout.write("We need to set Lemur's password to continue!\n")
|
||||
password1 = prompt_pass("Password")
|
||||
password2 = prompt_pass("Confirm Password")
|
||||
|
||||
if password1 != password2:
|
||||
sys.stderr.write("[!] Passwords do not match!\n")
|
||||
sys.exit(1)
|
||||
|
||||
role = role_service.get_by_name('admin')
|
||||
|
||||
if role:
|
||||
sys.stdout.write("[-] Admin role already created, skipping...!\n")
|
||||
else:
|
||||
# we create an admin role
|
||||
role = role_service.create('admin', description='this is the lemur administrator role')
|
||||
sys.stdout.write("[+] Created 'admin' role\n")
|
||||
|
||||
user_service.create("lemur", password1, 'lemur@nobody', True, None, [role])
|
||||
sys.stdout.write("[+] Added a 'lemur' user and added it to the 'admin' role!\n")
|
||||
|
||||
else:
|
||||
sys.stdout.write("[-] Default user has already been created, skipping...!\n")
|
||||
|
||||
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:
|
||||
account_service.create(account_number, label=account_name)
|
||||
sys.stdout.write("[+] Added new account {0}:{1}!\n".format(account_number, account_name))
|
||||
else:
|
||||
sys.stdout.write("[-] Account already exists, skipping...!\n")
|
||||
|
||||
sys.stdout.write("[/] Done!\n")
|
||||
|
||||
|
||||
|
||||
#def install_issuers(settings):
|
||||
# """
|
||||
# Installs new issuers that are not currently bundled with Lemur.
|
||||
#
|
||||
# :param settings:
|
||||
# :return:
|
||||
# """
|
||||
# from lemur.issuers import register
|
||||
# # entry_points={
|
||||
# # 'lemur.issuers': [
|
||||
# # 'verisign = lemur_issuers.issuers:VerisignPlugin'
|
||||
# # ],
|
||||
# # },
|
||||
# installed_apps = list(settings.INSTALLED_APPS)
|
||||
# for ep in pkg_resources.iter_entry_points('lemur.apps'):
|
||||
# try:
|
||||
# issuer = ep.load()
|
||||
# except Exception:
|
||||
# import sys
|
||||
# import traceback
|
||||
#
|
||||
# sys.stderr.write("Failed to load app %r:\n%s\n" % (ep.name, traceback.format_exc()))
|
||||
# else:
|
||||
# installed_apps.append(ep.module_name)
|
||||
# settings.INSTALLED_APPS = tuple(installed_apps)
|
||||
#
|
||||
# for ep in pkg_resources.iter_entry_points('lemur.issuers'):
|
||||
# try:
|
||||
# issuer = ep.load()
|
||||
# except Exception:
|
||||
# import sys
|
||||
# import traceback
|
||||
#
|
||||
# sys.stderr.write("Failed to load issuer %r:\n%s\n" % (ep.name, traceback.format_exc()))
|
||||
# else:
|
||||
# register(issuer)
|
||||
|
||||
|
||||
class CreateUser(Command):
|
||||
"""
|
||||
This command allows for the creation of a new user within Lemur
|
||||
"""
|
||||
option_list = (
|
||||
Option('-u', '--username', dest='username', required=True),
|
||||
Option('-e', '--email', dest='email', required=True),
|
||||
Option('-a', '--active', dest='active', default=True),
|
||||
Option('-r', '--roles', dest='roles', default=[])
|
||||
)
|
||||
|
||||
def run(self, username, email, active, roles):
|
||||
role_objs = []
|
||||
for r in roles:
|
||||
role_obj = role_service.get_by_name(r)
|
||||
if role_obj:
|
||||
role_objs.append(role_obj)
|
||||
else:
|
||||
sys.stderr.write("[!] Cannot find role {0}".format(r))
|
||||
sys.exit(1)
|
||||
|
||||
password1 = prompt_pass("Password")
|
||||
password2 = prompt_pass("Confirm Password")
|
||||
|
||||
if password1 != password2:
|
||||
sys.stderr.write("[!] Passwords do not match")
|
||||
sys.exit(1)
|
||||
|
||||
user_service.create(username, password1, email, active, None, role_objs)
|
||||
sys.stdout.write("[+] Created new user: {0}".format(username))
|
||||
|
||||
|
||||
class CreateRole(Command):
|
||||
"""
|
||||
This command allows for the creation of a new role within Lemur
|
||||
"""
|
||||
option_list = (
|
||||
Option('-n', '--name', dest='name', required=True),
|
||||
Option('-u', '--users', dest='users', default=[]),
|
||||
Option('-d', '--description', dest='description', required=True)
|
||||
)
|
||||
|
||||
def run(self, name, users, description):
|
||||
user_objs = []
|
||||
for u in users:
|
||||
user_obj = user_service.get_by_username(u)
|
||||
if user_obj:
|
||||
user_objs.append(user_obj)
|
||||
else:
|
||||
sys.stderr.write("[!] Cannot find user {0}".format(u))
|
||||
sys.exit(1)
|
||||
role_service.create(name, description=description, users=users)
|
||||
sys.stdout.write("[+] Created new role: {0}".format(name))
|
||||
|
||||
|
||||
@manager.command
|
||||
def create_config(config_path=None):
|
||||
"""
|
||||
Creates a new configuration file if one does not already exist
|
||||
"""
|
||||
if not config_path:
|
||||
config_path = DEFAULT_CONFIG_PATH
|
||||
|
||||
config_path = os.path.expanduser(config_path)
|
||||
dir = os.path.dirname(config_path)
|
||||
if not os.path.exists(dir):
|
||||
os.makedirs(dir)
|
||||
|
||||
config = generate_settings()
|
||||
with open(config_path, 'w') as f:
|
||||
f.write(config)
|
||||
|
||||
sys.stdout.write("Created a new configuration file {0}\n".format(config_path))
|
||||
|
||||
|
||||
class LemurServer(Command):
|
||||
"""
|
||||
This is the main Lemur server, it runs the flask app with gunicorn and
|
||||
uses any configuration options passed to it.
|
||||
|
||||
|
||||
You can pass all standard gunicorn flags to this command as if you were
|
||||
running gunicorn itself.
|
||||
|
||||
For example:
|
||||
|
||||
lemur start -w 4 -b 127.0.0.0:8002
|
||||
|
||||
Will start gunicorn with 4 workers bound to 127.0.0.0:8002
|
||||
"""
|
||||
description = 'Run the app within Gunicorn'
|
||||
|
||||
def get_options(self):
|
||||
settings = make_settings()
|
||||
options = (
|
||||
Option(*klass.cli, action=klass.action)
|
||||
for setting, klass in settings.iteritems() if klass.cli
|
||||
)
|
||||
|
||||
return options
|
||||
|
||||
def run(self, *args, **kwargs):
|
||||
from gunicorn.app.wsgiapp import WSGIApplication
|
||||
|
||||
app = WSGIApplication()
|
||||
app.app_uri = 'lemur:create_app(config="{0}")'.format(kwargs.get('config'))
|
||||
|
||||
return app.run()
|
||||
|
||||
|
||||
def main():
|
||||
manager.add_command("start", LemurServer())
|
||||
manager.add_command("runserver", Server(host='127.0.0.1'))
|
||||
manager.add_command("clean", Clean())
|
||||
manager.add_command("show_urls", ShowUrls())
|
||||
manager.add_command("db", MigrateCommand)
|
||||
manager.add_command("init", InitializeApp())
|
||||
manager.add_command('create_user', CreateUser())
|
||||
manager.add_command('create_role', CreateRole())
|
||||
manager.add_command("sync", Sync())
|
||||
manager.run()
|
1
lemur/migrations/README
Normal file
1
lemur/migrations/README
Normal file
@ -0,0 +1 @@
|
||||
Generic single-database configuration.
|
45
lemur/migrations/alembic.ini
Normal file
45
lemur/migrations/alembic.ini
Normal file
@ -0,0 +1,45 @@
|
||||
# A generic, single database configuration.
|
||||
|
||||
[alembic]
|
||||
# template used to generate migration files
|
||||
# file_template = %%(rev)s_%%(slug)s
|
||||
|
||||
# set to 'true' to run the environment during
|
||||
# the 'revision' command, regardless of autogenerate
|
||||
# revision_environment = false
|
||||
|
||||
|
||||
# Logging configuration
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARN
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARN
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
73
lemur/migrations/env.py
Normal file
73
lemur/migrations/env.py
Normal file
@ -0,0 +1,73 @@
|
||||
from __future__ import with_statement
|
||||
from alembic import context
|
||||
from sqlalchemy import engine_from_config, pool
|
||||
from logging.config import fileConfig
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
config = context.config
|
||||
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
fileConfig(config.config_file_name)
|
||||
|
||||
# add your model's MetaData object here
|
||||
# for 'autogenerate' support
|
||||
# from myapp import mymodel
|
||||
# target_metadata = mymodel.Base.metadata
|
||||
from flask import current_app
|
||||
config.set_main_option('sqlalchemy.url', current_app.config.get('SQLALCHEMY_DATABASE_URI'))
|
||||
target_metadata = current_app.extensions['migrate'].db.metadata
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
# can be acquired:
|
||||
# my_important_option = config.get_main_option("my_important_option")
|
||||
# ... etc.
|
||||
|
||||
|
||||
def run_migrations_offline():
|
||||
"""Run migrations in 'offline' mode.
|
||||
|
||||
This configures the context with just a URL
|
||||
and not an Engine, though an Engine is acceptable
|
||||
here as well. By skipping the Engine creation
|
||||
we don't even need a DBAPI to be available.
|
||||
|
||||
Calls to context.execute() here emit the given string to the
|
||||
script output.
|
||||
|
||||
"""
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(url=url)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def run_migrations_online():
|
||||
"""Run migrations in 'online' mode.
|
||||
|
||||
In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
engine = engine_from_config(config.get_section(config.config_ini_section),
|
||||
prefix='sqlalchemy.',
|
||||
poolclass=pool.NullPool)
|
||||
|
||||
connection = engine.connect()
|
||||
context.configure(connection=connection,
|
||||
target_metadata=target_metadata,
|
||||
**current_app.extensions['migrate'].configure_args)
|
||||
|
||||
try:
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
||||
|
22
lemur/migrations/script.py.mako
Normal file
22
lemur/migrations/script.py.mako
Normal file
@ -0,0 +1,22 @@
|
||||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = ${repr(up_revision)}
|
||||
down_revision = ${repr(down_revision)}
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
def upgrade():
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade():
|
||||
${downgrades if downgrades else "pass"}
|
30
lemur/models.py
Normal file
30
lemur/models.py
Normal file
@ -0,0 +1,30 @@
|
||||
"""
|
||||
.. module: lemur.models
|
||||
:platform: Unix
|
||||
:synopsis: This module contains all of the associative tables
|
||||
that help define the many to many relationships established in Lemur
|
||||
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
|
||||
from sqlalchemy import Column, Integer, ForeignKey
|
||||
|
||||
from lemur.database import db
|
||||
|
||||
certificate_associations = db.Table('certificate_associations',
|
||||
Column('domain_id', Integer, ForeignKey('domains.id')),
|
||||
Column('certificate_id', Integer, ForeignKey('certificates.id'))
|
||||
)
|
||||
|
||||
certificate_account_associations = db.Table('certificate_account_associations',
|
||||
Column('account_id', Integer, ForeignKey('accounts.id', ondelete='cascade')),
|
||||
Column('certificate_id', Integer, ForeignKey('certificates.id', ondelete='cascade'))
|
||||
)
|
||||
|
||||
roles_users = db.Table('roles_users',
|
||||
Column('user_id', Integer, ForeignKey('users.id')),
|
||||
Column('role_id', Integer, ForeignKey('roles.id'))
|
||||
)
|
||||
|
184
lemur/notifications.py
Normal file
184
lemur/notifications.py
Normal file
@ -0,0 +1,184 @@
|
||||
"""
|
||||
.. module: lemur.notifications
|
||||
: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 ssl
|
||||
import socket
|
||||
|
||||
import arrow
|
||||
from flask import current_app
|
||||
|
||||
from lemur import database
|
||||
from lemur.common.services.aws import ses
|
||||
from lemur.certificates.models import Certificate
|
||||
from lemur.domains.models import Domain
|
||||
|
||||
NOTIFICATION_INTERVALS = [30, 15, 5, 2]
|
||||
|
||||
|
||||
def _get_domain_certificate(name):
|
||||
"""
|
||||
Fetch the SSL certificate currently hosted at a given domain (if any) and
|
||||
compare it against our all of our know certificates to determine if a new
|
||||
SSL certificate has already been deployed
|
||||
|
||||
:param name:
|
||||
:return:
|
||||
"""
|
||||
query = database.session_query(Certificate)
|
||||
try:
|
||||
pub_key = ssl.get_server_certificate((name, 443))
|
||||
return query.filter(Certificate.body == pub_key.strip()).first()
|
||||
|
||||
except socket.gaierror as e:
|
||||
current_app.logger.info(str(e))
|
||||
|
||||
|
||||
def _find_superseded(domains):
|
||||
"""
|
||||
Here we try to fetch any domain in the certificate to see if we can resolve it
|
||||
and to try and see if it is currently serving the certificate we are
|
||||
alerting on
|
||||
|
||||
:param domains:
|
||||
:return:
|
||||
"""
|
||||
query = database.session_query(Certificate)
|
||||
ss_list = []
|
||||
for domain in domains:
|
||||
dc = _get_domain_certificate(domain.name)
|
||||
if dc:
|
||||
ss_list.append(dc)
|
||||
current_app.logger.info("Trying to resolve {0}".format(domain.name))
|
||||
|
||||
query = query.filter(Certificate.domains.any(Domain.name.in_([x.name for x in domains])))
|
||||
query = query.filter(Certificate.active == True)
|
||||
query = query.filter(Certificate.not_after >= arrow.utcnow().format('YYYY-MM-DD'))
|
||||
ss_list.extend(query.all())
|
||||
|
||||
return ss_list
|
||||
|
||||
|
||||
def send_expiration_notifications():
|
||||
"""
|
||||
This function will check for upcoming certificate expiration,
|
||||
and send out notification emails at given intervals.
|
||||
"""
|
||||
notifications = 0
|
||||
certs = _get_expiring_certs()
|
||||
|
||||
alerts = []
|
||||
for cert in certs:
|
||||
if _is_eligible_for_notifications(cert):
|
||||
data = _get_message_data(cert)
|
||||
recipients = _get_message_recipients(cert)
|
||||
alerts.append((data, recipients))
|
||||
|
||||
roll_ups = _create_roll_ups(alerts)
|
||||
|
||||
for messages, recipients in roll_ups:
|
||||
notifications += 1
|
||||
ses.send("Certificate Expiration", dict(messages=messages), 'event', recipients)
|
||||
|
||||
print notifications
|
||||
current_app.logger.info("Lemur has sent {0} certification notifications".format(notifications))
|
||||
|
||||
|
||||
def _get_message_recipients(cert):
|
||||
"""
|
||||
Determine who the recipients of the certificate expiration should be
|
||||
|
||||
:param cert:
|
||||
:return:
|
||||
"""
|
||||
recipients = []
|
||||
if current_app.config.get('SECURITY_TEAM_EMAIL'):
|
||||
recipients.extend(current_app.config.get('SECURITY_TEAM_EMAIL'))
|
||||
|
||||
recipients.append(cert.owner)
|
||||
|
||||
if cert.user:
|
||||
recipients.append(cert.user.email)
|
||||
return list(set(recipients))
|
||||
|
||||
|
||||
def _get_message_data(cert):
|
||||
"""
|
||||
Parse our the certification information needed for our notification
|
||||
|
||||
:param cert:
|
||||
:return:
|
||||
"""
|
||||
cert_dict = cert.as_dict()
|
||||
cert_dict['domains'] = [x .name for x in cert.domains]
|
||||
cert_dict['superseded'] = list(set([x.name for x in _find_superseded(cert.domains) if cert.name != x]))
|
||||
return cert_dict
|
||||
|
||||
|
||||
def _get_expiring_certs(outlook=30):
|
||||
"""
|
||||
Find all the certificates expiring within a given outlook
|
||||
|
||||
:param outlook: int days to look forward
|
||||
:return:
|
||||
"""
|
||||
now = arrow.utcnow()
|
||||
|
||||
query = database.session_query(Certificate)
|
||||
attr = Certificate.not_after
|
||||
|
||||
# get all certs expiring in the next 30 days
|
||||
to = now.replace(days=+outlook).format('YYYY-MM-DD')
|
||||
|
||||
certs = []
|
||||
for cert in query.filter(attr <= to).filter(attr >= now.format('YYYY-MM-DD')).all():
|
||||
if _is_eligible_for_notifications(cert):
|
||||
certs.append(cert)
|
||||
return certs
|
||||
|
||||
|
||||
def _is_eligible_for_notifications(cert, intervals=None):
|
||||
"""
|
||||
Determine if notifications for a given certificate should
|
||||
currently be sent
|
||||
|
||||
:param cert:
|
||||
:param intervals: list of days to alert on
|
||||
:return:
|
||||
"""
|
||||
now = arrow.utcnow()
|
||||
if cert.active:
|
||||
days = (cert.not_after - now.naive).days
|
||||
|
||||
if not intervals:
|
||||
intervals = NOTIFICATION_INTERVALS
|
||||
|
||||
if days in intervals:
|
||||
return cert
|
||||
|
||||
|
||||
def _create_roll_ups(messages):
|
||||
"""
|
||||
Take all of the messages that should be sent and provide
|
||||
a roll up to the same set if the recipients are the same
|
||||
|
||||
:param messages:
|
||||
"""
|
||||
roll_ups = []
|
||||
for message_data, recipients in messages:
|
||||
for m, r in roll_ups:
|
||||
if r == recipients:
|
||||
m.append(message_data)
|
||||
current_app.logger.info(
|
||||
"Sending email expiration alert about {0} to {1}".format(
|
||||
message_data['name'], ",".join(recipients)))
|
||||
break
|
||||
else:
|
||||
roll_ups.append(([message_data], recipients))
|
||||
return roll_ups
|
0
lemur/roles/__init__.py
Normal file
0
lemur/roles/__init__.py
Normal file
39
lemur/roles/models.py
Normal file
39
lemur/roles/models.py
Normal file
@ -0,0 +1,39 @@
|
||||
"""
|
||||
.. module: models
|
||||
:platform: unix
|
||||
:synopsis: This module contains all of the models need to create a role within Lemur
|
||||
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
|
||||
"""
|
||||
import os
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy import Column, Integer, String, Text, ForeignKey
|
||||
|
||||
from sqlalchemy_utils import EncryptedType
|
||||
|
||||
from lemur.database import db
|
||||
from lemur.models import roles_users
|
||||
|
||||
|
||||
class Role(db.Model):
|
||||
__tablename__ = 'roles'
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String(128), unique=True)
|
||||
username = Column(String(128))
|
||||
password = Column(EncryptedType(String, os.environ.get('LEMUR_ENCRYPTION_KEY')))
|
||||
description = Column(Text)
|
||||
authority_id = Column(Integer, ForeignKey('authorities.id'))
|
||||
user_id = Column(Integer, ForeignKey('users.id'))
|
||||
users = relationship("User", secondary=roles_users, passive_deletes=True, backref="role", cascade='all,delete')
|
||||
|
||||
def as_dict(self):
|
||||
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
|
||||
|
||||
def serialize(self):
|
||||
blob = self.as_dict()
|
||||
return blob
|
||||
|
125
lemur/roles/service.py
Normal file
125
lemur/roles/service.py
Normal file
@ -0,0 +1,125 @@
|
||||
"""
|
||||
.. module: service
|
||||
:platform: Unix
|
||||
:synopsis: This module contains all of the services level functions used to
|
||||
administer roles in Lemur
|
||||
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
from flask import g
|
||||
|
||||
from lemur import database
|
||||
from lemur.roles.models import Role
|
||||
from lemur.users.models import User
|
||||
|
||||
def update(role_id, name, description, users):
|
||||
"""
|
||||
Update a role
|
||||
|
||||
:param role_id:
|
||||
:param name:
|
||||
:param description:
|
||||
:param users:
|
||||
:return:
|
||||
"""
|
||||
role = get(role_id)
|
||||
role.name = name
|
||||
role.description = description
|
||||
role = database.update_list(role, 'users', User, users)
|
||||
database.update(role)
|
||||
return role
|
||||
|
||||
|
||||
def create(name, password=None, description=None, username=None, users=None):
|
||||
"""
|
||||
Create a new role
|
||||
|
||||
:param name:
|
||||
:param users:
|
||||
:param description:
|
||||
:param username:
|
||||
:param password:
|
||||
:return:
|
||||
"""
|
||||
role = Role(name=name, description=description, username=username, password=password)
|
||||
|
||||
if users:
|
||||
role = database.update_list(role, 'users', User, users)
|
||||
|
||||
return database.create(role)
|
||||
|
||||
|
||||
def get(role_id):
|
||||
"""
|
||||
Retrieve a role by ID
|
||||
|
||||
:param role_id:
|
||||
:return:
|
||||
"""
|
||||
return database.get(Role, role_id)
|
||||
|
||||
|
||||
def get_by_name(role_name):
|
||||
"""
|
||||
Retrieve a role by it's name
|
||||
|
||||
:param role_name:
|
||||
:return:
|
||||
"""
|
||||
return database.get(Role, role_name, field='name')
|
||||
|
||||
|
||||
def delete(role_id):
|
||||
"""
|
||||
Remove a role
|
||||
|
||||
:param role_id:
|
||||
:return:
|
||||
"""
|
||||
return database.delete(get(role_id))
|
||||
|
||||
|
||||
def render(args):
|
||||
"""
|
||||
Helper that filters subsets of roles depending on the parameters
|
||||
passed to the REST Api
|
||||
|
||||
:param args:
|
||||
:return:
|
||||
"""
|
||||
query = database.session_query(Role)
|
||||
sort_by = args.pop('sort_by')
|
||||
sort_dir = args.pop('sort_dir')
|
||||
page = args.pop('page')
|
||||
count = args.pop('count')
|
||||
filt = args.pop('filter')
|
||||
user_id = args.pop('user_id', None)
|
||||
authority_id = args.pop('authority_id', None)
|
||||
|
||||
if user_id:
|
||||
query = query.filter(Role.users.any(User.id == user_id))
|
||||
|
||||
if authority_id:
|
||||
query = query.filter(Role.authority_id == authority_id)
|
||||
|
||||
# we make sure that user can see the role - admins can see all
|
||||
if not g.current_user.is_admin:
|
||||
ids = []
|
||||
for role in g.current_user.roles:
|
||||
ids.append(role.id)
|
||||
query = query.filter(Role.id.in_(ids))
|
||||
|
||||
if filt:
|
||||
terms = filt.split(';')
|
||||
query = database.filter(query, Role, terms)
|
||||
|
||||
query = database.find_all(query, Role, args)
|
||||
|
||||
if sort_by and sort_dir:
|
||||
query = database.sort(query, Role, sort_by, sort_dir)
|
||||
|
||||
return database.paginate(query, page, count)
|
||||
|
445
lemur/roles/views.py
Normal file
445
lemur/roles/views.py
Normal file
@ -0,0 +1,445 @@
|
||||
"""
|
||||
.. module: lemur.roles.views
|
||||
:platform: Unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
|
||||
"""
|
||||
from flask import Blueprint
|
||||
from flask import make_response, jsonify, abort, g
|
||||
from flask.ext.restful import reqparse, fields, Api
|
||||
|
||||
from lemur.roles import service
|
||||
from lemur.auth.service import AuthenticatedResource
|
||||
from lemur.auth.permissions import ViewRoleCredentialsPermission, admin_permission
|
||||
from lemur.common.utils import marshal_items, paginated_parser
|
||||
|
||||
|
||||
mod = Blueprint('roles', __name__)
|
||||
api = Api(mod)
|
||||
|
||||
|
||||
FIELDS = {
|
||||
'name': fields.String,
|
||||
'description': fields.String,
|
||||
'id': fields.Integer,
|
||||
}
|
||||
|
||||
|
||||
class RolesList(AuthenticatedResource):
|
||||
""" Defines the 'roles' endpoint """
|
||||
def __init__(self):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(RolesList, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
def get(self):
|
||||
"""
|
||||
.. http:get:: /roles
|
||||
|
||||
The current role list
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /roles 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": 1,
|
||||
"name": "role1",
|
||||
"description": "this is role1"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "role2",
|
||||
"description": "this is role2"
|
||||
}
|
||||
]
|
||||
"total": 2
|
||||
}
|
||||
|
||||
:query sortBy: field to sort on
|
||||
:query sortDir: acs or desc
|
||||
:query page: int. default is 1
|
||||
:query filter: key value pair. format is k=v;
|
||||
:query limit: limit number. default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
"""
|
||||
parser = paginated_parser.copy()
|
||||
parser.add_argument('owner', type=str, location='args')
|
||||
parser.add_argument('id', type=str, location='args')
|
||||
|
||||
args = parser.parse_args()
|
||||
return service.render(args)
|
||||
|
||||
@admin_permission.require(http_exception=403)
|
||||
@marshal_items(FIELDS)
|
||||
def post(self):
|
||||
"""
|
||||
.. http:post:: /roles
|
||||
|
||||
Creates a new role
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /roles HTTP/1.1
|
||||
Host: example.com
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
{
|
||||
"name": "role3",
|
||||
"description": "this is role3",
|
||||
"username": null,
|
||||
"password": null,
|
||||
"users": []
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"id": 3,
|
||||
"description": "this is role3",
|
||||
"name": "role3"
|
||||
}
|
||||
|
||||
:arg name: name for new role
|
||||
:arg description: description for new role
|
||||
:arg password: password for new role
|
||||
:arg username: username for new role
|
||||
:arg users: list, of users to associate with role
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
"""
|
||||
self.reqparse.add_argument('name', type=str, location='json', required=True)
|
||||
self.reqparse.add_argument('description', type=str, location='json')
|
||||
self.reqparse.add_argument('username', type=str, location='json')
|
||||
self.reqparse.add_argument('password', type=str, location='json')
|
||||
self.reqparse.add_argument('users', type=dict, location='json')
|
||||
|
||||
args = self.reqparse.parse_args()
|
||||
return service.create(args['name'], args.get('password'), args.get('description'), args.get('username'),
|
||||
args.get('users'))
|
||||
|
||||
|
||||
class RoleViewCredentials(AuthenticatedResource):
|
||||
def __init__(self):
|
||||
super(RoleViewCredentials, self).__init__()
|
||||
|
||||
def get(self, role_id):
|
||||
"""
|
||||
.. http:get:: /roles/1/credentials
|
||||
|
||||
View a roles credentials
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /users/1 HTTP/1.1
|
||||
Host: example.com
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"username: "ausername",
|
||||
"password": "apassword"
|
||||
}
|
||||
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
"""
|
||||
permission = ViewRoleCredentialsPermission(role_id)
|
||||
if permission.can():
|
||||
role = service.get(role_id)
|
||||
response = make_response(jsonify(username=role.username, password=role.password), 200)
|
||||
response.headers['cache-control'] = 'private, max-age=0, no-cache, no-store'
|
||||
response.headers['pragma'] = 'no-cache'
|
||||
return response
|
||||
abort(403)
|
||||
|
||||
|
||||
class Roles(AuthenticatedResource):
|
||||
def __init__(self):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(Roles, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
def get(self, role_id):
|
||||
"""
|
||||
.. http:get:: /roles/1
|
||||
|
||||
Get a particular role
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /roles/1 HTTP/1.1
|
||||
Host: example.com
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"name": "role1",
|
||||
"description": "this is role1"
|
||||
}
|
||||
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
"""
|
||||
# we want to make sure that we cannot view roles that we are not members of
|
||||
if not g.current_user.is_admin:
|
||||
user_role_ids = set([r.id for r in g.current_user.roles])
|
||||
if role_id not in user_role_ids:
|
||||
return dict(message="You are not allowed to view a role which you are not a member of"), 400
|
||||
|
||||
return service.get(role_id)
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
def put(self, role_id):
|
||||
"""
|
||||
.. http:put:: /roles/1
|
||||
|
||||
Update a role
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PUT /roles/1 HTTP/1.1
|
||||
Host: example.com
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
{
|
||||
"name": "role1",
|
||||
"description": "This is a new description"
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"name": "role1",
|
||||
"description": "this is a new description"
|
||||
}
|
||||
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
"""
|
||||
permission = ViewRoleCredentialsPermission(role_id)
|
||||
if permission.can():
|
||||
self.reqparse.add_argument('name', type=str, location='json', required=True)
|
||||
self.reqparse.add_argument('description', type=str, location='json')
|
||||
self.reqparse.add_argument('users', type=list, location='json')
|
||||
args = self.reqparse.parse_args()
|
||||
return service.update(role_id, args['name'], args.get('description'), args.get('users'))
|
||||
abort(403)
|
||||
|
||||
@admin_permission.require(http_exception=403)
|
||||
def delete(self, role_id):
|
||||
"""
|
||||
.. http:delete:: /roles/1
|
||||
|
||||
Delete a role
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
DELETE /roles/1 HTTP/1.1
|
||||
Host: example.com
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"message": "ok"
|
||||
}
|
||||
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
"""
|
||||
service.delete(role_id)
|
||||
return {'message': 'ok'}
|
||||
|
||||
|
||||
class UserRolesList(AuthenticatedResource):
|
||||
""" Defines the 'roles' endpoint """
|
||||
def __init__(self):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(UserRolesList, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
def get(self, user_id):
|
||||
"""
|
||||
.. http:get:: /users/1/roles
|
||||
|
||||
List of roles for a given user
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /users/1/roles 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": 1,
|
||||
"name": "role1",
|
||||
"description": "this is role1"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "role2",
|
||||
"description": "this is role2"
|
||||
}
|
||||
]
|
||||
"total": 2
|
||||
}
|
||||
|
||||
:query sortBy: field to sort on
|
||||
:query sortDir: acs or desc
|
||||
:query page: int. default is 1
|
||||
:query filter: key value pair. format is k=v;
|
||||
:query limit: limit number. default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
"""
|
||||
parser = paginated_parser.copy()
|
||||
args = parser.parse_args()
|
||||
args['user_id'] = user_id
|
||||
return service.render(args)
|
||||
|
||||
|
||||
class AuthorityRolesList(AuthenticatedResource):
|
||||
""" Defines the 'roles' endpoint """
|
||||
def __init__(self):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(AuthorityRolesList, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
def get(self, authority_id):
|
||||
"""
|
||||
.. http:get:: /authorities/1/roles
|
||||
|
||||
List of roles for a given authority
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /authorities/1/roles 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": 1,
|
||||
"name": "role1",
|
||||
"description": "this is role1"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "role2",
|
||||
"description": "this is role2"
|
||||
}
|
||||
]
|
||||
"total": 2
|
||||
}
|
||||
|
||||
:query sortBy: field to sort on
|
||||
:query sortDir: acs or desc
|
||||
:query page: int. default is 1
|
||||
:query filter: key value pair. format is k=v;
|
||||
:query limit: limit number. default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
"""
|
||||
parser = paginated_parser.copy()
|
||||
args = parser.parse_args()
|
||||
args['authority_id'] = authority_id
|
||||
return service.render(args)
|
||||
|
||||
|
||||
api.add_resource(RolesList, '/roles', endpoint='roles')
|
||||
api.add_resource(Roles, '/roles/<int:role_id>', endpoint='role')
|
||||
api.add_resource(RoleViewCredentials, '/roles/<int:role_id>/credentials', endpoint='roleCredentials`')
|
||||
api.add_resource(AuthorityRolesList, '/authorities/<int:authority_id>/roles', endpoint='authorityRoles')
|
||||
api.add_resource(UserRolesList, '/users/<int:user_id>/roles', endpoint='userRoles')
|
1
lemur/static/app/.buildignore
Normal file
1
lemur/static/app/.buildignore
Normal file
@ -0,0 +1 @@
|
||||
*.coffee
|
157
lemur/static/app/404.html
Normal file
157
lemur/static/app/404.html
Normal file
@ -0,0 +1,157 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Page Not Found :(</title>
|
||||
<style>
|
||||
::-moz-selection {
|
||||
background: #b3d4fc;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: #b3d4fc;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
html {
|
||||
padding: 30px 10px;
|
||||
font-size: 20px;
|
||||
line-height: 1.4;
|
||||
color: #737373;
|
||||
background: #f0f0f0;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
html,
|
||||
input {
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
max-width: 500px;
|
||||
_width: 500px;
|
||||
padding: 30px 20px 50px;
|
||||
border: 1px solid #b3b3b3;
|
||||
border-radius: 4px;
|
||||
margin: 0 auto;
|
||||
box-shadow: 0 1px 10px #a7a7a7, inset 0 1px 0 #fff;
|
||||
background: #fcfcfc;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 10px;
|
||||
font-size: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 span {
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 1.5em 0 0.5em;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding: 0 0 0 40px;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 380px;
|
||||
_width: 380px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* google search */
|
||||
|
||||
#goog-fixurl ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#goog-fixurl form {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#goog-wm-qt,
|
||||
#goog-wm-sb {
|
||||
border: 1px solid #bbb;
|
||||
font-size: 16px;
|
||||
line-height: normal;
|
||||
vertical-align: top;
|
||||
color: #444;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
#goog-wm-qt {
|
||||
width: 220px;
|
||||
height: 20px;
|
||||
padding: 5px;
|
||||
margin: 5px 10px 0 0;
|
||||
box-shadow: inset 0 1px 1px #ccc;
|
||||
}
|
||||
|
||||
#goog-wm-sb {
|
||||
display: inline-block;
|
||||
height: 32px;
|
||||
padding: 0 10px;
|
||||
margin: 5px 0 0;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
background-color: #f5f5f5;
|
||||
background-image: -webkit-linear-gradient(rgba(255,255,255,0), #f1f1f1);
|
||||
background-image: -moz-linear-gradient(rgba(255,255,255,0), #f1f1f1);
|
||||
background-image: -ms-linear-gradient(rgba(255,255,255,0), #f1f1f1);
|
||||
background-image: -o-linear-gradient(rgba(255,255,255,0), #f1f1f1);
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
*overflow: visible;
|
||||
*display: inline;
|
||||
*zoom: 1;
|
||||
}
|
||||
|
||||
#goog-wm-sb:hover,
|
||||
#goog-wm-sb:focus {
|
||||
border-color: #aaa;
|
||||
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
|
||||
#goog-wm-qt:hover,
|
||||
#goog-wm-qt:focus {
|
||||
border-color: #105cb6;
|
||||
outline: 0;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
input::-moz-focus-inner {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Not found <span>:(</span></h1>
|
||||
<p>Sorry, but the page you were trying to view does not exist.</p>
|
||||
<p>It looks like this was the result of either:</p>
|
||||
<ul>
|
||||
<li>a mistyped address</li>
|
||||
<li>an out-of-date link</li>
|
||||
</ul>
|
||||
<script>
|
||||
var GOOG_FIXURL_LANG = (navigator.language || '').slice(0,2),GOOG_FIXURL_SITE = location.host;
|
||||
</script>
|
||||
<script src="//linkhelp.clients.google.com/tbproxy/lh/wm/fixurl.js"></script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
27
lemur/static/app/angular/accounts/account/account.js
vendored
Normal file
27
lemur/static/app/angular/accounts/account/account.js
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
'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;
|
||||
});
|
45
lemur/static/app/angular/accounts/account/account.tpl.html
Normal file
45
lemur/static/app/angular/accounts/account/account.tpl.html
Normal file
@ -0,0 +1,45 @@
|
||||
<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>
|
||||
|
53
lemur/static/app/angular/accounts/services.js
vendored
Normal file
53
lemur/static/app/angular/accounts/services.js
vendored
Normal file
@ -0,0 +1,53 @@
|
||||
'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;
|
||||
});
|
52
lemur/static/app/angular/accounts/view/view.js
vendored
Normal file
52
lemur/static/app/angular/accounts/view/view.js
vendored
Normal file
@ -0,0 +1,52 @@
|
||||
'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;
|
||||
};
|
||||
|
||||
});
|
44
lemur/static/app/angular/accounts/view/view.tpl.html
Normal file
44
lemur/static/app/angular/accounts/view/view.tpl.html
Normal file
@ -0,0 +1,44 @@
|
||||
<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>
|
113
lemur/static/app/angular/app.js
vendored
Normal file
113
lemur/static/app/angular/app.js
vendored
Normal file
@ -0,0 +1,113 @@
|
||||
'use strict';
|
||||
|
||||
var lemur = angular
|
||||
.module('lemur', [
|
||||
'ngRoute',
|
||||
'ngTable',
|
||||
'ngAnimate',
|
||||
'chart.js',
|
||||
'restangular',
|
||||
'angular-loading-bar',
|
||||
'ui.bootstrap',
|
||||
'angular-spinkit',
|
||||
'toaster',
|
||||
'uiSwitch',
|
||||
'mgo-angular-wizard',
|
||||
'satellizer'
|
||||
])
|
||||
.config(function ($routeProvider, $authProvider) {
|
||||
$routeProvider
|
||||
.when('/', {
|
||||
templateUrl: 'angular/welcome/welcome.html'
|
||||
})
|
||||
.otherwise({
|
||||
redirectTo: '/'
|
||||
});
|
||||
|
||||
$authProvider.oauth2({
|
||||
name: 'ping',
|
||||
url: 'http://localhost:5000/api/1/auth/ping',
|
||||
redirectUri: 'http://localhost:3000/',
|
||||
clientId: 'client-id',
|
||||
responseType: 'code',
|
||||
scope: ['openid', 'email', 'profile', 'address'],
|
||||
scopeDelimiter: ' ',
|
||||
authorizationEndpoint: 'https://example.com/as/authorization.oauth2',
|
||||
requiredUrlParams: ['scope']
|
||||
});
|
||||
});
|
||||
|
||||
lemur.service('MomentService', function () {
|
||||
this.diffMoment = function (start, end) {
|
||||
if (end !== 'None') {
|
||||
return moment(end, 'YYYY-MM-DD HH:mm Z').diff(moment(start, 'YYYY-MM-DD HH:mm Z'), 'minutes') + ' minutes';
|
||||
}
|
||||
return 'Unknown';
|
||||
};
|
||||
this.createMoment = function (date) {
|
||||
if (date !== 'None') {
|
||||
return moment(date, 'YYYY-MM-DD HH:mm Z').fromNow();
|
||||
}
|
||||
return 'Unknown';
|
||||
};
|
||||
});
|
||||
|
||||
lemur.controller('datePickerController', function ($scope, $timeout){
|
||||
$scope.open = function() {
|
||||
$timeout(function() {
|
||||
$scope.opened = true;
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
lemur.factory('LemurRestangular', function (Restangular, $location, $auth) {
|
||||
return Restangular.withConfig(function (RestangularConfigurer) {
|
||||
RestangularConfigurer.setBaseUrl('http://127.0.0.1:5000/api/1');
|
||||
RestangularConfigurer.setDefaultHttpFields({withCredentials: true});
|
||||
|
||||
RestangularConfigurer.addResponseInterceptor(function (data, operation, what, url, response, deferred) {
|
||||
var extractedData;
|
||||
|
||||
// .. to look for getList operations
|
||||
if (operation === "getList") {
|
||||
// .. and handle the data and meta data
|
||||
extractedData = data.items;
|
||||
extractedData.total = data.total;
|
||||
} else {
|
||||
extractedData = data;
|
||||
}
|
||||
return extractedData;
|
||||
});
|
||||
|
||||
RestangularConfigurer.addFullRequestInterceptor(function (element, operation, route, url, headers, params, httpConfig) {
|
||||
// We want to make sure the user is auth'd before any requests
|
||||
if (!$auth.isAuthenticated()) {
|
||||
$location.path('/login');
|
||||
return false;
|
||||
}
|
||||
|
||||
var regExp = /\[([^)]+)\]/;
|
||||
|
||||
var s = 'sorting';
|
||||
var f = 'filter';
|
||||
var newParams = {};
|
||||
for (var item in params) {
|
||||
if (item.indexOf(s) > -1) {
|
||||
newParams.sortBy = regExp.exec(item)[1];
|
||||
newParams.sortDir = params[item];
|
||||
} else if (item.indexOf(f) > -1) {
|
||||
var key = regExp.exec(item)[1];
|
||||
newParams['filter'] = key + ";" + params[item];
|
||||
} else {
|
||||
newParams[item] = params[item];
|
||||
}
|
||||
}
|
||||
return { params: newParams };
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
lemur.run(['$templateCache', function ($templateCache) {
|
||||
$templateCache.put('ng-table/pager.html', '<div class="ng-cloak ng-table-pager"> <div ng-if="params.settings().counts.length" class="ng-table-counts btn-group pull-left"> <button ng-repeat="count in params.settings().counts" type="button" ng-class="{\'active\':params.count()==count}" ng-click="params.count(count)" class="btn btn-default"> <span ng-bind="count"></span> </button></div><div class="pull-right"><ul style="margin: 0; padding: 0;" class="pagination ng-table-pagination"> <li ng-class="{\'disabled\': !page.active}" ng-repeat="page in pages" ng-switch="page.type"> <a ng-switch-when="prev" ng-click="params.page(page.number)" href="">«</a> <a ng-switch-when="first" ng-click="params.page(page.number)" href=""><span ng-bind="page.number"></span></a> <a ng-switch-when="page" ng-click="params.page(page.number)" href=""><span ng-bind="page.number"></span></a> <a ng-switch-when="more" ng-click="params.page(page.number)" href="">…</a> <a ng-switch-when="last" ng-click="params.page(page.number)" href=""><span ng-bind="page.number"></span></a> <a ng-switch-when="next" ng-click="params.page(page.number)" href="">»</a> </li> </ul> </div></div>');
|
||||
}]);
|
28
lemur/static/app/angular/authentication/login/login.js
vendored
Normal file
28
lemur/static/app/angular/authentication/login/login.js
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('lemur')
|
||||
.config(function config($routeProvider) {
|
||||
$routeProvider.when('/login', {
|
||||
templateUrl: '/angular/authentication/login/login.tpl.html',
|
||||
controller: 'LoginController'
|
||||
});
|
||||
})
|
||||
.controller('LoginController', function ($rootScope, $scope, AuthenticationService, UserService) {
|
||||
$scope.login = AuthenticationService.login;
|
||||
$scope.authenticate = AuthenticationService.authenticate;
|
||||
$scope.logout = AuthenticationService.logout;
|
||||
|
||||
UserService.getCurrentUser().then(function (user) {
|
||||
$scope.currentUser = user;
|
||||
});
|
||||
|
||||
$rootScope.$on('user:login', function () {
|
||||
UserService.getCurrentUser().then(function (user) {
|
||||
$scope.currentUser = user;
|
||||
});
|
||||
});
|
||||
|
||||
$rootScope.$on('user:logout', function () {
|
||||
$scope.currentUser = null;
|
||||
});
|
||||
});
|
27
lemur/static/app/angular/authentication/login/login.tpl.html
Normal file
27
lemur/static/app/angular/authentication/login/login.tpl.html
Normal file
@ -0,0 +1,27 @@
|
||||
<h2 class="featurette-heading">Login <span class="text-muted"><small>None shall pass</small></span></h2>
|
||||
<div class="row">
|
||||
<div class="login">
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-sm-12 col-md-12">
|
||||
<button class="btn btn-block btn-default" ng-click="authenticate('ping')">
|
||||
Login with Meechum
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="login-or">
|
||||
<hr class="hr-or">
|
||||
<span class="span-or">or</span>
|
||||
</div>
|
||||
<form role="form" _lpchecked="1">
|
||||
<div class="form-group">
|
||||
<input type="text" ng-model="username" placeholder="Username" class="form-control"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="password" ng-model="password" placeholder="Password" class="form-control"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button ng-click="login(username, password)" class="btn btn-block btn-success">Login</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
12
lemur/static/app/angular/authentication/logout/logout.js
vendored
Normal file
12
lemur/static/app/angular/authentication/logout/logout.js
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('lemur')
|
||||
.config(function config($routeProvider) {
|
||||
$routeProvider.when('/logout', {
|
||||
controller: 'LogoutCtrl'
|
||||
});
|
||||
})
|
||||
.controller('LogoutCtrl', function ($scope, $location, lemurRestangular, userService) {
|
||||
userService.logout();
|
||||
$location.path('/');
|
||||
});
|
@ -0,0 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head lang="en">
|
||||
<meta charset="UTF-8">
|
||||
<title></title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
</body>
|
||||
</html>
|
62
lemur/static/app/angular/authentication/services.js
vendored
Normal file
62
lemur/static/app/angular/authentication/services.js
vendored
Normal file
@ -0,0 +1,62 @@
|
||||
'use strict';
|
||||
angular.module('lemur')
|
||||
.service('AuthenticationApi', function (LemurRestangular) {
|
||||
return LemurRestangular.all('auth');
|
||||
})
|
||||
.service('AuthenticationService', function ($location, $rootScope, AuthenticationApi, UserService, toaster, $auth) {
|
||||
var AuthenticationService = this;
|
||||
|
||||
AuthenticationService.login = function (username, password) {
|
||||
AuthenticationApi.customPOST({'username': username, 'password': password}, 'login')
|
||||
.then(
|
||||
function (user) {
|
||||
$auth.setToken(user.token, true);
|
||||
$rootScope.$emit('user:login');
|
||||
$location.url('/certificates');
|
||||
},
|
||||
function (response) {
|
||||
toaster.pop({
|
||||
type: 'error',
|
||||
title: 'Whoa there',
|
||||
body: response.data.message,
|
||||
showCloseButton: true
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
AuthenticationService.authenticate = function (provider) {
|
||||
$auth.authenticate(provider)
|
||||
.then(
|
||||
function (user) {
|
||||
UserService.getCurrentUser();
|
||||
$rootScope.$emit('user:login');
|
||||
$location.url('/certificates');
|
||||
},
|
||||
function (response) {
|
||||
toaster.pop({
|
||||
type: 'error',
|
||||
title: 'Something went wrong',
|
||||
body: response.data.message
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
AuthenticationService.logout = function () {
|
||||
if (!$auth.isAuthenticated()) {
|
||||
return;
|
||||
}
|
||||
$auth.logout()
|
||||
.then(function() {
|
||||
$rootScope.$emit('user:logout');
|
||||
toaster.pop({
|
||||
type: 'success',
|
||||
title: 'Good job!',
|
||||
body: 'You have been successfully logged out.'
|
||||
});
|
||||
$location.path('/');
|
||||
})
|
||||
};
|
||||
|
||||
});
|
18
lemur/static/app/angular/authentication/unlock/unlock.js
vendored
Normal file
18
lemur/static/app/angular/authentication/unlock/unlock.js
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('lemur')
|
||||
.config(function config($routeProvider) {
|
||||
$routeProvider.when('/unlock', {
|
||||
templateUrl: '/angular/authentication/unlock/unlock.tpl.html',
|
||||
controller: 'UnlockCtrl'
|
||||
});
|
||||
})
|
||||
.controller('UnlockCtrl', function ($scope, $location, lemurRestangular, messageService) {
|
||||
$scope.unlock = function () {
|
||||
lemurRestangular.one('unlock').customPOST({'password': $scope.password})
|
||||
.then(function (data) {
|
||||
messageService.addMessage(data);
|
||||
$location.path('/dashboard');
|
||||
});
|
||||
};
|
||||
});
|
@ -0,0 +1,16 @@
|
||||
<h2 class="featurette-heading">Unlock <span class="text-muted"><small>Assume 9 is twice 5; how will you write 6 times 5 in the same system of notation?</small></span></h2>
|
||||
<form class="form-horizontal" _lpchecked="1">
|
||||
<fieldset class="col-lg-offset-4">
|
||||
<div class="form-group">
|
||||
<div class="col-lg-4">
|
||||
<input type="password" ng-model="password" placeholder="Password" class="form-control"/>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="featurette-divider">
|
||||
<div class="form-group">
|
||||
<div class="col-lg-4">
|
||||
<button ng-click="unlock()" class="btn btn-success">Unlock</button>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
55
lemur/static/app/angular/authorities/authority/authority.js
vendored
Normal file
55
lemur/static/app/angular/authorities/authority/authority.js
vendored
Normal file
@ -0,0 +1,55 @@
|
||||
'use strict';
|
||||
|
||||
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){
|
||||
AuthorityApi.get($routeParams.id).then(function (authority) {
|
||||
AuthorityService.getRoles(authority);
|
||||
$scope.authority = authority;
|
||||
});
|
||||
|
||||
$scope.authorityService = AuthorityService;
|
||||
$scope.save = AuthorityService.update;
|
||||
$scope.roleService = RoleService;
|
||||
})
|
||||
|
||||
.controller('AuthorityCreateController', function ($scope, $modal, AuthorityService, LemurRestangular, RoleService) {
|
||||
$scope.authority = LemurRestangular.restangularizeElement(null, {}, 'authorities');
|
||||
|
||||
$scope.save = function (authority) {
|
||||
var loadingModal = $modal.open({backdrop: 'static', template: '<wave-spinner></wave-spinner>', windowTemplateUrl: 'angular/loadingModal.html', size: 'large'});
|
||||
return AuthorityService.create(authority).then(function (response) {
|
||||
loadingModal.close();
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
$scope.roleService = RoleService;
|
||||
|
||||
$scope.authorityService = AuthorityService;
|
||||
|
||||
$scope.open = function($event) {
|
||||
$event.preventDefault();
|
||||
$event.stopPropagation();
|
||||
|
||||
$scope.opened1 = true;
|
||||
};
|
||||
|
||||
$scope.open2 = function($event) {
|
||||
$event.preventDefault();
|
||||
$event.stopPropagation();
|
||||
|
||||
$scope.opened2 = true;
|
||||
};
|
||||
});
|
@ -0,0 +1,44 @@
|
||||
<h2 class="featurette-heading">Edit</span> Authority <span class="text-muted"><small>Chain of command
|
||||
</small></span></h2>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<a href="#/authorities" 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">
|
||||
<label class="control-label col-sm-2">
|
||||
Roles
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<div class="input-group">
|
||||
<input type="text" ng-model="authority.selectedRole" placeholder="Role Name"
|
||||
typeahead="role.name for role in roleService.findRoleByName($viewValue)" typeahead-loading="loadingRoles"
|
||||
class="form-control input-md" typeahead-on-select="authority.attachRole($item)" typeahead-min-wait="50"
|
||||
tooltip="Roles control which authorities a user can issue certificates from"
|
||||
tooltip-trigger="focus" tooltip-placement="top">
|
||||
<span class="input-group-btn">
|
||||
<button ng-model="roles.show" class="btn btn-md btn-default" btn-checkbox btn-checkbox-true="1" btn-checkbox-false="0">
|
||||
<span class="badge">{{ authority.roles.length || 0 }}</span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<table ng-show="authority.roles" class="table">
|
||||
<tr ng-repeat="role in authority.roles track by $index">
|
||||
<td><a class="btn btn-sm btn-info" href="#/roles/{{ role.id }}/edit">{{ role.name }}</a></td>
|
||||
<td><span class="text-muted">{{ role.description }}</span></td>
|
||||
<td>
|
||||
<button type="button" ng-click="authority.removeRole($index)" class="btn btn-danger btn-sm pull-right">Remove</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="panel-footer">
|
||||
<button ng-click="save(authority)" class="btn btn-success pull-right">Save</button>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,17 @@
|
||||
<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>
|
||||
<wizard on-finish="save(authority)" template="angular/wizard.html">
|
||||
<wz-step title="Tracking" canexit="exitTracking">
|
||||
<ng-include src="'angular/authorities/authority/tracking.tpl.html'"></ng-include>
|
||||
</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">
|
||||
<ng-include src="'angular/authorities/authority/options.tpl.html'"></ng-include>
|
||||
</wz-step>
|
||||
<wz-step title="Extensions" canenter="exitOptions" canexit="exitExtensions">
|
||||
<ng-include src="'angular/authorities/authority/extensions.tpl.html'"></ng-include>
|
||||
</wz-step>
|
||||
</wizard>
|
||||
</div>
|
@ -0,0 +1,55 @@
|
||||
<form name="dnForm" novalidate>
|
||||
<div class="form-horizontal">
|
||||
<div class="form-group"
|
||||
ng-class="{'has-error': dnForm.country.$invalid, 'has-success': !dnForm.country.$invalid&&dnForm.country.$dirty}">
|
||||
<label class="control-label col-sm-2">
|
||||
Country
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input name="country" ng-model="authority.caDN.country" placeholder="Country" class="form-control" ng-init="authority.caDN.country = 'US'" required/>
|
||||
<p ng-show="dnForm.country.$invalid && !dnForm.country.$pristine" class="help-block">You must enter a country</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group"
|
||||
ng-class="{'has-error': dnForm.state.$invalid, 'has-success': !dnForm.$invalid&&dnForm.state.$dirty}">
|
||||
<label class="control-label col-sm-2">
|
||||
State
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input name="state" ng-model="authority.caDN.state" placeholder="State" class="form-control" ng-init="authority.caDN.state = 'CA'" required/>
|
||||
<p ng-show="dnForm.state.$invalid && !dnForm.state.$pristine" class="help-block">You must enter a state</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group"
|
||||
ng-class="{'has-error': dnForm.location.$invalid, 'has-success': !dnForm.$invalid&&dnForm.location.$dirty}">
|
||||
<label class="control-label col-sm-2">
|
||||
Location
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input name="location" ng-model="authority.caDN.location" placeholder="Location" class="form-control" ng-init="authority.caDN.location = 'Los Gatos'"required/>
|
||||
<p ng-show="dnForm.location.$invalid && !dnForm.location.$pristine" class="help-block">You must enter a location</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group"
|
||||
ng-class="{'has-error': dnForm.organization.$invalid, 'has-success': !dnForm.$invalid&&dnForm.organization.$dirty}">
|
||||
<label class="control-label col-sm-2">
|
||||
Organization
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input name="organization" ng-model="authority.caDN.organization" placeholder="Organization" class="form-control" ng-init="authority.caDN.organization = 'Netflix'" required/>
|
||||
<p ng-show="dnForm.organization.$invalid && !dnForm.organization.$pristine" class="help-block">You must enter a organization</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group"
|
||||
ng-class="{'has-error': dnForm.organizationalUnit.$invalid, 'has-success': !dnForm.$invalid&&dnForm.organizationalUnit.$dirty}">
|
||||
<label class="control-label col-sm-2">
|
||||
Organizational Unit
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input name="organizationalUnit" ng-model="authority.caDN.organizationalUnit" placeholder="Organizational Unit" class="form-control" ng-init="authority.caDN.organizationalUnit = 'Operations'"required/>
|
||||
<p ng-show="dnForm.organization.$invalid && !dnForm.organizationalUnit.$pristine" class="help-block">You must enter a organizational unit</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -0,0 +1,219 @@
|
||||
<div class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-2">
|
||||
Subject Alternate Names
|
||||
</label>
|
||||
<div class="col-sm-3">
|
||||
<select class="form-control" ng-model="authority.subAltType" ng-init="null" ng-options="item for item in ['DNSName', 'IPAddress', 'uniformResourceIdentifier', 'directoryName','rfc822Name', 'registeredID', 'otherName', 'x400Address', 'EDIPartyName']"></select>
|
||||
</div>
|
||||
<div class="col-sm-5">
|
||||
<div class="input-group">
|
||||
<input tooltip-trigger="focus" tooltip-placement="top" tooltip="String or Base64-encoded DER ASN.1 structure for the value" class="form-control" name="value" ng-model="authority.subAltValue" placeholder="Value" class="form-control" required/>
|
||||
<span class="input-group-btn">
|
||||
<button ng-click="authority.attachSubAltName()" class="btn btn-info">Add</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-8 col-sm-offset-2">
|
||||
<table class="table">
|
||||
<tr ng-repeat="alt in authority.extensions.subAltNames.names track by $index">
|
||||
<td>{{ alt.nameType }}</td>
|
||||
<td>{{ alt.value }}</td>
|
||||
<td>
|
||||
<button type="button" ng-click="authority.removeSubAltName($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">
|
||||
Key Usage
|
||||
</label>
|
||||
<div class="col-sm-3">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="authority.extensions.keyUsage.useDigitalSignature">Digital Signature
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="authority.extensions.keyUsage.useNonRepudiation">Non Repudiation
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="authority.extensions.keyUsage.useKeyEncipherment">Key Encipherment
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="authority.extensions.keyUsage.useDataEncipherment">Data Encipherment
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="authority.extensions.keyUsage.useKeyAgreement">Key Agreement
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="authority.extensions.keyUsage.useKeyCertSign">Key Certificate Signature
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="authority.extensions.keyUsage.useCRLSign">CRL Sign
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="authority.extensions.keyUsage.useEncipherOnly">Encipher Only
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="authority.extensions.keyUsage.useDecipherOnly">Decipher Only
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-2">
|
||||
Extended Key Usage
|
||||
</label>
|
||||
<div class="col-sm-3">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="authority.extensions.extendedKeyUsage.useServerAuthentication">Server Authentication
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="authority.extensions.extendedKeyUsage.useEmail">Email
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="authority.extensions.extendedKeyUsage.useTimestamping">Timestamping
|
||||
</label>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="authority.extensions.extendedKeyUsage.useEapOverLAN">EAP Over LAN
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="authority.extensions.extendedKeyUsage.useEapOverPPP">EAP Over PPP
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="authority.extensions.extendedKeyUsage.useSmartCardLogon">Smartcard Logon
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="authority.extensions.extendedKeyUsage.useOCSPSigning">OCSP Signing
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-2">
|
||||
Authority Key Identifier
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<div class="checkbox">
|
||||
<label tooltip-trigger="mouseenter" tooltip-placement="top" tooltip="Put Issuer's keyIdentifier in this extension" >
|
||||
<input type="checkbox" ng-model="authority.extensions.authorityKeyIdentifier.useKeyIdentifier">Key Identifier
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label tooltip-trigger="mouseenter" tooltip-placement="top" tooltip="Put Issuer's Name and Serial number" >
|
||||
<input type="checkbox" ng-model="authority.extensions.authorityKeyIdentifier.useAuthorityCert">Authority Certificate
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-2">
|
||||
Authority Information Access
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<div class="checkbox">
|
||||
<label tooltip-trigger="mouseenter" tooltip-placement="top" tooltip="Ask CA to include/not include AIA extension" >
|
||||
<input type="checkbox" ng-model="authority.extensions.authorityInfoAccess.includeAIA">Include AIA
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-2">
|
||||
Subject Key Identifier
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<div class="checkbox">
|
||||
<label tooltip-trigger="mouseenter" tooltip-placement="top" tooltip="Ask CA to include/not include Subject Key Identifier" >
|
||||
<input type="checkbox" ng-model="authority.extensions.subjectKeyIdentifier.includeSKI">Include SKI
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-2">
|
||||
cRL Distribution Points
|
||||
</label>
|
||||
<div class="col-sm-8">
|
||||
<select class="form-control" ng-model="authority.extensions.cRLDistributionPoints.includeCRLDP" ng-options="item for item in ['yes', 'no', 'default']"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-2">
|
||||
Custom
|
||||
</label>
|
||||
<div class="col-sm-2">
|
||||
<input tooltip-trigger="focus" tooltip-placement="top" tooltip="OID for the custom extension e.g. 1.12.123.12.10" class="form-control" name="oid" ng-model="authority.customOid" placeholder="Oid" class="form-control" required/>
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<select tooltip-trigger="focus" tooltip-placement="top" tooltip="Encoding for value" class="form-control col-sm-2" ng-model="authority.customEncoding" ng-options="item for item in ['b64asn1', 'string', 'ia5string']"></select>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="input-group">
|
||||
<input tooltip-trigger="focus" tooltip-placement="top" tooltip="String or Base64-encoded DER ASN.1 structure for the value" class="form-control" name="value" ng-model="authority.customValue" placeholder="Value" class="form-control" required/>
|
||||
<span class="input-group-btn">
|
||||
<button ng-click="authority.attachCustom()" class="btn btn-info">Add</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="authority.extensions.custom.isCritical">Critical
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-8 col-sm-offset-2">
|
||||
<table class="table">
|
||||
<tr ng-repeat="custom in authority.extensions.custom track by $index">
|
||||
<td>{{ custom.oid }}</td>
|
||||
<td>{{ custom.encoding }}</td>
|
||||
<td>{{ custom.value }}</td>
|
||||
<td>{{ custom.isCritical}}</td>
|
||||
<td>
|
||||
<button type="button" ng-click="authority.removeCustom($index)" class="btn btn-danger btn-sm pull-right">Remove</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,78 @@
|
||||
<div class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-2">
|
||||
Type
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control" ng-model="authority.caType" ng-options="option for option in ['root', 'subca']" ng-init="authority.caType = 'root'"required></select>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-show="authority.caType == 'subca'" class="form-group">
|
||||
<label class="control-label col-sm-2">
|
||||
Parent Authority
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" ng-model="authority.caParent" placeholder="Parent Authority Name"
|
||||
typeahead="authority.name for authority in authorityService.findAuthorityByName($viewValue)" typeahead-loading="loadingAuthorities"
|
||||
class="form-control input-md" typeahead-min-wait="50"
|
||||
tooltip="When you specifiy a subordinate certificate authority you must specific the parent authority"
|
||||
tooltip-trigger="focus" tooltip-placement="top">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-2">
|
||||
Signing Algorithm
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control" ng-model="authority.caSigningAlgo" ng-options="option for option in ['sha1WithRSA', 'sha256WithRSA']" ng-init="authority.caSigningAlgo = 'sha256WithRSA'"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-2">
|
||||
Sensitivity
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control" ng-model="authority.caSensitivity" ng-options="option for option in ['medium', 'high']" ng-init="authority.caSensitivity = 'medium'"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-2">
|
||||
Key Type
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control" ng-model="authority.keyType" ng-options="option for option in ['RSA2048', 'RSA4096']" ng-init="authority.keyType = 'RSA2048'"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-show="authority.caSensitivity == 'high'" class="form-group">
|
||||
<label class="control-label col-sm-2">
|
||||
Key Name
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" ng-model="authority.keyName" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-2">
|
||||
Serial Number
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="number" name="serialNumber" ng-model="authority.caSerialNumber" placeholder="Serial Number" class="form-control"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-2">
|
||||
First Serial Number
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="number" name="firstSerialNumber" ng-model="authority.caFirstSerial" placeholder="First Serial Number" class="form-control" ng-init="1000" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-2">
|
||||
Plugin Name
|
||||
</label>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,28 @@
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-2">
|
||||
Roles
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<div class="input-group">
|
||||
<input type="text" ng-model="authority.selectedRole" placeholder="Role Name"
|
||||
typeahead="role.name for role in roleService.findRoleByName($viewValue)" typeahead-loading="loadingAccounts"
|
||||
class="form-control input-md" typeahead-on-select="authority.attachRole($item)" typeahead-min-wait="50"
|
||||
tooltip="These are the User roles you wish to associated with your authority"
|
||||
tooltip-trigger="focus" tooltip-placement="top">
|
||||
<span class="input-group-btn">
|
||||
<button ng-model="roles.show" class="btn btn-md btn-default" btn-checkbox btn-checkbox-true="1" btn-checkbox-false="0">
|
||||
<span class="badge">{{ authority.roles.length || 0 }}</span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<table ng-show="roles.show" class="table">
|
||||
<tr ng-repeat="role in authority.roles track by $index">
|
||||
<td><a class="btn btn-sm btn-info" href="#/accounts/{{ account.id }}/certificates">{{ role.name }}</a></td>
|
||||
<td><span class="text-muted">{{ role.description }}</span></td>
|
||||
<td>
|
||||
<button type="button" ng-click="authority.removeRole($index)" class="btn btn-danger btn-sm pull-right">Remove</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,3 @@
|
||||
<a tabindex="-1">
|
||||
{{ match.model.name }} - <span class="text-muted">{{match.model.description }}</span>
|
||||
</a>
|
@ -0,0 +1,71 @@
|
||||
<form name="trackingForm" novalidate>
|
||||
<div class="form-horizontal">
|
||||
<div class="form-group"
|
||||
ng-class="{'has-error': trackingForm.caName.$invalid, 'has-success': !trackingForm.caName.$invalid&&trackingForm.caName.$dirty}">
|
||||
<label class="control-label col-sm-2">
|
||||
Name
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input name="caName" ng-model="authority.caName" placeholder="Name" tooltip="This will be the name of your authority, it is the name you will reference when creating new certificates" class="form-control" ng-pattern="/^[A-Za-z0-9_-]+$/" required/>
|
||||
<p ng-show="trackingForm.caName.$invalid && !trackingForm.caName.$pristine" class="help-block">You must enter a valid authority name, spaces are not allowed</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group"
|
||||
ng-class="{'has-error': trackingForm.ownerEmail.$invalid, 'has-success': !trackingForm.$invalid&&trackingForm.ownerEmail.$dirty}">
|
||||
<label class="control-label col-sm-2">
|
||||
Owner
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="email" name="ownerEmail" ng-model="authority.ownerEmail" placeholder="TeamDL@netflix.com" tooltip="This is the authorities team distribution list or the main point of contact for this authority" class="form-control" required/>
|
||||
<p ng-show="trackingForm.ownerEmail.$invalid && !trackingForm.ownerEmail.$pristine" class="help-block">You must enter an Certificate Authority owner</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group"
|
||||
ng-class="{'has-error': trackingForm.caDescription.$invalid, 'has-success': !trackingForm.$invalid&&trackingForm.caDescription.$dirty}">
|
||||
<label class="control-label col-sm-2">
|
||||
Description
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea name="caDescription" ng-model="authority.caDescription" placeholder="Something elegant" class="form-control" ng-maxlength="250" ng-pattern="/^[\w\-\s]+$/" required></textarea>
|
||||
<p ng-show="trackingForm.caDescription.$invalid && !trackingForm.caDescription.$pristine" class="help-block">You must give a short description about this authority will be used for, it should contain only alphanumeric characters</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group"
|
||||
ng-class="{'has-error': trackingForm.commonName.$invalid, 'has-success': !trackingForm.$invalid&&trackingForm.commanName.$dirty}">
|
||||
<label class="control-label col-sm-2">
|
||||
Common Name
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-2">
|
||||
Validity Range
|
||||
</label>
|
||||
<div class="col-sm-4">
|
||||
<div>
|
||||
<div class="input-group">
|
||||
<input tooltip="Starting Date" class="form-control" datepicker-popup="yyyy/MM/dd" is-open="opened1" ng-model="authority.validityStart" />
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default" ng-click="open($event)"><i class="glyphicon glyphicon-calendar"></i></button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-center col-sm-2"><label><span class="glyphicon glyphicon-resize-horizontal"></span></label></span>
|
||||
<div class="col-sm-4">
|
||||
<div>
|
||||
<div class="input-group">
|
||||
<input tooltip="Ending Date" class="form-control" datepicker-popup="yyyy/MM/dd" is-open="opened2" ng-model="authority.validityEnd" />
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default" ng-click="open2($event)"><i class="glyphicon glyphicon-calendar"></i></button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
130
lemur/static/app/angular/authorities/services.js
vendored
Normal file
130
lemur/static/app/angular/authorities/services.js
vendored
Normal file
@ -0,0 +1,130 @@
|
||||
'use strict';
|
||||
angular.module('lemur')
|
||||
.service('AuthorityApi', function (LemurRestangular) {
|
||||
LemurRestangular.extendModel('authorities', function (obj) {
|
||||
return angular.extend(obj, {
|
||||
attachRole: function (role) {
|
||||
this.selectedRole = null;
|
||||
if (this.roles === undefined) {
|
||||
this.roles = [];
|
||||
}
|
||||
this.roles.push(role);
|
||||
},
|
||||
removeRole: function (index) {
|
||||
this.roles.splice(index, 1);
|
||||
},
|
||||
attachSubAltName: function () {
|
||||
if (this.extensions === undefined || this.extensions.subAltNames === undefined) {
|
||||
this.extensions = {'subAltNames': {'names': []}};
|
||||
}
|
||||
|
||||
if (angular.isString(this.subAltType) && angular.isString(this.subAltValue)) {
|
||||
this.extensions.subAltNames.names.push({'nameType': this.subAltType, 'value': this.subAltValue});
|
||||
}
|
||||
|
||||
this.subAltType = null;
|
||||
this.subAltValue = null;
|
||||
},
|
||||
removeSubAltName: function (index) {
|
||||
this.extensions.subAltNames.names.splice(index, 1);
|
||||
},
|
||||
attachCustom: function () {
|
||||
if (this.extensions === undefined || this.extensions.custom === undefined) {
|
||||
this.extensions = {'custom': []};
|
||||
}
|
||||
|
||||
if (angular.isString(this.customOid) && angular.isString(this.customEncoding) && angular.isString(this.customValue)) {
|
||||
this.extensions.custom.push(
|
||||
{
|
||||
'oid': this.customOid,
|
||||
'isCritical': this.customIsCritical,
|
||||
'encoding': this.customEncoding,
|
||||
'value': this.customValue
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
this.customOid = null;
|
||||
this.customIsCritical = null;
|
||||
this.customEncoding = null;
|
||||
this.customValue = null;
|
||||
},
|
||||
removeCustom: function (index) {
|
||||
this.extensions.custom.splice(index, 1);
|
||||
}
|
||||
});
|
||||
});
|
||||
return LemurRestangular.all('authorities');
|
||||
})
|
||||
.service('AuthorityService', function ($location, AuthorityApi, toaster) {
|
||||
var AuthorityService = this;
|
||||
AuthorityService.findAuthorityByName = function (filterValue) {
|
||||
return AuthorityApi.getList({'filter[name]': filterValue})
|
||||
.then(function (authorites) {
|
||||
return authorites;
|
||||
});
|
||||
};
|
||||
|
||||
AuthorityService.create = function (authority) {
|
||||
authority.attachSubAltName();
|
||||
return AuthorityApi.post(authority).then(
|
||||
function () {
|
||||
toaster.pop({
|
||||
type: 'success',
|
||||
title: authority.name,
|
||||
body: 'Successfully created!'
|
||||
});
|
||||
$location.path('/authorities');
|
||||
},
|
||||
function (response) {
|
||||
toaster.pop({
|
||||
type: 'error',
|
||||
title: authority.name,
|
||||
body: 'Was not created! ' + response.data.message
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
AuthorityService.update = function (authority) {
|
||||
authority.put().then(
|
||||
function () {
|
||||
toaster.pop({
|
||||
type: 'success',
|
||||
title: authority.name,
|
||||
body: 'Successfully updated!'
|
||||
});
|
||||
$location.path('/authorities');
|
||||
},
|
||||
function (response) {
|
||||
toaster.pop({
|
||||
type: 'error',
|
||||
title: authority.name,
|
||||
body: 'Update Failed! ' + response.data.message
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
AuthorityService.getRoles = function (authority) {
|
||||
authority.getList('roles').then(function (roles) {
|
||||
authority.roles = roles;
|
||||
});
|
||||
};
|
||||
|
||||
AuthorityService.updateActive = function (authority) {
|
||||
authority.put().then(
|
||||
function () {
|
||||
toaster.pop({
|
||||
type: 'success',
|
||||
title: authority.name,
|
||||
body: 'Successfully updated!'
|
||||
});
|
||||
},
|
||||
function (response) {
|
||||
toaster.pop({
|
||||
type: 'error',
|
||||
title: authority.name,
|
||||
body: 'Update Failed! ' + response.data.message
|
||||
});
|
||||
});
|
||||
};
|
||||
});
|
46
lemur/static/app/angular/authorities/view/view.js
vendored
Normal file
46
lemur/static/app/angular/authorities/view/view.js
vendored
Normal file
@ -0,0 +1,46 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('lemur')
|
||||
|
||||
.config(function config($routeProvider) {
|
||||
$routeProvider.when('/authorities', {
|
||||
templateUrl: '/angular/authorities/view/view.tpl.html',
|
||||
controller: 'AuthoritiesViewController'
|
||||
});
|
||||
})
|
||||
|
||||
.controller('AuthoritiesViewController', function ($scope, $q, AuthorityApi, AuthorityService, ngTableParams) {
|
||||
$scope.filter = {};
|
||||
$scope.authoritiesTable = 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) {
|
||||
AuthorityApi.getList(params.url()).then(function (data) {
|
||||
_.each(data, function(authority) {
|
||||
AuthorityService.getRoles(authority);
|
||||
});
|
||||
params.total(data.total);
|
||||
$defer.resolve(data);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$scope.authorityService = AuthorityService;
|
||||
|
||||
$scope.getAuthorityStatus = function () {
|
||||
var def = $q.defer();
|
||||
def.resolve([{'title': 'Active', 'id': true}, {'title': 'Inactive', 'id': false}])
|
||||
return def;
|
||||
};
|
||||
|
||||
$scope.toggleFilter = function (params) {
|
||||
params.settings().$scope.show_filter = !params.settings().$scope.show_filter;
|
||||
};
|
||||
|
||||
});
|
50
lemur/static/app/angular/authorities/view/view.tpl.html
Normal file
50
lemur/static/app/angular/authorities/view/view.tpl.html
Normal file
@ -0,0 +1,50 @@
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h2 class="featurette-heading">Authorities
|
||||
<span class="text-muted"><small>The nail that sticks out farthest gets hammered the hardest</small></span></h2>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<div class="btn-group pull-right">
|
||||
<a href="#/authorities/create" class="btn btn-primary">Create</a>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button ng-click="toggleFilter(authoritiesTable)" class="btn btn-default">Filter</button>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table ng-table="authoritiesTable" class="table table-striped" template-pagination="angular/pager.html" show-filter="false">
|
||||
<tbody>
|
||||
<tr ng-repeat="authority in $data track by $index">
|
||||
<td data-title="'Name'" sortable="'name'" filter="{ 'name': 'text' }">
|
||||
<ul class="list-unstyled">
|
||||
<li>{{ authority.name }}</li>
|
||||
<li><span class="text-muted">{{ authority.description }}</span></li>
|
||||
</ul>
|
||||
</td>
|
||||
<td data-title="'Active'" filter="{ 'active': 'select' }" filter-data="getAuthorityStatus()">
|
||||
<form>
|
||||
<switch ng-change="authorityService.updateActive(authority)" id="status" name="status" ng-model="authority.active" class="green small"></switch>
|
||||
</form>
|
||||
</td>
|
||||
<td data-title="'Roles'"> <!--filter="{ 'select': 'role' }" filter-data="roleService.getRoleDropDown()">-->
|
||||
<div class="btn-group">
|
||||
<button ng-repeat="role in authority.roles" class="btn btn-sm btn-danger">
|
||||
{{ role.name }}
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<td data-title="''">
|
||||
<div class="btn-group-vertical pull-right">
|
||||
<a tooltip="Edit Authority" href="#/authorities/{{ authority.id }}/edit" class="btn btn-sm btn-info">
|
||||
Edit
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
94
lemur/static/app/angular/certificates/certificate/certificate.js
vendored
Normal file
94
lemur/static/app/angular/certificates/certificate/certificate.js
vendored
Normal file
@ -0,0 +1,94 @@
|
||||
'use strict';
|
||||
|
||||
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) {
|
||||
CertificateApi.get($routeParams.id).then(function (certificate) {
|
||||
$scope.certificate = certificate;
|
||||
});
|
||||
|
||||
$scope.momentService = MomentService;
|
||||
$scope.save = CertificateService.update;
|
||||
|
||||
})
|
||||
|
||||
.controller('CertificateCreateController', function ($scope, $modal, CertificateApi, CertificateService, AccountService, ELBService, AuthorityService, MomentService, LemurRestangular) {
|
||||
$scope.certificate = LemurRestangular.restangularizeElement(null, {}, 'certificates');
|
||||
|
||||
$scope.save = function (certificate) {
|
||||
var loadingModal = $modal.open({backdrop: 'static', template: '<wave-spinner></wave-spinner>', windowTemplateUrl: 'angular/loadingModal.html', size: 'large'});
|
||||
CertificateService.create(certificate).then(function (response) {
|
||||
loadingModal.close();
|
||||
});
|
||||
};
|
||||
|
||||
$scope.templates = [
|
||||
{
|
||||
'name': 'Client Certificate',
|
||||
'description': '',
|
||||
'extensions': {
|
||||
'basicConstraints': {},
|
||||
'keyUsage': {
|
||||
'isCritical': true,
|
||||
'useDigitalSignature': true
|
||||
},
|
||||
'extendedKeyUsage': {
|
||||
'isCritical': true,
|
||||
'useClientAuthentication': true
|
||||
},
|
||||
'subjectKeyIdentifier': {
|
||||
'includeSKI': true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'Server Certificate',
|
||||
'description': '',
|
||||
'extensions' : {
|
||||
'basicConstraints': {},
|
||||
'keyUsage': {
|
||||
'isCritical': true,
|
||||
'useKeyEncipherment': true,
|
||||
'useDigitalSignature': true
|
||||
},
|
||||
'extendedKeyUsage': {
|
||||
'isCritical': true,
|
||||
'useServerAuthentication': true
|
||||
},
|
||||
'subjectKeyIdentifier': {
|
||||
'includeSKI': true
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
$scope.openNotBefore = function($event) {
|
||||
$event.preventDefault();
|
||||
$event.stopPropagation();
|
||||
|
||||
$scope.openNotBefore.isOpen = true;
|
||||
};
|
||||
|
||||
$scope.openNotAfter = function($event) {
|
||||
$event.preventDefault();
|
||||
$event.stopPropagation();
|
||||
|
||||
$scope.openNotAfter.isOpen = true;
|
||||
|
||||
};
|
||||
|
||||
$scope.elbService = ELBService;
|
||||
$scope.authorityService = AuthorityService;
|
||||
$scope.accountService = AccountService;
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user