Compare commits
88 Commits
Author | SHA1 | Date | |
---|---|---|---|
c182055dbe | |||
97a289f02a | |||
089c0b2b1b | |||
3b109ec578 | |||
45158c64a2 | |||
fe7b075f7b | |||
a350940cd1 | |||
efec79d8de | |||
62950128a2 | |||
aca69ce03c | |||
bf8ce354e5 | |||
8d09d865b1 | |||
8848146e8d | |||
480078da42 | |||
46a5355377 | |||
3fb226ec11 | |||
7471984ecf | |||
df9b345541 | |||
d75c641848 | |||
a484a6e24d | |||
a7fd74396c | |||
8977c5ddbf | |||
bbb63b4aa6 | |||
f492e9ec1b | |||
03e2991ced | |||
80136834b5 | |||
3b2f71cc8a | |||
572c44b78b | |||
783acf6d8c | |||
fc22f76708 | |||
6ec5d26f0c | |||
53ce9cac4c | |||
51800d5e4b | |||
627b36d2a5 | |||
70ccd137e1 | |||
9a04371680 | |||
f799ff3af1 | |||
6db1d0b031 | |||
d599aaa410 | |||
09bc79ef84 | |||
6e39a1e666 | |||
bb51b59400 | |||
75de814b15 | |||
b4c348aef7 | |||
3476d3bcf3 | |||
45c442000e | |||
a07db5625b | |||
3df50f15f7 | |||
4b7a55c89f | |||
3ff5cdf43f | |||
dbfd6b1e17 | |||
4b9a05198c | |||
d62f57eab3 | |||
96c3ab7f9d | |||
38ebeab163 | |||
fcfaa21a24 | |||
0f0d11a828 | |||
6b2da2fe6b | |||
74525e8e8e | |||
cbcc8af3bd | |||
ab7b0c442c | |||
39c022dbf3 | |||
b00917aa60 | |||
4a0328cd8f | |||
b96af3a1f1 | |||
28e12a973f | |||
1883f3c0e7 | |||
c6747439fb | |||
0b9c814ea5 | |||
f09f5eb0f1 | |||
95ac5245e1 | |||
dd607e5c07 | |||
eb55d5465f | |||
500b212a25 | |||
7554a86d23 | |||
43d4dbbfbd | |||
90e49613f9 | |||
d3ff79d800 | |||
bfcbd1b065 | |||
b488c349e8 | |||
590f43297f | |||
b8720566d7 | |||
d0d3e06c81 | |||
48f38a8625 | |||
13f34fc600 | |||
f679392c61 | |||
f78e9d47d1 | |||
92a3c1a5a0 |
2
.bowerrc
2
.bowerrc
@ -1,3 +1,3 @@
|
||||
{
|
||||
"directory": "lemur/static/app/vendor/bower_components"
|
||||
"directory": "bower_components"
|
||||
}
|
||||
|
3
AUTHORS
3
AUTHORS
@ -1 +1,2 @@
|
||||
- Kevin Glisson (kglisson@netflix.com)
|
||||
- Kevin Glisson (kglisson@netflix.com)
|
||||
- Jeremy Heffner <jheffner@netflix.com>
|
||||
|
2
Makefile
2
Makefile
@ -6,8 +6,6 @@ develop: update-submodules setup-git
|
||||
npm install
|
||||
pip install "setuptools>=0.9.8"
|
||||
# order matters here, base package must install first
|
||||
# this is temporary until the version we need is released
|
||||
pip install -e 'git+https://git@github.com/pyca/cryptography.git#egg=cryptography-1.0.dev1'
|
||||
pip install -e .
|
||||
pip install "file://`pwd`#egg=lemur[dev]"
|
||||
pip install "file://`pwd`#egg=lemur[tests]"
|
||||
|
11
README.rst
11
README.rst
@ -1,6 +1,10 @@
|
||||
Lemur
|
||||
=====
|
||||
|
||||
.. image:: https://badges.gitter.im/Join%20Chat.svg
|
||||
:alt: Join the chat at https://gitter.im/Netflix/lemur
|
||||
:target: https://gitter.im/Netflix/lemur?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
|
||||
|
||||
.. image:: https://img.shields.io/pypi/v/lemur.svg
|
||||
:target: https://pypi.python.org/pypi/lemur/
|
||||
:alt: Latest Version
|
||||
@ -15,7 +19,7 @@ Lemur
|
||||
|
||||
Lemur manages SSL certificate creation. It provides a central portal for developers to issuer their own SSL certificates with 'sane' defaults.
|
||||
|
||||
It works on CPython 2.7. It is known
|
||||
It works on CPython 2.7, 3.3, 3.4 It is known
|
||||
to work on Ubuntu Linux and OS X.
|
||||
|
||||
Project resources
|
||||
@ -24,8 +28,3 @@ Project resources
|
||||
- `Documentation <http://lemur.readthedocs.org/>`_
|
||||
- `Source code <https://github.com/netflix/lemur>`_
|
||||
- `Issue tracker <https://github.com/netflix/lemur/issues>`_
|
||||
|
||||
|
||||
.. image:: https://badges.gitter.im/Join%20Chat.svg
|
||||
:alt: Join the chat at https://gitter.im/Netflix/lemur
|
||||
:target: https://gitter.im/Netflix/lemur?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
|
@ -102,6 +102,53 @@ Basic Configuration
|
||||
LEMUR_ENCRYPTION_KEY = 'supersupersecret'
|
||||
|
||||
|
||||
Certificate Default Options
|
||||
---------------------------
|
||||
|
||||
Lemur allows you to find tune your certificates to your organization. The following defaults are presented in the UI
|
||||
and are used when Lemur creates the CSR for your certificates.
|
||||
|
||||
|
||||
.. data:: LEMUR_DEFAULT_COUNTRY
|
||||
:noindex:
|
||||
|
||||
::
|
||||
|
||||
LEMUR_DEFAULT_COUNTRY = "US"
|
||||
|
||||
|
||||
.. data:: LEMUR_DEFAULT_STATE
|
||||
:noindex:
|
||||
|
||||
::
|
||||
|
||||
LEMUR_DEFAULT_STATE = "CA"
|
||||
|
||||
|
||||
.. data:: LEMUR_DEFAULT_LOCATION
|
||||
:noindex:
|
||||
|
||||
::
|
||||
|
||||
LEMUR_DEFAULT_LOCATION = "Los Gatos"
|
||||
|
||||
|
||||
.. data:: LEMUR_DEFAULT_ORGANIZATION
|
||||
:noindex:
|
||||
|
||||
::
|
||||
|
||||
LEMUR_DEFAULT_ORGANIZATION = "Netflix"
|
||||
|
||||
|
||||
.. data:: LEMUR_DEFAULT_ORGANIZATION_UNIT
|
||||
:noindex:
|
||||
|
||||
::
|
||||
|
||||
LEMUR_DEFAULT_ORGANIZATIONAL_UNIT = "Operations"
|
||||
|
||||
|
||||
Notification Options
|
||||
--------------------
|
||||
|
||||
@ -186,8 +233,10 @@ Verisign/Symantec and CloudCA
|
||||
|
||||
Authentication
|
||||
--------------
|
||||
Lemur currently supports Basic Authentication and Ping OAuth2, additional flows can be added relatively easily
|
||||
If you are not using PING you do not need to configure any of these options
|
||||
Lemur currently supports Basic Authentication and Ping OAuth2 out of the box, additional flows can be added relatively easily
|
||||
If you are not using Ping you do not need to configure any of these options.
|
||||
|
||||
For more information about how to use social logins, see: `Satellizer <https://github.com/sahat/satellizer>`_
|
||||
|
||||
.. data:: PING_SECRET
|
||||
:noindex:
|
||||
|
@ -187,6 +187,12 @@ You'll use the builtin HttpProxyModule within Nginx to handle proxying
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
|
||||
location / {
|
||||
root /www/lemur/lemur/static/dist;
|
||||
include mime.types;
|
||||
index index.html;
|
||||
}
|
||||
|
||||
location / {
|
||||
root /www/lemur/lemur/static/dist;
|
||||
|
@ -48,8 +48,8 @@ gulp.task('test', function (done) {
|
||||
|
||||
gulp.task('dev:fonts', function () {
|
||||
var fileList = [
|
||||
'lemur/static/app/vendor/bower_components/bootstrap/dist/fonts/*',
|
||||
'lemur/static/app/vendor/bower_components/fontawesome/fonts/*'
|
||||
'bower_components/bootstrap/dist/fonts/*',
|
||||
'bower_components/fontawesome/fonts/*'
|
||||
];
|
||||
|
||||
return gulp.src(fileList)
|
||||
@ -57,7 +57,7 @@ gulp.task('dev:fonts', function () {
|
||||
});
|
||||
|
||||
gulp.task('dev:styles', function () {
|
||||
var baseContent = '@import "lemur/static/app/vendor/bower_components/bootstrap/less/bootstrap.less";@import "lemur/static/app/vendor/bower_components/bootswatch/$theme$/variables.less";@import "lemur/static/app/vendor/bower_components/bootswatch/$theme$/bootswatch.less";@import "lemur/static/app/vendor/bower_components/bootstrap/less/utilities.less";';
|
||||
var baseContent = '@import "bower_components/bootstrap/less/bootstrap.less";@import "bower_components/bootswatch/$theme$/variables.less";@import "bower_components/bootswatch/$theme$/bootswatch.less";@import "bower_components/bootstrap/less/utilities.less";';
|
||||
var isBootswatchFile = function (file) {
|
||||
|
||||
var suffix = 'bootswatch.less';
|
||||
@ -73,15 +73,15 @@ gulp.task('dev:styles', function () {
|
||||
|
||||
var fileList = [
|
||||
'lemur/static/app/styles/lemur.css',
|
||||
'lemur/static/app/vendor/bower_components/bootswatch/sandstone/bootswatch.less',
|
||||
'lemur/static/app/vendor/bower_components/fontawesome/css/font-awesome.css',
|
||||
'lemur/static/app/vendor/bower_components/angular-spinkit/src/angular-spinkit.css',
|
||||
'lemur/static/app/vendor/bower_components/angular-chart.js/dist/angular-chart.css',
|
||||
'lemur/static/app/vendor/bower_components/angular-loading-bar/src/loading-bar.css',
|
||||
'lemur/static/app/vendor/bower_components/angular-ui-switch/angular-ui-switch.css',
|
||||
'lemur/static/app/vendor/bower_components/angular-wizard/dist/angular-wizard.css',
|
||||
'lemur/static/app/vendor/bower_components/ng-table/ng-table.css',
|
||||
'lemur/static/app/vendor/bower_components/angularjs-toaster/toaster.css'
|
||||
'bower_components/bootswatch/sandstone/bootswatch.less',
|
||||
'bower_components/fontawesome/css/font-awesome.css',
|
||||
'bower_components/angular-spinkit/src/angular-spinkit.css',
|
||||
'bower_components/angular-chart.js/dist/angular-chart.css',
|
||||
'bower_components/angular-loading-bar/src/loading-bar.css',
|
||||
'bower_components/angular-ui-switch/angular-ui-switch.css',
|
||||
'bower_components/angular-wizard/dist/angular-wizard.css',
|
||||
'bower_components/ng-table/ng-table.css',
|
||||
'bower_components/angularjs-toaster/toaster.css'
|
||||
];
|
||||
|
||||
return gulp.src(fileList)
|
||||
|
@ -27,7 +27,10 @@ function browserSyncInit(baseDir, files, browser) {
|
||||
browserSync.instance = browserSync.init(files, {
|
||||
startPath: '/index.html',
|
||||
server: {
|
||||
baseDir: baseDir
|
||||
baseDir: baseDir,
|
||||
routes: {
|
||||
'/bower_components': './bower_components'
|
||||
}
|
||||
},
|
||||
browser: browser,
|
||||
ghostMode: false
|
||||
|
@ -54,7 +54,7 @@ def configure_hook(app):
|
||||
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'])
|
||||
@crossdomain(origin=u"http://localhost:3000", methods=['PUT', 'HEAD', 'GET', 'POST', 'OPTIONS', 'DELETE'])
|
||||
def after(response):
|
||||
return response
|
||||
|
||||
|
@ -16,24 +16,19 @@ operator_permission = Permission(RoleNeed('operator'))
|
||||
admin_permission = Permission(RoleNeed('admin'))
|
||||
|
||||
CertificateCreator = namedtuple('certificate', ['method', 'value'])
|
||||
CertificateCreatorNeed = partial(CertificateCreator, 'certificateView')
|
||||
|
||||
CertificateOwner = namedtuple('certificate', ['method', 'value'])
|
||||
CertificateOwnerNeed = partial(CertificateOwner, 'certificateView')
|
||||
CertificateCreatorNeed = partial(CertificateCreator, 'key')
|
||||
|
||||
|
||||
class ViewKeyPermission(Permission):
|
||||
def __init__(self, role_id, certificate_id):
|
||||
c_need = CertificateCreatorNeed(str(certificate_id))
|
||||
o_need = CertificateOwnerNeed(str(role_id))
|
||||
super(ViewKeyPermission, self).__init__(o_need, c_need, RoleNeed('admin'))
|
||||
def __init__(self, certificate_id, owner):
|
||||
c_need = CertificateCreatorNeed(certificate_id)
|
||||
super(ViewKeyPermission, self).__init__(c_need, RoleNeed(owner), RoleNeed('admin'))
|
||||
|
||||
|
||||
class UpdateCertificatePermission(Permission):
|
||||
def __init__(self, role_id, certificate_id):
|
||||
c_need = CertificateCreatorNeed(str(certificate_id))
|
||||
o_need = CertificateOwnerNeed(str(role_id))
|
||||
super(UpdateCertificatePermission, self).__init__(o_need, c_need, RoleNeed('admin'))
|
||||
def __init__(self, certificate_id, owner):
|
||||
c_need = CertificateCreatorNeed(certificate_id)
|
||||
super(UpdateCertificatePermission, self).__init__(c_need, RoleNeed(owner), RoleNeed('admin'))
|
||||
|
||||
|
||||
RoleUser = namedtuple('role', ['method', 'value'])
|
||||
@ -42,7 +37,7 @@ ViewRoleCredentialsNeed = partial(RoleUser, 'roleView')
|
||||
|
||||
class ViewRoleCredentialsPermission(Permission):
|
||||
def __init__(self, role_id):
|
||||
need = ViewRoleCredentialsNeed(str(role_id))
|
||||
need = ViewRoleCredentialsNeed(role_id)
|
||||
super(ViewRoleCredentialsPermission, self).__init__(need, RoleNeed('admin'))
|
||||
|
||||
|
||||
|
@ -8,11 +8,12 @@
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
from builtins import bytes
|
||||
import jwt
|
||||
import json
|
||||
import base64
|
||||
import binascii
|
||||
from builtins import str
|
||||
|
||||
from functools import wraps
|
||||
from datetime import datetime, timedelta
|
||||
@ -29,24 +30,21 @@ 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, \
|
||||
from lemur.auth.permissions import CertificateCreatorNeed, \
|
||||
AuthorityCreatorNeed, ViewRoleCredentialsNeed
|
||||
|
||||
|
||||
def base64url_decode(data):
|
||||
if isinstance(data, str):
|
||||
data = str(data)
|
||||
|
||||
rem = len(data) % 4
|
||||
|
||||
if rem > 0:
|
||||
data += b'=' * (4 - rem)
|
||||
data += '=' * (4 - rem)
|
||||
|
||||
return base64.urlsafe_b64decode(data)
|
||||
return base64.urlsafe_b64decode(bytes(data.encode('latin-1')))
|
||||
|
||||
|
||||
def base64url_encode(data):
|
||||
return base64.urlsafe_b64encode(data).replace(b'=', b'')
|
||||
return base64.urlsafe_b64encode(data).replace('=', '')
|
||||
|
||||
|
||||
def get_rsa_public_key(n, e):
|
||||
@ -141,9 +139,11 @@ def fetch_token_header(token):
|
||||
|
||||
try:
|
||||
return json.loads(base64url_decode(header_segment))
|
||||
except TypeError:
|
||||
except TypeError as e:
|
||||
current_app.logger.exception(e)
|
||||
raise jwt.DecodeError('Invalid header padding')
|
||||
except binascii.Error:
|
||||
except binascii.Error as e:
|
||||
current_app.logger.exception(e)
|
||||
raise jwt.DecodeError('Invalid header padding')
|
||||
|
||||
|
||||
@ -165,7 +165,6 @@ def on_identity_loaded(sender, identity):
|
||||
# identity with the roles that the user provides
|
||||
if hasattr(user, 'roles'):
|
||||
for role in user.roles:
|
||||
identity.provides.add(CertificateOwnerNeed(role.id))
|
||||
identity.provides.add(ViewRoleCredentialsNeed(role.id))
|
||||
identity.provides.add(RoleNeed(role.name))
|
||||
|
||||
|
@ -183,10 +183,6 @@ class Ping(Resource):
|
||||
# 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:
|
||||
@ -196,10 +192,12 @@ class Ping(Resource):
|
||||
# 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)
|
||||
|
||||
# every user is an operator (tied to a default role)
|
||||
if current_app.config.get('LEMUR_DEFAULT_ROLE'):
|
||||
v = role_service.get_by_name(current_app.config.get('LEMUR_DEFAULT_ROLE'))
|
||||
if v:
|
||||
roles.append(v)
|
||||
|
||||
user = user_service.create(
|
||||
profile['email'],
|
||||
|
@ -22,7 +22,7 @@ from lemur.certificates.models import Certificate
|
||||
from lemur.plugins.base import plugins
|
||||
|
||||
|
||||
def update(authority_id, active=None, roles=None):
|
||||
def update(authority_id, description=None, owner=None, active=None, roles=None):
|
||||
"""
|
||||
Update a an authority with new values.
|
||||
|
||||
@ -37,6 +37,9 @@ def update(authority_id, active=None, roles=None):
|
||||
|
||||
if active:
|
||||
authority.active = active
|
||||
|
||||
authority.description = description
|
||||
authority.owner = owner
|
||||
return database.update(authority)
|
||||
|
||||
|
||||
|
@ -20,6 +20,7 @@ from lemur.common.utils import paginated_parser, marshal_items
|
||||
|
||||
FIELDS = {
|
||||
'name': fields.String,
|
||||
'owner': fields.String,
|
||||
'description': fields.String,
|
||||
'options': fields.Raw,
|
||||
'pluginName': fields.String,
|
||||
@ -264,7 +265,9 @@ class Authorities(AuthenticatedResource):
|
||||
|
||||
{
|
||||
"roles": [],
|
||||
"active": false
|
||||
"active": false,
|
||||
"owner": "bob@example.com",
|
||||
"description": "this is authority1"
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
@ -279,12 +282,12 @@ class Authorities(AuthenticatedResource):
|
||||
"id": 1,
|
||||
"name": "authority1",
|
||||
"description": "this is authority1",
|
||||
"pluginname": null,
|
||||
"pluginName": null,
|
||||
"chain": "-----begin ...",
|
||||
"body": "-----begin ...",
|
||||
"active": false,
|
||||
"notbefore": "2015-06-05t17:09:39",
|
||||
"notafter": "2015-06-10t17:09:39"
|
||||
"notBefore": "2015-06-05t17:09:39",
|
||||
"notAfter": "2015-06-10t17:09:39"
|
||||
"options": null
|
||||
}
|
||||
|
||||
@ -292,8 +295,10 @@ class Authorities(AuthenticatedResource):
|
||||
: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')
|
||||
self.reqparse.add_argument('roles', type=list, default=[], location='json')
|
||||
self.reqparse.add_argument('active', type=str, location='json', required=True)
|
||||
self.reqparse.add_argument('owner', type=str, location='json', required=True)
|
||||
self.reqparse.add_argument('description', type=str, location='json', required=True)
|
||||
args = self.reqparse.parse_args()
|
||||
|
||||
authority = service.get(authority_id)
|
||||
@ -315,7 +320,13 @@ class Authorities(AuthenticatedResource):
|
||||
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 service.update(
|
||||
authority_id,
|
||||
owner=args['owner'],
|
||||
description=args['description'],
|
||||
active=args['active'],
|
||||
roles=args['roles']
|
||||
)
|
||||
|
||||
return dict(message="You are not authorized to update this authority"), 403
|
||||
|
||||
|
@ -85,14 +85,28 @@ def update(cert_id, owner, description, active, destinations, notifications):
|
||||
:param active:
|
||||
:return:
|
||||
"""
|
||||
from lemur.notifications import service as notification_service
|
||||
cert = get(cert_id)
|
||||
cert.owner = owner
|
||||
cert.active = active
|
||||
cert.description = description
|
||||
|
||||
database.update_list(cert, 'notifications', Notification, notifications)
|
||||
# we might have to create new notifications if the owner changes
|
||||
new_notifications = []
|
||||
# get existing names to remove
|
||||
notification_name = "DEFAULT_{0}".format(cert.owner.split('@')[0].upper())
|
||||
for n in notifications:
|
||||
if notification_name not in n.label:
|
||||
new_notifications.append(n)
|
||||
|
||||
notification_name = "DEFAULT_{0}".format(owner.split('@')[0].upper())
|
||||
new_notifications += notification_service.create_default_expiration_notifications(notification_name, owner)
|
||||
|
||||
cert.notifications = new_notifications
|
||||
|
||||
database.update_list(cert, 'destinations', Destination, destinations)
|
||||
|
||||
cert.owner = owner
|
||||
|
||||
return database.update(cert)
|
||||
|
||||
|
||||
@ -168,6 +182,10 @@ def upload(**kwargs):
|
||||
kwargs.get('intermediate_cert'),
|
||||
)
|
||||
|
||||
# we override the generated name if one is provided
|
||||
if kwargs.get('name'):
|
||||
cert.name = kwargs['name']
|
||||
|
||||
cert.description = kwargs.get('description')
|
||||
|
||||
cert.owner = kwargs['owner']
|
||||
@ -220,7 +238,8 @@ def create(**kwargs):
|
||||
notifications += notification_service.create_default_expiration_notifications(notification_name, [cert.owner])
|
||||
|
||||
notification_name = 'DEFAULT_SECURITY'
|
||||
notifications += notification_service.create_default_expiration_notifications(notification_name, current_app.config.get('LEMUR_SECURITY_TEAM_EMAIL'))
|
||||
notifications += notification_service.create_default_expiration_notifications(notification_name,
|
||||
current_app.config.get('LEMUR_SECURITY_TEAM_EMAIL'))
|
||||
cert.notifications = notifications
|
||||
|
||||
database.update(cert)
|
||||
@ -319,17 +338,18 @@ def create_csr(csr_config):
|
||||
x509.BasicConstraints(ca=False, path_length=None), critical=True,
|
||||
)
|
||||
|
||||
for k, v in csr_config.get('extensions', {}).items():
|
||||
if k == 'subAltNames':
|
||||
# map types to their x509 objects
|
||||
general_names = []
|
||||
for name in v['names']:
|
||||
if name['nameType'] == 'DNSName':
|
||||
general_names.append(x509.DNSName(name['value']))
|
||||
if csr_config.get('extensions'):
|
||||
for k, v in csr_config.get('extensions', {}).items():
|
||||
if k == 'subAltNames':
|
||||
# map types to their x509 objects
|
||||
general_names = []
|
||||
for name in v['names']:
|
||||
if name['nameType'] == 'DNSName':
|
||||
general_names.append(x509.DNSName(name['value']))
|
||||
|
||||
builder = builder.add_extension(
|
||||
x509.SubjectAlternativeName(general_names), critical=True
|
||||
)
|
||||
builder = builder.add_extension(
|
||||
x509.SubjectAlternativeName(general_names), critical=True
|
||||
)
|
||||
|
||||
# TODO support more CSR options, none of the authority plugins currently support these options
|
||||
# builder.add_extension(
|
||||
|
@ -6,14 +6,28 @@
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import hashlib
|
||||
import requests
|
||||
import subprocess
|
||||
from OpenSSL import crypto
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
|
||||
from flask import current_app
|
||||
|
||||
from contextlib import contextmanager
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
|
||||
@contextmanager
|
||||
def mktempfile():
|
||||
with NamedTemporaryFile(delete=False) as f:
|
||||
name = f.name
|
||||
|
||||
try:
|
||||
yield name
|
||||
finally:
|
||||
os.unlink(name)
|
||||
|
||||
|
||||
def ocsp_verify(cert_path, issuer_chain_path):
|
||||
"""
|
||||
@ -53,27 +67,18 @@ def crl_verify(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)
|
||||
with open(cert_path, 'rt') as c:
|
||||
cert = x509.load_pem_x509_certificate(c.read(), default_backend())
|
||||
|
||||
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
|
||||
distribution_points = cert.extensions.get_extension_for_oid(x509.OID_CRL_DISTRIBUTION_POINTS).value
|
||||
for p in distribution_points:
|
||||
point = p.full_name[0].value
|
||||
response = requests.get(point)
|
||||
crl = crypto.load_crl(crypto.FILETYPE_ASN1, response.content) # TODO this should be switched to cryptography when support exists
|
||||
revoked = crl.get_revoked()
|
||||
for r in revoked:
|
||||
if cert.serial == r.get_serial():
|
||||
return
|
||||
return True
|
||||
|
||||
|
||||
@ -99,22 +104,6 @@ def verify(cert_path, issuer_chain_path):
|
||||
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
|
||||
@ -123,13 +112,11 @@ def verify_string(cert_string, issuer_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)
|
||||
with mktempfile() as cert_tmp:
|
||||
with open(cert_tmp, 'w') as f:
|
||||
f.write(cert_string)
|
||||
with mktempfile() as issuer_tmp:
|
||||
with open(issuer_tmp, 'w') as f:
|
||||
f.write(issuer_string)
|
||||
status = verify(cert_tmp, issuer_tmp)
|
||||
return status
|
||||
|
||||
|
||||
def remove_tmp_file(file_path):
|
||||
os.remove(file_path)
|
||||
|
@ -7,7 +7,7 @@
|
||||
"""
|
||||
from builtins import str
|
||||
|
||||
from flask import Blueprint, make_response, jsonify
|
||||
from flask import Blueprint, current_app, make_response, jsonify
|
||||
from flask.ext.restful import reqparse, Api, fields
|
||||
|
||||
from cryptography import x509
|
||||
@ -24,6 +24,8 @@ from lemur.roles import service as role_service
|
||||
|
||||
from lemur.common.utils import marshal_items, paginated_parser
|
||||
|
||||
from lemur.notifications.views import notification_list
|
||||
|
||||
|
||||
mod = Blueprint('certificates', __name__)
|
||||
api = Api(mod)
|
||||
@ -332,7 +334,8 @@ class CertificatesUpload(AuthenticatedResource):
|
||||
"intermediateCert": "---Begin Public...",
|
||||
"privateKey": "---Begin Private..."
|
||||
"destinations": [],
|
||||
"notifications": []
|
||||
"notifications": [],
|
||||
"name": "cert1"
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
@ -373,6 +376,7 @@ class CertificatesUpload(AuthenticatedResource):
|
||||
"""
|
||||
self.reqparse.add_argument('description', type=str, location='json')
|
||||
self.reqparse.add_argument('owner', type=str, required=True, location='json')
|
||||
self.reqparse.add_argument('name', type=str, location='json')
|
||||
self.reqparse.add_argument('publicCert', type=pem_str, required=True, dest='public_cert', location='json')
|
||||
self.reqparse.add_argument('destinations', type=list, default=[], dest='destinations', location='json')
|
||||
self.reqparse.add_argument('notifications', type=list, default=[], dest='notifications', location='json')
|
||||
@ -446,7 +450,7 @@ class CertificatePrivateKey(AuthenticatedResource):
|
||||
|
||||
role = role_service.get_by_name(cert.owner)
|
||||
|
||||
permission = ViewKeyPermission(certificate_id, hasattr(role, 'id'))
|
||||
permission = ViewKeyPermission(certificate_id, getattr(role, 'name', None))
|
||||
|
||||
if permission.can():
|
||||
response = make_response(jsonify(key=cert.private_key), 200)
|
||||
@ -567,12 +571,13 @@ class Certificates(AuthenticatedResource):
|
||||
self.reqparse.add_argument('owner', type=str, location='json')
|
||||
self.reqparse.add_argument('description', type=str, location='json')
|
||||
self.reqparse.add_argument('destinations', type=list, default=[], location='json')
|
||||
self.reqparse.add_argument('notifications', type=list, default=[], location='json')
|
||||
self.reqparse.add_argument('notifications', type=notification_list, default=[], 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'))
|
||||
|
||||
permission = UpdateCertificatePermission(certificate_id, getattr(role, 'name', None))
|
||||
|
||||
if permission.can():
|
||||
return service.update(
|
||||
@ -662,9 +667,59 @@ class NotificationCertificatesList(AuthenticatedResource):
|
||||
args['notification_id'] = notification_id
|
||||
return service.render(args)
|
||||
|
||||
|
||||
class CertificatesDefaults(AuthenticatedResource):
|
||||
""" Defineds the 'certificates' defaults endpoint """
|
||||
def __init__(self):
|
||||
super(CertificatesDefaults)
|
||||
|
||||
def get(self):
|
||||
"""
|
||||
.. http:get:: /certificates/defaults
|
||||
|
||||
Returns defaults needed to generate CSRs
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /certificates/defaults 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
|
||||
|
||||
{
|
||||
"country": "US",
|
||||
"state": "CA",
|
||||
"location": "Los Gatos",
|
||||
"organization": "Netflix",
|
||||
"organizationalUnit": "Operations"
|
||||
}
|
||||
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
"""
|
||||
return dict(
|
||||
country=current_app.config.get('LEMUR_DEFAULT_COUNTRY'),
|
||||
state=current_app.config.get('LEMUR_DEFAULT_STATE'),
|
||||
location=current_app.config.get('LEMUR_DEFAULT_LOCATION'),
|
||||
organization=current_app.config.get('LEMUR_DEFAULT_ORGANIZATION'),
|
||||
organizationalUnit=current_app.config.get('LEMUR_DEFAULT_ORGANIZATIONAL_UNIT')
|
||||
)
|
||||
|
||||
|
||||
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')
|
||||
api.add_resource(NotificationCertificatesList, '/notifications/<int:notification_id>/certificates', endpoint='notificationCertificates')
|
||||
api.add_resource(CertificatesDefaults, '/certificates/defaults', endpoint='certificatesDefault')
|
||||
|
@ -9,10 +9,9 @@
|
||||
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
from flask import current_app
|
||||
|
||||
from sqlalchemy import exc
|
||||
from sqlalchemy.sql import and_, or_
|
||||
from sqlalchemy.orm.exc import NoResultFound
|
||||
|
||||
from lemur.extensions import db
|
||||
from lemur.exceptions import AttrNotFound, DuplicateError
|
||||
@ -126,8 +125,7 @@ def get(model, value, field="id"):
|
||||
query = session_query(model)
|
||||
try:
|
||||
return query.filter(getattr(model, field) == value).one()
|
||||
except Exception as e:
|
||||
current_app.logger.exception(e)
|
||||
except NoResultFound as e:
|
||||
return
|
||||
|
||||
|
||||
|
274
lemur/manage.py
274
lemur/manage.py
@ -1,7 +1,11 @@
|
||||
from __future__ import unicode_literals # at top of module
|
||||
|
||||
import os
|
||||
import sys
|
||||
import base64
|
||||
import time
|
||||
import requests
|
||||
import json
|
||||
from gunicorn.config import make_settings
|
||||
|
||||
from cryptography.fernet import Fernet
|
||||
@ -77,7 +81,15 @@ LEMUR_RESTRICTED_DOMAINS = []
|
||||
|
||||
LEMUR_EMAIL = ''
|
||||
LEMUR_SECURITY_TEAM_EMAIL = []
|
||||
LEMUR_DEFAULT_EXPIRATION_NOTIFICATION_INTERVALS = [30, 15, 2]
|
||||
|
||||
# Certificate Defaults
|
||||
|
||||
LEMUR_DEFAULT_COUNTRY = ''
|
||||
LEMUR_DEFAULT_STATE = ''
|
||||
LEMUR_DEFAULT_LOCATION = ''
|
||||
LEMUR_DEFAULT_ORGANIZATION = ''
|
||||
LEMUR_DEFAULT_ORGANIZATIONAL_UNIT = ''
|
||||
|
||||
|
||||
# Logging
|
||||
|
||||
@ -136,12 +148,15 @@ def check_revoked():
|
||||
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, "")
|
||||
try:
|
||||
if cert.chain:
|
||||
status = verify_string(cert.body, cert.chain)
|
||||
else:
|
||||
status = verify_string(cert.body, "")
|
||||
|
||||
cert.status = 'valid' if status else "invalid"
|
||||
cert.status = 'valid' if status else 'invalid'
|
||||
except Exception as e:
|
||||
cert.status = 'unknown'
|
||||
database.update(cert)
|
||||
|
||||
|
||||
@ -171,19 +186,18 @@ def generate_settings():
|
||||
return output
|
||||
|
||||
|
||||
@manager.option('-s', '--sources', dest='labels', default='', required=False)
|
||||
@manager.option('-l', '--list', dest='view', default=False, required=False)
|
||||
def sync_sources(labels, view):
|
||||
@manager.option('-s', '--sources', dest='labels')
|
||||
def sync_sources(labels):
|
||||
"""
|
||||
Attempts to run several methods Certificate discovery. This is
|
||||
run on a periodic basis and updates the Lemur datastore with the
|
||||
information it discovers.
|
||||
"""
|
||||
if view:
|
||||
if not labels:
|
||||
sys.stdout.write("Active\tLabel\tDescription\n")
|
||||
for source in source_service.get_all():
|
||||
sys.stdout.write(
|
||||
"[{active}]\t{label}\t{description}!\n".format(
|
||||
"{active}\t{label}\t{description}!\n".format(
|
||||
label=source.label,
|
||||
description=source.description,
|
||||
active=source.active
|
||||
@ -198,13 +212,14 @@ def sync_sources(labels, view):
|
||||
try:
|
||||
sync_lock.acquire(timeout=10) # wait up to 10 seconds
|
||||
|
||||
if labels:
|
||||
sys.stdout.write("[+] Staring to sync sources: {labels}!\n".format(labels=labels))
|
||||
labels = labels.split(",")
|
||||
else:
|
||||
sys.stdout.write("[+] Starting to sync ALL sources!\n")
|
||||
sys.stdout.write("[+] Staring to sync sources: {labels}!\n".format(labels=labels))
|
||||
labels = labels.split(",")
|
||||
|
||||
if labels[0] == 'all':
|
||||
sync()
|
||||
else:
|
||||
sync(labels=labels)
|
||||
|
||||
sync(labels=labels)
|
||||
sys.stdout.write(
|
||||
"[+] Finished syncing sources. Run Time: {time}\n".format(
|
||||
time=(time.time() - start_time)
|
||||
@ -480,6 +495,229 @@ def unlock(path=None):
|
||||
sys.stdout.write("[+] Keys have been unencrypted!\n")
|
||||
|
||||
|
||||
def unicode_(data):
|
||||
import sys
|
||||
|
||||
if sys.version_info.major < 3:
|
||||
return data.decode('UTF-8')
|
||||
return data
|
||||
|
||||
|
||||
class ProvisionELB(Command):
|
||||
"""
|
||||
Creates and provisions a certificate on an ELB based on command line arguments
|
||||
"""
|
||||
|
||||
option_list = (
|
||||
Option('-d', '--dns', dest='dns', action='append', required=True, type=unicode_),
|
||||
Option('-e', '--elb', dest='elb_name', required=True, type=unicode_),
|
||||
Option('-o', '--owner', dest='owner', type=unicode_),
|
||||
Option('-a', '--authority', dest='authority', required=True, type=unicode_),
|
||||
Option('-s', '--description', dest='description', default=u'Command line provisioned keypair', type=unicode_),
|
||||
Option('-t', '--destination', dest='destinations', action='append', type=unicode_, required=True),
|
||||
Option('-n', '--notification', dest='notifications', action='append', type=unicode_, default=[]),
|
||||
Option('-r', '--region', dest='region', default=u'us-east-1', type=unicode_),
|
||||
Option('-p', '--dport', '--port', dest='dport', default=7002),
|
||||
Option('--src-port', '--source-port', '--sport', dest='sport', default=443),
|
||||
Option('--dry-run', dest='dryrun', action='store_true')
|
||||
)
|
||||
|
||||
def configure_user(self, owner):
|
||||
from flask import g
|
||||
import lemur.users.service
|
||||
|
||||
# grab the user
|
||||
g.user = lemur.users.service.get_by_username(owner)
|
||||
# get the first user by default
|
||||
if not g.user:
|
||||
g.user = lemur.users.service.get_all()[0]
|
||||
|
||||
return g.user.username
|
||||
|
||||
def build_cert_options(self, destinations, notifications, description, owner, dns, authority):
|
||||
from sqlalchemy.orm.exc import NoResultFound
|
||||
from lemur.certificates.views import valid_authority
|
||||
import sys
|
||||
|
||||
# convert argument lists to arrays, or empty sets
|
||||
destinations = self.get_destinations(destinations)
|
||||
if not destinations:
|
||||
sys.stderr.write("Valid destinations provided\n")
|
||||
sys.exit(1)
|
||||
|
||||
# get the primary CN
|
||||
common_name = dns[0]
|
||||
|
||||
# If there are more than one fqdn, add them as alternate names
|
||||
extensions = {}
|
||||
if len(dns) > 1:
|
||||
extensions['subAltNames'] = {'names': map(lambda x: {'nameType': 'DNSName', 'value': x}, dns)}
|
||||
|
||||
try:
|
||||
authority = valid_authority({"name": authority})
|
||||
except NoResultFound:
|
||||
sys.stderr.write("Invalid authority specified: '{}'\naborting\n".format(authority))
|
||||
sys.exit(1)
|
||||
|
||||
options = {
|
||||
# Convert from the Destination model to the JSON input expected further in the code
|
||||
'destinations': map(lambda x: {'id': x.id, 'label': x.label}, destinations),
|
||||
'description': description,
|
||||
'notifications': notifications,
|
||||
'commonName': common_name,
|
||||
'extensions': extensions,
|
||||
'authority': authority,
|
||||
'owner': owner,
|
||||
# defaults:
|
||||
'organization': u'Netflix, Inc.',
|
||||
'organizationalUnit': u'Operations',
|
||||
'country': u'US',
|
||||
'state': u'California',
|
||||
'location': u'Los Gatos'
|
||||
}
|
||||
|
||||
return options
|
||||
|
||||
def get_destinations(self, destination_names):
|
||||
from lemur.destinations import service
|
||||
|
||||
destinations = []
|
||||
|
||||
for destination_name in destination_names:
|
||||
destination = service.get_by_label(destination_name)
|
||||
|
||||
if not destination:
|
||||
sys.stderr.write("Invalid destination specified: '{}'\nAborting...\n".format(destination_name))
|
||||
sys.exit(1)
|
||||
|
||||
destinations.append(service.get_by_label(destination_name))
|
||||
|
||||
return destinations
|
||||
|
||||
def check_duplicate_listener(self, elb_name, region, account, sport, dport):
|
||||
from lemur.plugins.lemur_aws import elb
|
||||
|
||||
listeners = elb.get_listeners(account, region, elb_name)
|
||||
for listener in listeners:
|
||||
if listener[0] == sport and listener[1] == dport:
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_destination_account(self, destinations):
|
||||
for destination in self.get_destinations(destinations):
|
||||
if destination.plugin_name == 'aws-destination':
|
||||
|
||||
account_number = destination.plugin.get_option('accountNumber', destination.options)
|
||||
return account_number
|
||||
|
||||
sys.stderr.write("No destination AWS account provided, failing\n")
|
||||
sys.exit(1)
|
||||
|
||||
def run(self, dns, elb_name, owner, authority, description, notifications, destinations, region, dport, sport,
|
||||
dryrun):
|
||||
from lemur.certificates import service
|
||||
from lemur.plugins.lemur_aws import elb
|
||||
from boto.exception import BotoServerError
|
||||
|
||||
# configure the owner if we can find it, or go for default, and put it in the global
|
||||
owner = self.configure_user(owner)
|
||||
|
||||
# make a config blob from the command line arguments
|
||||
cert_options = self.build_cert_options(
|
||||
destinations=destinations,
|
||||
notifications=notifications,
|
||||
description=description,
|
||||
owner=owner,
|
||||
dns=dns,
|
||||
authority=authority)
|
||||
|
||||
aws_account = self.get_destination_account(destinations)
|
||||
|
||||
if dryrun:
|
||||
import json
|
||||
|
||||
cert_options['authority'] = cert_options['authority'].name
|
||||
sys.stdout.write('Will create certificate using options: {}\n'
|
||||
.format(json.dumps(cert_options, sort_keys=True, indent=2)))
|
||||
sys.stdout.write('Will create listener {}->{} HTTPS using the new certificate to elb {}\n'
|
||||
.format(sport, dport, elb_name))
|
||||
sys.exit(0)
|
||||
|
||||
if self.check_duplicate_listener(elb_name, region, aws_account, sport, dport):
|
||||
sys.stderr.write("ELB {} already has a listener {}->{}\nAborting...\n".format(elb_name, sport, dport))
|
||||
sys.exit(1)
|
||||
|
||||
# create the certificate
|
||||
try:
|
||||
sys.stdout.write('Creating certificate for {}\n'.format(cert_options['commonName']))
|
||||
cert = service.create(**cert_options)
|
||||
except Exception as e:
|
||||
if e.message == 'Duplicate certificate: a certificate with the same common name exists already':
|
||||
sys.stderr.write("Certificate already exists named: {}\n".format(dns[0]))
|
||||
sys.exit(1)
|
||||
raise e
|
||||
|
||||
cert_arn = cert.get_arn(aws_account)
|
||||
sys.stderr.write('cert arn: {}\n'.format(cert_arn))
|
||||
|
||||
sys.stderr.write('Configuring elb {} from port {} to port {} in region {} with cert {}\n'
|
||||
.format(elb_name, sport, dport, region, cert_arn))
|
||||
|
||||
delay = 1
|
||||
done = False
|
||||
retries = 5
|
||||
while not done and retries > 0:
|
||||
try:
|
||||
elb.create_new_listeners(aws_account, region, elb_name, [(sport, dport, 'HTTPS', cert_arn)])
|
||||
except BotoServerError as bse:
|
||||
# if the server returns ad error, the certificate
|
||||
if bse.error_code == 'CertificateNotFound':
|
||||
sys.stderr.write('Certificate not available yet in the AWS account, waiting {}, {} retries left\n'
|
||||
.format(delay, retries))
|
||||
time.sleep(delay)
|
||||
delay *= 2
|
||||
retries -= 1
|
||||
elif bse.error_code == 'DuplicateListener':
|
||||
sys.stderr.write('ELB {} already has a listener {}->{}'.format(elb_name, sport, dport))
|
||||
sys.exit(1)
|
||||
else:
|
||||
raise bse
|
||||
else:
|
||||
done = True
|
||||
|
||||
|
||||
@manager.command
|
||||
def publish_verisign_units():
|
||||
"""
|
||||
Simple function that queries verisign for API units and posts the mertics to
|
||||
Atlas API for other teams to consume.
|
||||
:return:
|
||||
"""
|
||||
from lemur.plugins import plugins
|
||||
v = plugins.get('verisign-issuer')
|
||||
units = v.get_available_units()
|
||||
|
||||
metrics = {}
|
||||
for item in units:
|
||||
if item['@type'] in metrics.keys():
|
||||
metrics[item['@type']] += int(item['@remaining'])
|
||||
else:
|
||||
metrics.update({item['@type']: int(item['@remaining'])})
|
||||
|
||||
for name, value in metrics.items():
|
||||
metric = [
|
||||
{
|
||||
"timestamp": 1321351651,
|
||||
"type": "GAUGE",
|
||||
"name": "Symantec {0} Unit Count".format(name),
|
||||
"tags": {},
|
||||
"value": value
|
||||
}
|
||||
]
|
||||
|
||||
requests.post('http://localhost:8078/metrics', data=json.dumps(metric))
|
||||
|
||||
|
||||
def main():
|
||||
manager.add_command("start", LemurServer())
|
||||
manager.add_command("runserver", Server(host='127.0.0.1'))
|
||||
@ -489,8 +727,8 @@ def main():
|
||||
manager.add_command("init", InitializeApp())
|
||||
manager.add_command("create_user", CreateUser())
|
||||
manager.add_command("create_role", CreateRole())
|
||||
manager.add_command("provision_elb", ProvisionELB())
|
||||
manager.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
@ -38,7 +38,10 @@ def _get_message_data(cert):
|
||||
:return:
|
||||
"""
|
||||
cert_dict = cert.as_dict()
|
||||
cert_dict['creator'] = cert.user.email
|
||||
|
||||
if cert.user:
|
||||
cert_dict['creator'] = cert.user.email
|
||||
|
||||
cert_dict['domains'] = [x .name for x in cert.domains]
|
||||
cert_dict['superseded'] = list(set([x.name for x in _find_superseded(cert) if cert.name != x]))
|
||||
return cert_dict
|
||||
@ -56,13 +59,18 @@ def _deduplicate(messages):
|
||||
|
||||
for m, r, o in roll_ups:
|
||||
if r == targets:
|
||||
m.append(data)
|
||||
current_app.logger.info(
|
||||
"Sending expiration alert about {0} to {1}".format(
|
||||
data['name'], ",".join(targets)))
|
||||
for cert in m:
|
||||
if cert['body'] == data['body']:
|
||||
break
|
||||
else:
|
||||
m.append(data)
|
||||
current_app.logger.info(
|
||||
"Sending expiration alert about {0} to {1}".format(
|
||||
data['name'], ",".join(targets)))
|
||||
break
|
||||
else:
|
||||
roll_ups.append(([data], targets, options))
|
||||
|
||||
return roll_ups
|
||||
|
||||
|
||||
@ -178,6 +186,9 @@ def create_default_expiration_notifications(name, recipients):
|
||||
:param name:
|
||||
:return:
|
||||
"""
|
||||
if not recipients:
|
||||
return []
|
||||
|
||||
options = [
|
||||
{
|
||||
'name': 'unit',
|
||||
@ -198,7 +209,7 @@ def create_default_expiration_notifications(name, recipients):
|
||||
},
|
||||
]
|
||||
|
||||
intervals = current_app.config.get("LEMUR_DEFAULT_EXPIRATION_NOTIFICATION_INTERVALS")
|
||||
intervals = current_app.config.get("LEMUR_DEFAULT_EXPIRATION_NOTIFICATION_INTERVALS", [30, 15, 2])
|
||||
|
||||
notifications = []
|
||||
for i in intervals:
|
||||
|
@ -28,6 +28,36 @@ FIELDS = {
|
||||
}
|
||||
|
||||
|
||||
def notification(value, name):
|
||||
"""
|
||||
Validates a given notification exits
|
||||
:param value:
|
||||
:param name:
|
||||
:return:
|
||||
"""
|
||||
n = service.get(value)
|
||||
if not n:
|
||||
raise ValueError("Unable to find notification specified")
|
||||
return n
|
||||
|
||||
|
||||
def notification_list(value, name):
|
||||
"""
|
||||
Validates a given notification exists and returns a list
|
||||
:param value:
|
||||
:param name:
|
||||
:return:
|
||||
"""
|
||||
notifications = []
|
||||
for v in value:
|
||||
try:
|
||||
notifications.append(notification(v['id'], 'id'))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return notifications
|
||||
|
||||
|
||||
class NotificationsList(AuthenticatedResource):
|
||||
""" Defines the 'notifications' endpoint """
|
||||
def __init__(self):
|
||||
|
@ -110,7 +110,7 @@ class IPlugin(local):
|
||||
|
||||
def get_option(self, name, options):
|
||||
for o in options:
|
||||
if o.get(name):
|
||||
if o.get('name') == name:
|
||||
return o['value']
|
||||
|
||||
|
||||
|
@ -138,3 +138,19 @@ def delete_listeners(account_number, region, name, ports):
|
||||
:return:
|
||||
"""
|
||||
return assume_service(account_number, 'elb', region).delete_load_balancer_listeners(name, ports)
|
||||
|
||||
|
||||
def get_listeners(account_number, region, name):
|
||||
"""
|
||||
Gets the listeners configured on an elb and returns a array of tuples
|
||||
|
||||
:param account_number:
|
||||
:param region:
|
||||
:param name:
|
||||
:return: list of tuples
|
||||
"""
|
||||
|
||||
conn = assume_service(account_number, 'elb', region)
|
||||
elbs = conn.get_all_load_balancers(load_balancer_names=[name])
|
||||
if elbs:
|
||||
return elbs[0].listeners
|
||||
|
@ -6,6 +6,7 @@
|
||||
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
from boto.exception import BotoServerError
|
||||
from lemur.plugins.bases import DestinationPlugin, SourcePlugin
|
||||
from lemur.plugins.lemur_aws import iam, elb
|
||||
from lemur.plugins import lemur_aws as aws
|
||||
@ -42,7 +43,11 @@ class AWSDestinationPlugin(DestinationPlugin):
|
||||
# }
|
||||
|
||||
def upload(self, name, body, private_key, cert_chain, options, **kwargs):
|
||||
iam.upload_cert(find_value('accountNumber', options), name, body, private_key, cert_chain=cert_chain)
|
||||
try:
|
||||
iam.upload_cert(find_value('accountNumber', options), name, body, private_key, cert_chain=cert_chain)
|
||||
except BotoServerError as e:
|
||||
if e.error_code != 'EntityAlreadyExists':
|
||||
raise Exception(e)
|
||||
|
||||
e = find_value('elb', options)
|
||||
if e:
|
||||
|
@ -7,6 +7,7 @@
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
|
||||
"""
|
||||
import re
|
||||
import ssl
|
||||
import base64
|
||||
from json import dumps
|
||||
@ -14,6 +15,7 @@ from json import dumps
|
||||
import arrow
|
||||
import requests
|
||||
from requests.adapters import HTTPAdapter
|
||||
from requests.exceptions import ConnectionError
|
||||
|
||||
from flask import current_app
|
||||
|
||||
@ -23,8 +25,6 @@ from lemur.plugins import lemur_cloudca as cloudca
|
||||
|
||||
from lemur.authorities import service as authority_service
|
||||
|
||||
API_ENDPOINT = '/v1/ca/netflix' # TODO this should be configurable
|
||||
|
||||
|
||||
class CloudCAException(LemurException):
|
||||
def __init__(self, message):
|
||||
@ -172,7 +172,11 @@ class CloudCA(object):
|
||||
|
||||
# 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)
|
||||
try:
|
||||
response = self.session.post(self.url + endpoint, data=data, timeout=10, verify=self.ca_bundle)
|
||||
except ConnectionError:
|
||||
raise Exception("Could not talk to CloudCA, is it up?")
|
||||
|
||||
return process_response(response)
|
||||
|
||||
def get(self, endpoint):
|
||||
@ -182,7 +186,11 @@ class CloudCA(object):
|
||||
:param endpoint:
|
||||
:return:
|
||||
"""
|
||||
response = self.session.get(self.url + endpoint, timeout=10, verify=self.ca_bundle)
|
||||
try:
|
||||
response = self.session.get(self.url + endpoint, timeout=10, verify=self.ca_bundle)
|
||||
except ConnectionError:
|
||||
raise Exception("Could not talk to CloudCA, is it up?")
|
||||
|
||||
return process_response(response)
|
||||
|
||||
def random(self, length=10):
|
||||
@ -202,7 +210,7 @@ class CloudCA(object):
|
||||
|
||||
:return:
|
||||
"""
|
||||
endpoint = '{0}/listCAs'.format(API_ENDPOINT)
|
||||
endpoint = '{0}/listCAs'.format(current_app.config.get('CLOUDCA_API_ENDPOINT'))
|
||||
authorities = []
|
||||
for ca in self.get(endpoint)['data']['caList']:
|
||||
try:
|
||||
@ -230,7 +238,7 @@ class CloudCAIssuerPlugin(IssuerPlugin, CloudCA):
|
||||
:return:
|
||||
"""
|
||||
# this is weird and I don't like it
|
||||
endpoint = '{0}/createCA'.format(API_ENDPOINT)
|
||||
endpoint = '{0}/createCA'.format(current_app.config.get('CLOUDCA_API_ENDPOINT'))
|
||||
options['caDN']['email'] = options['ownerEmail']
|
||||
|
||||
if options['caType'] == 'subca':
|
||||
@ -238,9 +246,13 @@ class CloudCAIssuerPlugin(IssuerPlugin, CloudCA):
|
||||
|
||||
options['validityStart'] = convert_date_to_utc_time(options['validityStart']).isoformat()
|
||||
options['validityEnd'] = convert_date_to_utc_time(options['validityEnd']).isoformat()
|
||||
options['description'] = re.sub(r'[^a-zA-Z0-9]', '', options['caDescription'])
|
||||
|
||||
response = self.session.post(self.url + endpoint, data=dumps(remove_none(options)), timeout=10,
|
||||
verify=self.ca_bundle)
|
||||
try:
|
||||
response = self.session.post(self.url + endpoint, data=dumps(remove_none(options)), timeout=10,
|
||||
verify=self.ca_bundle)
|
||||
except ConnectionError:
|
||||
raise Exception("Could not communicate with CloudCA, is it up?")
|
||||
|
||||
json = process_response(response)
|
||||
roles = []
|
||||
@ -274,7 +286,7 @@ class CloudCAIssuerPlugin(IssuerPlugin, CloudCA):
|
||||
:param csr:
|
||||
:param options:
|
||||
"""
|
||||
endpoint = '{0}/enroll'.format(API_ENDPOINT)
|
||||
endpoint = '{0}/enroll'.format(current_app.config.get('CLOUDCA_API_ENDPOINT'))
|
||||
# lets default to two years if it's not specified
|
||||
# we do some last minute data massaging
|
||||
options = get_default_issuance(options)
|
||||
@ -287,7 +299,7 @@ class CloudCAIssuerPlugin(IssuerPlugin, CloudCA):
|
||||
'ownerEmail': options['owner'],
|
||||
'caName': options['authority'].name,
|
||||
'csr': csr,
|
||||
'comment': options['description']
|
||||
'comment': re.sub(r'[^a-zA-Z0-9]', '', options['description'])
|
||||
}
|
||||
|
||||
response = self.post(endpoint, remove_none(cloudca_options))
|
||||
@ -316,11 +328,11 @@ class CloudCASourcePlugin(SourcePlugin, CloudCA):
|
||||
'pollRate': {'type': 'int', 'default': '60'}
|
||||
}
|
||||
|
||||
def get_certificates(self, **kwargs):
|
||||
def get_certificates(self, options, **kwargs):
|
||||
certs = []
|
||||
for authority in self.get_authorities():
|
||||
certs += self.get_cert(ca_name=authority)
|
||||
return
|
||||
return certs
|
||||
|
||||
def get_cert(self, ca_name=None, cert_handle=None):
|
||||
"""
|
||||
@ -330,7 +342,7 @@ class CloudCASourcePlugin(SourcePlugin, CloudCA):
|
||||
:param cert_handle:
|
||||
:return:
|
||||
"""
|
||||
endpoint = '{0}/getCert'.format(API_ENDPOINT)
|
||||
endpoint = '{0}/getCert'.format(current_app.config.get('CLOUDCA_API_ENDPOINT'))
|
||||
response = self.session.post(self.url + endpoint, data=dumps({'caName': ca_name}), timeout=10,
|
||||
verify=self.ca_bundle)
|
||||
raw = process_response(response)
|
||||
@ -345,7 +357,7 @@ class CloudCASourcePlugin(SourcePlugin, CloudCA):
|
||||
|
||||
certs.append({
|
||||
'public_certificate': cert,
|
||||
'intermediate_cert': "\n".join(intermediates),
|
||||
'intermediate_certificate': "\n".join(intermediates),
|
||||
'owner': c['ownerEmail']
|
||||
})
|
||||
|
||||
|
@ -53,9 +53,9 @@ class EmailNotificationPlugin(ExpirationNotificationPlugin):
|
||||
|
||||
# jinja template depending on type
|
||||
template = env.get_template('{}.html'.format(event_type))
|
||||
body = template.render(**kwargs)
|
||||
body = template.render(dict(messages=message, hostname=current_app.config.get('LEMUR_HOSTNAME')))
|
||||
|
||||
s_type = current_app.config.get("LEMUR_EMAIL_SENDER").lower()
|
||||
s_type = current_app.config.get("LEMUR_EMAIL_SENDER", 'ses').lower()
|
||||
if s_type == 'ses':
|
||||
conn = boto.connect_ses()
|
||||
conn.send_email(current_app.config.get("LEMUR_EMAIL"), subject, body, targets, format='html')
|
||||
|
@ -1,4 +1,5 @@
|
||||
from jinja2 import Environment, PackageLoader
|
||||
import os
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
|
||||
loader = PackageLoader('lemur')
|
||||
loader = FileSystemLoader(searchpath=os.path.dirname(os.path.realpath(__file__)))
|
||||
env = Environment(loader=loader)
|
||||
|
@ -52,8 +52,13 @@
|
||||
<span style="color: #29abe0">Notice: Your SSL certificates are expiring!</span>
|
||||
<hr />
|
||||
</div>
|
||||
Lemur, Netflix's SSL management portal has noticed that the following certificates are expiring soon, if you rely on these certificates
|
||||
you should create new certificates to replace the certificates that are expiring. Visit https://lemur.netflix.com/#/certificates/create to reissue them.
|
||||
<p>
|
||||
Lemur, Netflix's SSL management portal has noticed that the following certificates are expiring soon, if you rely on these certificates
|
||||
you should create new certificates to replace the certificates that are expiring.
|
||||
</p>
|
||||
<p>
|
||||
Visit https://{{ hostname }}/#/certificates/create to reissue them.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
{% for message in messages %}
|
||||
@ -78,6 +83,12 @@
|
||||
<tr>
|
||||
<td>{{ message.creator }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Description</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ message.description }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Not Before</strong></td>
|
||||
</tr>
|
||||
@ -104,20 +115,6 @@
|
||||
<td>Unknown</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td><strong>Associated ELBs</strong></td>
|
||||
</tr>
|
||||
{% if message.listeners %}
|
||||
{% for name in message.listeners %}
|
||||
<tr>
|
||||
<td>{{ name }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td>None</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td><strong>Potentially Superseded by</strong> (Lemur's best guess)</td>
|
||||
</tr>
|
||||
@ -139,7 +136,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding-top: 0px" align="center" valign="top">
|
||||
<em style="font-style:italic; font-size: 12px; color: #aaa;">Lemur is broken regularly by <a style="color: #29abe0; text-decoration: none;" href="mailto:secops@netflix.com">Security Operations</a></em>
|
||||
<em style="font-style:italic; font-size: 12px; color: #aaa;">Lemur is broken regularly by Netflix</em>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
@ -56,6 +56,7 @@ VERISIGN_ERRORS = {
|
||||
"0x4828": "Verisign certificates can be at most two years in length",
|
||||
"0x3043": "Certificates must have a validity of at least 1 day",
|
||||
"0x950b": "CSR: Invalid State",
|
||||
"0x3105": "Organization Name Not Matched",
|
||||
}
|
||||
|
||||
|
||||
|
@ -140,7 +140,7 @@ class RolesList(AuthenticatedResource):
|
||||
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')
|
||||
self.reqparse.add_argument('users', type=list, location='json')
|
||||
|
||||
args = self.reqparse.parse_args()
|
||||
return service.create(args['name'], args.get('password'), args.get('description'), args.get('username'),
|
||||
|
@ -39,6 +39,7 @@ def _disassociate_certs_from_source(current_certificates, found_certificates, so
|
||||
|
||||
def sync_create(certificate, source):
|
||||
cert = cert_service.import_certificate(**certificate)
|
||||
cert.description = "This certificate was automatically discovered by Lemur"
|
||||
cert.sources.append(source)
|
||||
sync_update_destination(cert, source)
|
||||
database.update(cert)
|
||||
|
2
lemur/static/app/angular/app.js
vendored
2
lemur/static/app/angular/app.js
vendored
@ -25,7 +25,7 @@ var lemur = angular
|
||||
});
|
||||
|
||||
$authProvider.oauth2({
|
||||
name: 'ping',
|
||||
name: 'example',
|
||||
url: 'http://localhost:5000/api/1/auth/ping',
|
||||
redirectUri: 'http://localhost:3000/',
|
||||
clientId: 'client-id',
|
||||
|
@ -3,8 +3,8 @@
|
||||
<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 class="btn btn-block btn-default" ng-click="authenticate('Example')">
|
||||
Login with Example
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,18 +0,0 @@
|
||||
'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');
|
||||
});
|
||||
};
|
||||
});
|
@ -1,16 +0,0 @@
|
||||
<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>
|
@ -1,9 +1,32 @@
|
||||
<div class="modal-header">
|
||||
<div class="modal-title">
|
||||
<div class="modal-header">Edit Authority <span class="text-muted"><small>chain of command!</small></span></div>
|
||||
<h3 class="modal-header">Edit <span class="text-muted"><small>{{ authority.name }}</small></span></h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="createForm" class="form-horizontal" role="form" novalidate>
|
||||
<div class="form-group"
|
||||
ng-class="{'has-error': editForm.owner.$invalid, 'has-success': !editForm.owner.$invalid&&editForm.owner.$dirty}">
|
||||
<label class="control-label col-sm-2">
|
||||
Owner
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="email" name="owner" ng-model="authority.owner" placeholder="owner@netflix.com"
|
||||
class="form-control" required/>
|
||||
|
||||
<p ng-show="editForm.owner.$invalid && !editForm.owner.$pristine" class="help-block">Enter a valid
|
||||
email.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group"
|
||||
ng-class="{'has-error': editForm.description.$invalid, 'has-success': !editForm.$invalid&&editForm.description.$dirty}">
|
||||
<label class="control-label col-sm-2">
|
||||
Description
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea name="description" ng-model="authority.description" placeholder="Something elegant" class="form-control" required></textarea>
|
||||
<p ng-show="editForm.description.$invalid && !editForm.description.$pristine" class="help-block">You must give a short description about this authority will be used for, this description should only include alphanumeric characters</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-2">
|
||||
Roles
|
@ -26,8 +26,8 @@
|
||||
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>
|
||||
<textarea name="caDescription" ng-model="authority.caDescription" placeholder="Something elegant" class="form-control" ng-maxlength="250" 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</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group"
|
||||
@ -36,8 +36,8 @@
|
||||
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.commonName.$invalid && !trackingForm.commonName.$pristine" class="help-block">You must enter a common name</p>
|
||||
<input name="commonName" ng-model="authority.caDN.commonName" placeholder="Common Name" class="form-control" ng-maxlength="64" required/>
|
||||
<p ng-show="trackingForm.commonName.$invalid && !trackingForm.commonName.$pristine" class="help-block">You must enter a common name and it must be less than 64 characters in length</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group"
|
||||
|
@ -46,7 +46,7 @@ angular.module('lemur')
|
||||
$scope.edit = function (authorityId) {
|
||||
var modalInstance = $modal.open({
|
||||
animation: true,
|
||||
templateUrl: '/angular/authorities/authority/authorityEdit.tpl.html',
|
||||
templateUrl: '/angular/authorities/authority/edit.tpl.html',
|
||||
controller: 'AuthorityEditController',
|
||||
size: 'lg',
|
||||
resolve: {
|
||||
@ -62,6 +62,25 @@ angular.module('lemur')
|
||||
|
||||
};
|
||||
|
||||
$scope.editRole = function (roleId) {
|
||||
var modalInstance = $modal.open({
|
||||
animation: true,
|
||||
templateUrl: '/angular/roles/role/role.tpl.html',
|
||||
controller: 'RolesEditController',
|
||||
size: 'lg',
|
||||
resolve: {
|
||||
editId: function () {
|
||||
return roleId;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
modalInstance.result.then(function () {
|
||||
$scope.authoritiesTable.reload();
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
$scope.create = function () {
|
||||
var modalInstance = $modal.open({
|
||||
animation: true,
|
||||
|
@ -29,7 +29,7 @@
|
||||
</td>
|
||||
<td data-title="'Roles'"> <!--filter="{ 'select': 'role' }" filter-data="roleService.getRoleDropDown()">-->
|
||||
<div class="btn-group">
|
||||
<a href="#/roles/{{ role.id }}/edit" ng-repeat="role in authority.roles" class="btn btn-sm btn-danger">
|
||||
<a ng-click="editRole(role.id)" ng-repeat="role in authority.roles" class="btn btn-sm btn-danger">
|
||||
{{ role.name }}
|
||||
</a>
|
||||
</div>
|
||||
|
@ -25,6 +25,9 @@ angular.module('lemur')
|
||||
.controller('CertificateCreateController', function ($scope, $modalInstance, CertificateApi, CertificateService, DestinationService, AuthorityService, PluginService, MomentService, WizardHandler, LemurRestangular, NotificationService) {
|
||||
$scope.certificate = LemurRestangular.restangularizeElement(null, {}, 'certificates');
|
||||
|
||||
// set the defaults
|
||||
CertificateService.getDefaults($scope.certificate);
|
||||
|
||||
$scope.create = function (certificate) {
|
||||
WizardHandler.wizard().context.loading = true;
|
||||
CertificateService.create(certificate).then(function () {
|
||||
|
@ -6,7 +6,7 @@
|
||||
Country
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input name="country" ng-model="certificate.country" placeholder="Country" class="form-control" ng-init="certificate.country = 'US'" required/>
|
||||
<input name="country" ng-model="certificate.country" placeholder="Country" class="form-control" required/>
|
||||
<p ng-show="dnForm.country.$invalid && !dnForm.country.$pristine" class="help-block">You must enter a country</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -16,7 +16,7 @@
|
||||
State
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input name="state" ng-model="certificate.state" placeholder="State" class="form-control" ng-init="certificate.state = 'California'" required/>
|
||||
<input name="state" ng-model="certificate.state" placeholder="State" class="form-control" required/>
|
||||
<p ng-show="dnForm.state.$invalid && !dnForm.state.$pristine" class="help-block">You must enter a state</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -26,7 +26,7 @@
|
||||
Location
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input name="location" ng-model="certificate.location" placeholder="Location" class="form-control" ng-init="certificate.location = 'Los Gatos'"required/>
|
||||
<input name="location" ng-model="certificate.location" placeholder="Location" class="form-control" required/>
|
||||
<p ng-show="dnForm.location.$invalid && !dnForm.location.$pristine" class="help-block">You must enter a location</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -36,7 +36,7 @@
|
||||
Organization
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input name="organization" ng-model="certificate.organization" placeholder="Organization" class="form-control" ng-init="certificate.organization = 'Netflix'" required/>
|
||||
<input name="organization" ng-model="certificate.organization" placeholder="Organization" class="form-control" required/>
|
||||
<p ng-show="dnForm.organization.$invalid && !dnForm.organization.$pristine" class="help-block">You must enter a organization</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -46,7 +46,7 @@
|
||||
Organizational Unit
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input name="organizationalUnit" ng-model="certificate.organizationalUnit" placeholder="Organizational Unit" class="form-control" ng-init="certificate.organizationalUnit = 'Operations'"required/>
|
||||
<input name="organizationalUnit" ng-model="certificate.organizationalUnit" placeholder="Organizational Unit" class="form-control" required/>
|
||||
<p ng-show="dnForm.organization.$invalid && !dnForm.organizationalUnit.$pristine" class="help-block">You must enter a organizational unit</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -94,6 +94,11 @@
|
||||
<input type="checkbox" ng-model="certificate.extensions.extendedKeyUsage.useServerAuthentication">Server Authentication
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="certificate.extensions.extendedKeyUsage.useClientAuthentication">Client Authentication
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="certificate.extensions.extendedKeyUsage.useEmail">Email
|
||||
|
@ -16,8 +16,8 @@
|
||||
Description
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea name="description" ng-model="certificate.description" placeholder="Something elegant" class="form-control" ng-pattern="/^[\w\-\s]+$/" required></textarea>
|
||||
<p ng-show="trackingForm.description.$invalid && !trackingForm.description.$pristine" class="help-block">You must give a short description about this authority will be used for, this description should only include alphanumeric characters</p>
|
||||
<textarea name="description" ng-model="certificate.description" placeholder="Something elegant" class="form-control" required></textarea>
|
||||
<p ng-show="trackingForm.description.$invalid && !trackingForm.description.$pristine" class="help-block">You must give a short description about this authority will be used for.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group"
|
||||
@ -47,8 +47,8 @@
|
||||
Common Name
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input name="commonName" tooltip="If you need a certificate with multiple domains enter your primary domain here and the rest under 'Subject Alternate Names' in the next panel" ng-model="certificate.commonName" placeholder="Common Name" class="form-control" required/>
|
||||
<p ng-show="trackingForm.commonName.$invalid && !trackingForm.commonName.$pristine" class="help-block">You must enter a common name</p>
|
||||
<input name="commonName" tooltip="If you need a certificate with multiple domains enter your primary domain here and the rest under 'Subject Alternate Names' in the next few panels" ng-model="certificate.commonName" placeholder="Common Name" class="form-control" ng-maxlength="64" required/>
|
||||
<p ng-show="trackingForm.commonName.$invalid && !trackingForm.commonName.$pristine" class="help-block">You must enter a common name and it must be less than 64 characters</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
|
@ -18,14 +18,23 @@
|
||||
email.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group"
|
||||
ng-class="{'has-error': uploadForm.name.$invalid, 'has-success': !uploadForm.name.$invalid&&uploadForm.name.$dirty}">
|
||||
<label class="control-label col-sm-2" tooltip="If no name is provided, Lemur will generate a name for you">
|
||||
Custom Name <span class="glyphicon glyphicon-question-sign"></span>
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input name="name" ng-model="certificate.name" placeholder="example.netflix.net-SymantecCorporation-20150828-20160830" class="form-control"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group"
|
||||
ng-class="{'has-error': uploadForm.description.$invalid, 'has-success': !uploadForm.$invalid&&uploadForm.description.$dirty}">
|
||||
<label class="control-label col-sm-2">
|
||||
Description
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea name="description" ng-model="certificate.description" placeholder="Something elegant" class="form-control" ng-pattern="/^[\w\-\s]+$/" required></textarea>
|
||||
<p ng-show="uploadForm.description.$invalid && !uploadForm.description.$pristine" class="help-block">You must give a short description about this authority will be used for, this description should only include alphanumeric characters</p>
|
||||
<textarea name="description" ng-model="certificate.description" placeholder="Something elegant" class="form-control" required></textarea>
|
||||
<p ng-show="uploadForm.description.$invalid && !uploadForm.description.$pristine" class="help-block">You must give a short description about this authority will be used for.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group"
|
||||
|
@ -107,7 +107,6 @@ angular.module('lemur')
|
||||
title: certificate.name,
|
||||
body: 'Successfully created!'
|
||||
});
|
||||
$location.path('/certificates');
|
||||
},
|
||||
function (response) {
|
||||
toaster.pop({
|
||||
@ -120,14 +119,21 @@ angular.module('lemur')
|
||||
};
|
||||
|
||||
CertificateService.update = function (certificate) {
|
||||
return LemurRestangular.copy(certificate).put().then(function () {
|
||||
toaster.pop({
|
||||
type: 'success',
|
||||
title: certificate.name,
|
||||
body: 'Successfully updated!'
|
||||
return LemurRestangular.copy(certificate).put().then(
|
||||
function () {
|
||||
toaster.pop({
|
||||
type: 'success',
|
||||
title: certificate.name,
|
||||
body: 'Successfully updated!'
|
||||
});
|
||||
},
|
||||
function (response) {
|
||||
toaster.pop({
|
||||
type: 'error',
|
||||
title: certificate.name,
|
||||
body: 'Failed to update ' + response.data.message
|
||||
});
|
||||
});
|
||||
$location.path('certificates');
|
||||
});
|
||||
};
|
||||
|
||||
CertificateService.upload = function (certificate) {
|
||||
@ -138,7 +144,6 @@ angular.module('lemur')
|
||||
title: certificate.name,
|
||||
body: 'Successfully uploaded!'
|
||||
});
|
||||
$location.path('/certificates');
|
||||
},
|
||||
function (response) {
|
||||
toaster.pop({
|
||||
@ -201,6 +206,16 @@ angular.module('lemur')
|
||||
});
|
||||
};
|
||||
|
||||
CertificateService.getDefaults = function (certificate) {
|
||||
return certificate.customGET('defaults').then(function (defaults) {
|
||||
certificate.country = defaults.country;
|
||||
certificate.state = defaults.state;
|
||||
certificate.location = defaults.location;
|
||||
certificate.organization = defaults.organization;
|
||||
certificate.organizationalUnit = defaults.organizationalUnit;
|
||||
});
|
||||
};
|
||||
|
||||
CertificateService.updateActive = function (certificate) {
|
||||
return certificate.put().then(
|
||||
function () {
|
||||
|
@ -34,16 +34,6 @@ angular.module('lemur')
|
||||
});
|
||||
});
|
||||
|
||||
PluginService.getByType('destination').then(function (plugins) {
|
||||
$scope.plugins = plugins;
|
||||
_.each($scope.plugins, function (plugin) {
|
||||
if (plugin.slug === $scope.destination.pluginName) {
|
||||
plugin.pluginOptions = $scope.destination.destinationOptions;
|
||||
$scope.destination.plugin = plugin;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$scope.save = function (destination) {
|
||||
DestinationService.update(destination).then(function () {
|
||||
$modalInstance.close();
|
||||
|
@ -21,7 +21,7 @@ angular.module('lemur')
|
||||
}, {
|
||||
total: 0, // length of data
|
||||
getData: function ($defer, params) {
|
||||
DomainApi.getList().then(function (data) {
|
||||
DomainApi.getList(params.url()).then(function (data) {
|
||||
params.total(data.total);
|
||||
$defer.resolve(data);
|
||||
});
|
||||
|
6
lemur/static/app/angular/roles/role/role.js
vendored
6
lemur/static/app/angular/roles/role/role.js
vendored
@ -18,6 +18,12 @@ angular.module('lemur')
|
||||
$modalInstance.dismiss('cancel');
|
||||
};
|
||||
|
||||
$scope.userPage = 1;
|
||||
$scope.loadMoreRoles = function () {
|
||||
$scope.userPage += 1;
|
||||
RoleService.loadMoreUsers($scope.role, $scope.userPage);
|
||||
};
|
||||
|
||||
$scope.userService = UserService;
|
||||
$scope.roleService = RoleService;
|
||||
})
|
||||
|
@ -27,7 +27,7 @@
|
||||
Username
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input ng-show="!role.fromServer" name="username" ng-model="role.username" placeholder="Username" class="form-control" required/>
|
||||
<input ng-show="!role.fromServer" name="username" ng-model="role.username" placeholder="Username" class="form-control"/>
|
||||
<div class="well">
|
||||
<span ng-show="role.password">
|
||||
{{ role.username }}
|
||||
@ -43,7 +43,7 @@
|
||||
Password
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input ng-show="!role.fromServer" type="password" name="password" ng-model="role.password" placeholder="hunter2" class="form-control" required/>
|
||||
<input ng-show="!role.fromServer" type="password" name="password" ng-model="role.password" placeholder="hunter2" class="form-control"/>
|
||||
<p ng-show="createForm.password.$invalid && !createForm.password.$pristine" class="help-block">You must enter an password</p>
|
||||
<div class="well">
|
||||
<span ng-show="role.password">
|
||||
@ -61,7 +61,7 @@
|
||||
<input tooltip="You can attach any user to this role, once attached they will have access as defined by this role"
|
||||
typeahead="user.username for user in userService.findUserByName($viewValue)" typeahead-loading="loadingUsers"
|
||||
typeahead-min-wait="100" typeahead-on-select="role.addUser($item)"
|
||||
type="text" name="user" ng-model="role.selectedUser" placeholder="Username..." class="form-control" required/>
|
||||
type="text" name="user" ng-model="role.selectedUser" placeholder="Username..." class="form-control"/>
|
||||
<table ng-show="role.users" class="table">
|
||||
<tr ng-repeat="user in role.users track by $index">
|
||||
<td>{{ user.username }}</td>
|
||||
@ -69,6 +69,10 @@
|
||||
<button type="button" ng-click="role.removeUser($index)" class="btn btn-danger btn-sm pull-right">Remove</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td><a class="pull-right" ng-click="loadMoreUsers()"><strong>More</strong></a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
12
lemur/static/app/angular/roles/services.js
vendored
12
lemur/static/app/angular/roles/services.js
vendored
@ -34,11 +34,19 @@ angular.module('lemur')
|
||||
};
|
||||
|
||||
RoleService.getUsers = function (role) {
|
||||
role.customGET('users').then(function (users) {
|
||||
return role.getList('users').then(function (users) {
|
||||
role.users = users;
|
||||
});
|
||||
};
|
||||
|
||||
RoleService.loadMoreUsers = function (role, page) {
|
||||
role.getList('users', {page: page}).then(function (users) {
|
||||
_.each(users, function (user) {
|
||||
role.users.push(user);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
RoleService.create = function (role) {
|
||||
return RoleApi.post(role).then(
|
||||
function () {
|
||||
@ -47,7 +55,6 @@ angular.module('lemur')
|
||||
title: role.name,
|
||||
body: 'Has been successfully created!'
|
||||
});
|
||||
$location.path('roles');
|
||||
},
|
||||
function (response) {
|
||||
toaster.pop({
|
||||
@ -66,7 +73,6 @@ angular.module('lemur')
|
||||
title: role.name,
|
||||
body: 'Successfully updated!'
|
||||
});
|
||||
$location.path('roles');
|
||||
},
|
||||
function (response) {
|
||||
toaster.pop({
|
||||
|
@ -87,7 +87,7 @@
|
||||
</div>
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<p class="text-muted">Lemur is maintained by <a href="mailto:secops@netflix.com">Security Operations</a>.</p>
|
||||
<p class="text-muted">Lemur is broken regularly by <a href="https://github.com/Netflix/lemur.git">Netflix</a>.</p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
|
@ -15,6 +15,7 @@ def get_key():
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
return current_app.config.get('LEMUR_ENCRYPTION_KEY')
|
||||
return current_app.config.get('LEMUR_ENCRYPTION_KEY').strip()
|
||||
except RuntimeError:
|
||||
print("No Encryption Key Found")
|
||||
return ''
|
||||
|
7
setup.py
7
setup.py
@ -39,7 +39,7 @@ install_requires = [
|
||||
'six==1.9.0',
|
||||
'gunicorn==19.3.0',
|
||||
'pycrypto==2.6.1',
|
||||
'cryptography>=1.0dev',
|
||||
'cryptography==1.0',
|
||||
'pyopenssl==0.15.1',
|
||||
'pyjwt==1.0.1',
|
||||
'xmltodict==0.9.2',
|
||||
@ -72,7 +72,7 @@ class SmartInstall(install):
|
||||
`build_static` which is required for JavaScript assets and other things.
|
||||
"""
|
||||
def _needs_static(self):
|
||||
return not os.path.exists(os.path.join(ROOT, 'lemur-package.json'))
|
||||
return not os.path.exists(os.path.join(ROOT, 'lemur/static/dist'))
|
||||
|
||||
def run(self):
|
||||
if self._needs_static():
|
||||
@ -113,7 +113,7 @@ setup(
|
||||
version='0.1',
|
||||
author='Kevin Glisson',
|
||||
author_email='kglisson@netflix.com',
|
||||
long_description=open('README.rst').read(),
|
||||
long_description=open(os.path.join(ROOT, 'README.rst')).read(),
|
||||
packages=['lemur'],
|
||||
include_package_data=True,
|
||||
zip_safe=False,
|
||||
@ -125,7 +125,6 @@ setup(
|
||||
},
|
||||
cmdclass={
|
||||
'build_static': BuildStatic,
|
||||
'develop': DevelopWithBuildStatic,
|
||||
'sdist': SdistWithBuildStatic,
|
||||
'install': SmartInstall
|
||||
|
||||
|
Reference in New Issue
Block a user