initial commit

This commit is contained in:
Kevin Glisson
2015-06-22 13:47:27 -07:00
commit 4330ac9c05
228 changed files with 16656 additions and 0 deletions

69
lemur/__init__.py Normal file
View 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

View File

29
lemur/accounts/models.py Normal file
View 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
View 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
View 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')

View File

62
lemur/analyze/service.py Normal file
View 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
View File

62
lemur/auth/permissions.py Normal file
View 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
View 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
View 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')

View File

View 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

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

View File

View 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'])

View 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

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

View 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
View 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
View File

185
lemur/common/crypto.py Normal file
View 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
View 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'

View File

View File

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

View 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),

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

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

View 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

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

View File

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

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

View 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=
"""

View File

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

View 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-----
"""

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

View File

27
lemur/domains/models.py Normal file
View 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
View 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
View 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
View File

44
lemur/elbs/models.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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)

View File

43
lemur/listeners/models.py Normal file
View 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
View 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
View 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
View 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
View File

@ -0,0 +1 @@
Generic single-database configuration.

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

View 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
View 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
View 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
View File

39
lemur/roles/models.py Normal file
View 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
View 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
View 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')

View File

@ -0,0 +1 @@
*.coffee

157
lemur/static/app/404.html Normal file
View 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>

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

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

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

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

View 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
View 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="">&laquo;</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="">&#8230;</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="">&raquo;</a> </li> </ul> </div></div>');
}]);

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

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

View 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('/');
});

View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title></title>
</head>
<body>
</body>
</html>

View 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('/');
})
};
});

View 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');
});
};
});

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,3 @@
<a tabindex="-1">
{{ match.model.name }} - <span class="text-muted">{{match.model.description }}</span>
</a>

View File

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

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

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

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

View 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