Compare commits

..

88 Commits
0.1 ... 0.1.2

Author SHA1 Message Date
c182055dbe Merge pull request #70 from jeremy-h/master
Adding me to authors
2015-09-02 10:48:27 -07:00
97a289f02a Adding me to authors 2015-09-02 09:48:28 -07:00
089c0b2b1b Merge pull request #68 from kevgliss/crons
Crons
2015-09-02 09:35:46 -07:00
3b109ec578 Cleaning up temporary file creation, and revocation checking 2015-09-02 09:19:06 -07:00
45158c64a2 cleaning up temporary file creation 2015-09-02 09:19:06 -07:00
fe7b075f7b rely on stable version of cryptography instead of dev 2015-09-02 09:19:06 -07:00
a350940cd1 Adding command to fetch and publish verisign units 2015-09-02 09:19:06 -07:00
efec79d8de removing silly description validation from lemur and enforcing it on the cloudca plugin (who actually cares) 2015-09-02 09:15:12 -07:00
62950128a2 Adding a better error message for really long common names Fixes #38 2015-09-02 09:15:11 -07:00
aca69ce03c Closes #53 2015-09-02 09:15:11 -07:00
bf8ce354e5 Closes #55 2015-09-02 09:13:47 -07:00
8d09d865b1 Closes #57 2015-09-02 09:13:47 -07:00
8848146e8d Merge pull request #67 from kevgliss/authority
Authority fixes
2015-09-01 15:02:51 -07:00
480078da42 Removing str casting for role permission 2015-09-01 14:15:40 -07:00
46a5355377 Allows authorities to have editable owners and descriptions 2015-09-01 14:15:40 -07:00
3fb226ec11 Merge pull request #64 from kevgliss/validation
Validation of common name field
2015-08-29 14:01:31 -07:00
7471984ecf removing silly description validation from lemur and enforcing it on the cloudca plugin (who actually cares) 2015-08-29 13:57:07 -07:00
df9b345541 Adding a better error message for really long common names Fixes #38 2015-08-29 13:57:07 -07:00
d75c641848 Merge pull request #62 from kevgliss/notifications
Closes #53
2015-08-29 13:39:11 -07:00
a484a6e24d Closes #53 2015-08-29 13:07:30 -07:00
a7fd74396c Merge pull request #61 from kevgliss/editOwner
Closes #55
2015-08-29 12:09:09 -07:00
8977c5ddbf Ensuring notifications follow owner 2015-08-29 12:02:50 -07:00
bbb63b4aa6 Merge pull request #60 from kevgliss/customName
Closes #57
2015-08-29 12:00:53 -07:00
f492e9ec1b Closes #55 2015-08-29 11:53:46 -07:00
03e2991ced Closes #57 2015-08-29 11:48:39 -07:00
80136834b5 Merge pull request #59 from kevgliss/cleanup
Cleanup
2015-08-29 10:30:03 -07:00
3b2f71cc8a Merge pull request #58 from kevgliss/configBasedNames
Adding ability to define distinguished names in config
2015-08-29 10:23:21 -07:00
572c44b78b Adding a some more docs around oauth2 2015-08-29 10:15:31 -07:00
783acf6d8c Removing Meechum specific code 2015-08-29 10:11:03 -07:00
fc22f76708 Updating supported versions 2015-08-29 09:54:56 -07:00
6ec5d26f0c Merge pull request #51 from jeremy-h/elb-ssl-automation
Elb ssl automation
2015-08-29 09:52:35 -07:00
53ce9cac4c Fix a typo, add a typo 2015-08-27 15:55:39 -07:00
51800d5e4b Added better error handling
Added a "dry run" option
2015-08-27 15:48:49 -07:00
627b36d2a5 Adding method to get existing listeners 2015-08-27 15:45:00 -07:00
70ccd137e1 removing netflix specific code from auth flow 2015-08-27 13:09:02 -07:00
9a04371680 Adding ability to define distinguished names in config 2015-08-27 12:59:40 -07:00
f799ff3af1 Seeing if using decode explicity this helps py3 problem 2015-08-24 20:10:03 -07:00
6db1d0b031 fixing unicode support 2015-08-24 16:37:24 -07:00
d599aaa410 Updating to handle unicode in python 2 and 3$
added retry with backoff for the SSL cert to show up after it is added (CAP, ftw)$
2015-08-24 16:17:04 -07:00
09bc79ef84 Merge remote-tracking branch 'upstream/master' into elb-ssl-automation 2015-08-24 12:18:40 -07:00
6e39a1e666 Finished glue code to push ELBs. 2015-08-24 12:18:15 -07:00
bb51b59400 Merge pull request #49 from kevgliss/orgname
Orgname
2015-08-24 10:00:09 -07:00
75de814b15 Adding new verisign error 2015-08-24 09:43:30 -07:00
b4c348aef7 switching out default orgname 2015-08-24 09:41:03 -07:00
3476d3bcf3 Merge pull request #48 from kevgliss/fixes
Fixes
2015-08-22 13:04:02 -07:00
45c442000e Fixing some unfortunate casting that prevent creators from viewing/updating their certs 2015-08-22 10:56:15 -07:00
a07db5625b Fixing an issue were extensions were implicitly required 2015-08-22 10:22:36 -07:00
3df50f15f7 Merge pull request #47 from kevgliss/keys
Fixing issue with a certificate with no role not being viewable
2015-08-21 16:18:13 -07:00
4b7a55c89f Fixing issue with a certificate with no role not being viewable 2015-08-21 16:08:53 -07:00
3ff5cdf43f Merge remote-tracking branch 'upstream/master' into elb-ssl-automation 2015-08-21 14:29:03 -07:00
dbfd6b1e17 Fixing this so it pulls the named option 2015-08-21 13:09:29 -07:00
4b9a05198c Merge pull request #46 from kevgliss/b64fix
Fixing an issue with futures
2015-08-20 16:02:31 -07:00
d62f57eab3 Fixing an issue with futures, unicode and b64 not being able to handle the unicode values 2015-08-20 15:49:08 -07:00
96c3ab7f9d Merge remote-tracking branch 'upstream/master' into elb-ssl-automation 2015-08-20 15:46:11 -07:00
38ebeab163 Refactoring.. with pep8 fixes 2015-08-20 15:45:53 -07:00
fcfaa21a24 Refactoring 2015-08-20 15:45:42 -07:00
0f0d11a828 Merge pull request #45 from kevgliss/authByOwner
Fixes #35
2015-08-19 18:08:55 -07:00
6b2da2fe6b Fixes #35 2015-08-19 18:05:18 -07:00
74525e8e8e Merge pull request #44 from kevgliss/domains
Fixing bug were domains would not have correct pagination
2015-08-19 16:58:56 -07:00
cbcc8af3bd Fixing bug were domains would not have correct pagination 2015-08-19 16:42:56 -07:00
ab7b0c442c provisionelb creates certs. needs some cleanup and the rest of the glue 2015-08-19 16:10:45 -07:00
39c022dbf3 Merge pull request #43 from kevgliss/decryption
Ensure there are no accidental newlines when fetching the ENCRYPTION_KEY
2015-08-19 15:49:07 -07:00
b00917aa60 Ensure there are no accidental newlines when fetching the ENCRYPTION_KEY 2015-08-19 15:46:10 -07:00
4a0328cd8f Merge pull request #42 from kevgliss/notify
Minor fixes in notifications
2015-08-19 10:29:14 -07:00
b96af3a1f1 Editing footer text 2015-08-19 10:10:19 -07:00
28e12a973f Misc fixed around certificate notifications 2015-08-19 10:07:22 -07:00
1883f3c0e7 Merge pull request #41 from kevgliss/sync
Misc fixed around certificate syncing
2015-08-18 16:21:11 -07:00
c6747439fb Misc fixed around certificate syncing 2015-08-18 16:17:20 -07:00
0b9c814ea5 Merge pull request #40 from kevgliss/privateKey
Fixing issue with creating roles
2015-08-18 09:30:25 -07:00
f09f5eb0f1 Fixing issue with creating roles 2015-08-17 22:51:29 -07:00
95ac5245e1 Merge pull request #39 from kevgliss/notificationInterval
Notification interval
2015-08-17 20:52:32 -07:00
dd607e5c07 Making CLOUDCA_API_ENDPOINT configurable 2015-08-17 17:09:31 -07:00
eb55d5465f Making LEMUR_DEFAULT_SECURITY_EMAIL optional 2015-08-17 16:03:57 -07:00
500b212a25 Adding a few default expiration intervals 2015-08-17 15:49:16 -07:00
7554a86d23 Merge pull request #37 from kevgliss/fixes
General fixes around build time issues
2015-08-14 10:11:46 -07:00
43d4dbbfbd Fixing the paths related to javascript dependecies 2015-08-14 10:05:30 -07:00
90e49613f9 develop doesn't need to build the static files, the make develop will do that 2015-08-11 16:21:00 -07:00
d3ff79d800 Getting correct path to readme so that it doesn't matter where setup.py is run from 2015-08-11 15:46:54 -07:00
bfcbd1b065 Fixes issue where client authentication was not displaying in the UI 2015-08-11 15:43:59 -07:00
b488c349e8 Look for compiled static files, to see if they need to be created 2015-08-11 14:53:28 -07:00
590f43297f Merge pull request #34 from Netflix/0.1.x
Joining 0.1.x with master
2015-08-09 17:01:31 -07:00
b8720566d7 fixing merge conflict 2015-08-09 16:52:14 -07:00
d0d3e06c81 fixing merge conflicts 2015-08-09 16:51:25 -07:00
48f38a8625 Fixing bad cherry pick 2015-08-09 16:49:18 -07:00
13f34fc600 Merge pull request #21 from kevgliss/buildfixes
Build Fixes
2015-08-09 16:47:39 -07:00
f679392c61 Fixing bad cherry pick 2015-07-19 19:28:49 -07:00
f78e9d47d1 Merge pull request #21 from kevgliss/buildfixes
Build Fixes
2015-07-19 19:21:46 -07:00
92a3c1a5a0 Merge pull request #14 from kevgliss/master
A few misc fixes found during testing.
2015-07-02 14:14:49 -07:00
53 changed files with 764 additions and 284 deletions

View File

@ -1,3 +1,3 @@
{
"directory": "lemur/static/app/vendor/bower_components"
"directory": "bower_components"
}

View File

@ -1 +1,2 @@
- Kevin Glisson (kglisson@netflix.com)
- Kevin Glisson (kglisson@netflix.com)
- Jeremy Heffner <jheffner@netflix.com>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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'],

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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']
})

View File

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

View File

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

View File

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

View File

@ -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",
}

View File

@ -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'),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 () {

View File

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

View File

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

View File

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

View File

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

View File

@ -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 () {

View File

@ -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();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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