Compare commits

...

119 Commits
0.1 ... 0.1.3

Author SHA1 Message Date
aa76379d6c Merge pull request #81 from kevgliss/pipy
Minor fixes
2015-09-18 15:56:06 -07:00
ef72de89b3 Minor fixes 2015-09-18 15:50:59 -07:00
e2962a4b8d Merge pull request #79 from kevgliss/order
fixing an error where dates components were not replaced in logical o…
2015-09-16 11:14:47 -07:00
a563986ce4 fixing an error where dates components were not replaced in logical order 2015-09-16 11:10:09 -07:00
a4e294634a Merge pull request #78 from kevgliss/docs
Doc fixes
2015-09-14 15:28:42 -07:00
067122f8f4 improving docs 2015-09-14 13:46:39 -07:00
6a1a744eff removing duplicate route 2015-09-12 10:05:58 -07:00
d3cf273a45 Merge pull request #72 from kevgliss/docker
[WIP] Docker
2015-09-11 15:36:25 -07:00
52e267468a Merge pull request #76 from kevgliss/verisignSource
Verisign source
2015-09-11 08:49:11 -07:00
e80b58899d following RFC 2015-09-11 08:39:27 -07:00
25f652c1eb fixing merge conflict 2015-09-11 08:38:48 -07:00
0dde4c9f80 removing source plugin form being installed atm 2015-09-11 08:36:55 -07:00
a512b82196 Merge pull request #75 from kevgliss/fixes
Fixes
2015-09-11 08:36:24 -07:00
7f119e95e1 making the verisign urls more generic 2015-09-11 08:27:34 -07:00
bf957d2509 moving to hotfix version of cryptography 2015-09-11 08:19:35 -07:00
1e314b505f fixing keyerror 2015-09-08 18:18:14 -07:00
ef9a80ebfd adding actual recipients 2015-09-08 18:03:18 -07:00
84d0afae4c fixing email internvals 2015-09-08 17:56:20 -07:00
48a53ad436 fixing error in default password creation 2015-09-08 17:42:57 -07:00
2f4aee49e2 adding logging 2015-09-08 10:56:23 -07:00
f3f5b9eeb3 adding password commandline option 2015-09-08 10:56:23 -07:00
541d5420bb Merge pull request #73 from kevgliss/cleanup
Cleanup
2015-09-08 08:43:56 -07:00
0383e2a1e1 adding manifest 2015-09-07 22:14:18 -07:00
084604cf3c fixing setup.py 2015-09-07 21:54:23 -07:00
8ab9c06778 removing more netflix 2015-09-04 15:54:52 -07:00
0afd4c94b4 removing more netflix 2015-09-04 15:54:02 -07:00
aaae4d5a1f unifying lemur defaults 2015-09-04 15:52:56 -07:00
9da713ab06 cleaning up references to netflix 2015-09-04 15:29:57 -07:00
540d56f0b8 Merge pull request #71 from kevgliss/notifications
Fixing issue with expiration emails not being sent
2015-09-04 09:46:06 -07:00
160eaa6901 Fixing issue with expiration emails not being sent 2015-09-04 09:24:55 -07:00
180c8228e1 adding verisign source 2015-09-02 14:37:07 -07:00
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
79 changed files with 1049 additions and 444 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>

4
MANIFEST.in Normal file
View File

@ -0,0 +1,4 @@
include setup.py package.json bower.json gulpfile.js README.rst MANIFEST.in LICENSE AUTHORS
recursive-include lemur/plugins/lemur_email/templates *
recursive-include lemur/static *
global-exclude *~

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,8 +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
to work on Ubuntu Linux and OS X.
It works on CPython 2.7, 3.3, 3.4. We deploy on Ubuntu and develop on OS X.
Project resources
=================
@ -24,8 +27,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

@ -74,8 +74,6 @@ Basic Configuration
The TOKEN_SECRET is the secret used to create JWT tokens that are given out to users. This should be securely generated and be kept private.
See `SECRET_KEY` for methods on secure secret generation.
::
LEMUR_TOKEN_SECRET = 'supersecret'
@ -102,18 +100,66 @@ 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 = "California"
.. 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
--------------------
Lemur currently has very basic support for notifications. Notifications are sent to the certificate creator, owner and
security team as specified by the `SECURITY_TEAM_EMAIL` configuration parameter.
Lemur currently has very basic support for notifications. Currently only expiration notifications are supported. Actual notification
is handling by the notification plugins that you have configured. Lemur ships with the 'Email' notification that allows expiration emails
to be sent to subscribers.
The template for all of these notifications lives under lemur/template/event.html and can be easily modified to fit your
needs.
Templates for expiration emails are located under `lemur/plugins/lemur_email/templates` and can be modified for your needs.
Notifications are sent to the certificate creator, owner and security team as specified by the `LEMUR_SECURITY_TEAM_EMAIL` configuration parameter.
Certificates marked as in-active will **not** be notified of upcoming expiration. This enables a user to essentially
silence the expiration. If a certificate is active and is expiring the above will be notified at 30, 15, 5, 2 days
respectively.
silence the expiration. If a certificate is active and is expiring the above will be notified according to the `LEMUR_DEFAULT_EXPIRATION_NOTIFICATION_INTERVALS` or
30, 15, 2 days before expiration if no intervals are set.
Lemur supports sending certification expiration notifications through SES and SMTP.
@ -150,6 +196,16 @@ Lemur supports sending certification expiration notifications through SES and SM
LEMUR_SECURITY_TEAM_EMAIL = ['security@example.com']
.. data:: LEMUR_DEFAULT_EXPIRATION_NOTIFICATION_INTERVALS
:noindex:
Lemur notification intervals
::
LEMUR_DEFAULT_EXPIRATION_NOTIFICATION_INTERVALS = [30, 15, 2]
Authority Options
-----------------
@ -168,6 +224,35 @@ Verisign/Symantec and CloudCA
This is the path to the mutual SSL certificate used for communicating with Verisign
.. data:: VERISIGN_FIRST_NAME
:noindex:
This is the first name to be used when requesting the certificate
.. data:: VERISIGN_LAST_NAME
:noindex:
This is the last name to be used when requesting the certificate
.. data:: VERISIGN_EMAIL
:noindex:
This is the email to be used when requesting the certificate
.. data:: VERISIGN_INTERMEDIATE
:noindex:
This is the intermediate to be used for your CA chain
.. data:: VERISIGN_ROOT
:noindex:
This is the root to be used for your CA chain
.. data:: CLOUDCA_URL
:noindex:
@ -184,10 +269,13 @@ Verisign/Symantec and CloudCA
This is the path to the CLOUDCA certificate bundle
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:
@ -230,9 +318,9 @@ In order for Lemur to manage it's own account and other accounts we must ensure
Setting up IAM roles
--------------------
Lemur's aws plugin uses boto heavily to talk to all the AWS resources it manages. By default it uses the on-instance credentials to make the necessary calls.
Lemur's AWS plugin uses boto heavily to talk to all the AWS resources it manages. By default it uses the on-instance credentials to make the necessary calls.
In order to limit the permissions we will create a new two IAM roles for Lemur. You can name them whatever you would like but for example sake we will be calling them LemurInstanceProfile and Lemur.
In order to limit the permissions, we will create two new IAM roles for Lemur. You can name them whatever you would like but for example sake we will be calling them LemurInstanceProfile and Lemur.
Lemur uses to STS to talk to different accounts. For managing one account this isn't necessary but we will still use it so that we can easily add new accounts.
@ -278,6 +366,11 @@ STS-AssumeRole
Next we will create the the Lemur IAM role. Lemur
..note::
The default IAM role that Lemur assumes into is called `Lemur`, if you need to change this ensure you set `LEMUR_INSTANCE_PROFILE` to your role name in the configuration.
Here is an example policy for Lemur:
IAM-ServerCertificate
@ -535,7 +628,8 @@ that the `Authority` is associated with it Lemur allows that user to user/view/u
This RBAC is also used when determining which users can access which certificate private key. Lemur's current permission
structure is setup such that if the user is a `Creator` or `Owner` of a given certificate they are allow to view that
private key.
private key. Owners can also be a role name, such that any user with the same role as owner will be allowed to view the
private key information.
These permissions are applied to the user upon login and refreshed on every request.

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
# security_monkey documentation build configuration file, created by
# lemur documentation build configuration file, created by
# sphinx-quickstart on Sat Jun 7 18:43:48 2014.
#
# This file is execfile()d with the current directory set to its

View File

@ -215,7 +215,7 @@ certificate Lemur does not know about and adding the certificate to it's invento
The `SourcePlugin` object has one default option of `pollRate`. This controls the number of seconds which to get new certificates.
.. warning::
Lemur currently has a very basic polling system of running a cron job every 15min to see which source plugins need to be run. A lock file is generated to guarentee that ]
Lemur currently has a very basic polling system of running a cron job every 15min to see which source plugins need to be run. A lock file is generated to guarantee that
only one sync is running at a time. It also means that the minimum resolution of a source plugin poll rate is effectively 15min. You can always specify a faster cron
job if you need a higher resolution sync job.

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

BIN
docs/guide/create.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

BIN
docs/guide/create_role.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

BIN
docs/guide/create_user.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View File

@ -1,14 +1,95 @@
Creating Users
==============
User Guide
==========
These guides are quick tutorials on how to perform basic tasks in Lemur.
Create a New User
~~~~~~~~~~~~~~~~~
.. figure:: settings.png
From the settings dropdown select "Users"
.. figure:: create.png
In the user table select "Create"
.. figure:: create_user.png
Enter the username, email and password for the user. You can also assign any
roles that the user will need when they login. While there is no deletion
(we want to track creators forever) you can mark a user as 'Inactive' that will
not allow them to login to Lemur.
Creating Roles
==============
Create a New Role
~~~~~~~~~~~~~~~~~
.. figure:: settings.png
From the settings dropdown select "Roles"
.. figure:: create.png
In the role table select "Create"
.. figure:: create_role.png
Enter a role name and short description about the role. You can optionally store
a user/password on the role. This is useful if your authority require specific roles.
You can then accurately map those roles onto Lemur users. Also optional you can assign
users to your new role.
Creating Authorities
====================
Create a New Authority
~~~~~~~~~~~~~~~~~~~~~~
.. figure:: create.png
In the authority table select "Create"
.. figure:: create_authority.png
Enter a authority name and short description about the authority. Enter an owner,
and certificate common name. Depending on the authority and the authority/issuer plugin
these values may or may not be used.
.. figure:: create_authority_options.png
Again how many of these values get used largely depends on the underlying plugin. It
is important to make sure you select the right plugin that you wish to use.
Creating Certificates
=====================
Create a New Certificate
~~~~~~~~~~~~~~~~~~~~~~~~
.. figure:: create.png
In the certificate table select "Create"
.. figure:: create_certificate.png
Enter a owner, short description and the authority you wish to issue this certificate.
Enter a common name into the certificate, if no validity range is selected two years is
the default.
You can add notification options and upload the created certificate to a destination, both
of these are editable features and can be changed after the certificate has been created.
.. figure:: certificate_extensions.png
These options are typically for advanced users, the one exception is the `Subject Alternate Names` or SAN.
For certificates that need to include more than one domains, the first domain is the Common Name and all
other domains are added here as DNSName entries.
Import an Existing Certificate
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. figure:: upload_certificate.png
Enter a owner, short description and public certificate. If there are intermediates and private keys
Lemur will track them just as it does if the certificate were created through Lemur. Lemur generates
a certificate name but you can override that by passing a value to the `Custom Name` field.
You can add notification options and upload the created certificate to a destination, both
of these are editable features and can be changed after the certificate has been created.

BIN
docs/guide/settings.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

View File

@ -4,6 +4,8 @@ Quickstart
This guide will step you through setting up a Python-based virtualenv, installing the required packages, and configuring the basic web service.
This guide assumes a clean Ubuntu 14.04 instance, commands may differ based on the OS and configuration being used.
Pressed for time? See the Lemur docker file on `Github <https://github.com/Netflix/lemur-docker>`_.
Dependencies
------------
@ -18,6 +20,7 @@ Some basic prerequisites which you'll need in order to run Lemur:
are largely handled for us. Lemur does **not** require AWS to function. Our guides and documentation try to be
be as generic as possible and are not intended to document every step of launching Lemur into a given environment.
Setting up an Environment
-------------------------
@ -187,7 +190,7 @@ 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;

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

@ -17,7 +17,7 @@ from lemur.domains.views import mod as domains_bp
from lemur.destinations.views import mod as destinations_bp
from lemur.authorities.views import mod as authorities_bp
from lemur.certificates.views import mod as certificates_bp
from lemur.status.views import mod as status_bp
from lemur.defaults.views import mod as defaults_bp
from lemur.plugins.views import mod as plugins_bp
from lemur.notifications.views import mod as notifications_bp
from lemur.sources.views import mod as sources_bp
@ -31,7 +31,7 @@ LEMUR_BLUEPRINTS = (
destinations_bp,
authorities_bp,
certificates_bp,
status_bp,
defaults_bp,
plugins_bp,
notifications_bp,
sources_bp
@ -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

@ -125,7 +125,7 @@ class Ping(Resource):
args = self.reqparse.parse_args()
# take the information we have received from Meechum to create a new request
# take the information we have received from the provider to create a new request
params = {
'client_id': args['clientId'],
'grant_type': 'authorization_code',
@ -138,7 +138,7 @@ class Ping(Resource):
access_token_url = current_app.config.get('PING_ACCESS_TOKEN_URL')
user_api_url = current_app.config.get('PING_USER_API_URL')
# the secret and cliendId will be given to you when you signup for meechum
# the secret and cliendId will be given to you when you signup for the provider
basic = base64.b64encode('{0}:{1}'.format(args['clientId'], current_app.config.get("PING_SECRET")))
headers = {'Authorization': 'Basic {0}'.format(basic)}
@ -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'],
@ -222,7 +220,7 @@ class Ping(Resource):
profile['email'],
profile['email'],
True,
profile.get('thumbnailPhotoUrl'), # Encase profile isn't google+ enabled
profile.get('thumbnailPhotoUrl'), # incase profile isn't google+ enabled
roles
)

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

@ -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,6 +667,7 @@ class NotificationCertificatesList(AuthenticatedResource):
args['notification_id'] = notification_id
return service.render(args)
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')

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

63
lemur/defaults/views.py Normal file
View File

@ -0,0 +1,63 @@
"""
.. module: lemur.status.views
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
"""
from flask import current_app, Blueprint
from flask.ext.restful import Api
from lemur.auth.service import AuthenticatedResource
mod = Blueprint('default', __name__)
api = Api(mod)
class LemurDefaults(AuthenticatedResource):
""" Defines the 'defaults' endpoint """
def __init__(self):
super(LemurDefaults)
def get(self):
"""
.. http:get:: /defaults
Returns defaults needed to generate CSRs
**Example request**:
.. sourcecode:: http
GET /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(LemurDefaults, '/defaults', endpoint='default')

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)
@ -248,18 +263,23 @@ class InitializeApp(Command):
Additionally a Lemur user will be created as a default user
and be used when certificates are discovered by Lemur.
"""
def run(self):
option_list = (
Option('-p', '--password', dest='password'),
)
def run(self, password):
create()
user = user_service.get_by_username("lemur")
if not user:
sys.stdout.write("We need to set Lemur's password to continue!\n")
password1 = prompt_pass("Password")
password2 = prompt_pass("Confirm Password")
if not password:
sys.stdout.write("We need to set Lemur's password to continue!\n")
password = prompt_pass("Password")
password1 = prompt_pass("Confirm Password")
if password1 != password2:
sys.stderr.write("[!] Passwords do not match!\n")
sys.exit(1)
if password != password1:
sys.stderr.write("[!] Passwords do not match!\n")
sys.exit(1)
role = role_service.get_by_name('admin')
@ -270,16 +290,16 @@ class InitializeApp(Command):
role = role_service.create('admin', description='this is the lemur administrator role')
sys.stdout.write("[+] Created 'admin' role\n")
user_service.create("lemur", password1, 'lemur@nobody', True, None, [role])
user_service.create("lemur", password, 'lemur@nobody', True, None, [role])
sys.stdout.write("[+] Added a 'lemur' user and added it to the 'admin' role!\n")
else:
sys.stdout.write("[-] Default user has already been created, skipping...!\n")
sys.stdout.write("[+] Creating expiration email notifications!\n")
sys.stdout.write("[!] Using {recipients} as specified by LEMUR_SECURITY_TEAM_EMAIL for notifications\n")
sys.stdout.write("[!] Using {0} as specified by LEMUR_SECURITY_TEAM_EMAIL for notifications\n".format("LEMUR_SECURITY_TEAM_EMAIL"))
intervals = current_app.config.get("LEMUR_DEFAULT_EXPIRATION_NOTIFICATION_INTERVALS")
intervals = current_app.config.get("LEMUR_DEFAULT_EXPIRATION_NOTIFICATION_INTERVALS", [])
sys.stdout.write(
"[!] Creating {num} notifications for {intervals} days as specified by LEMUR_DEFAULT_EXPIRATION_NOTIFICATION_INTERVALS\n".format(
num=len(intervals),
@ -480,6 +500,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': current_app.config.get('LEMUR_DEFAULT_ORGANIZATION'),
'organizationalUnit': current_app.config.get('LEMUR_DEFAULT_ORGANIZATIONAL_UNIT'),
'country': current_app.config.get('LEMUR_DEFAULT_COUNTRY'),
'state': current_app.config.get('LEMUR_DEFAULT_STATE'),
'location': current_app.config.get('LEMUR_DEFAULT_LOCATION')
}
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 +732,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

@ -9,7 +9,6 @@
"""
import ssl
import socket
import arrow
@ -38,7 +37,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 +58,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
@ -106,8 +113,9 @@ def _get_domain_certificate(name):
try:
pub_key = ssl.get_server_certificate((name, 443))
return cert_service.find_duplicates(pub_key.strip())
except socket.gaierror as e:
except Exception as e:
current_app.logger.info(str(e))
return []
def _find_superseded(cert):
@ -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):
@ -96,7 +96,7 @@ def convert_date_to_utc_time(date):
:return:
"""
d = arrow.get(date)
return arrow.utcnow().replace(day=d.naive.day).replace(month=d.naive.month).replace(year=d.naive.year)\
return arrow.utcnow().replace(year=d.naive.year).replace(month=d.naive.month).replace(day=d.naive.day)\
.replace(microsecond=0)
@ -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, 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

@ -1,57 +0,0 @@
VERISIGN_INTERMEDIATE = """-----BEGIN CERTIFICATE-----
MIIFFTCCA/2gAwIBAgIQKC4nkXkzkuQo8iGnTsk3rjANBgkqhkiG9w0BAQsFADCB
yjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL
ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMTk5OSBWZXJp
U2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxW
ZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0
aG9yaXR5IC0gRzMwHhcNMTMxMDMxMDAwMDAwWhcNMjMxMDMwMjM1OTU5WjB+MQsw
CQYDVQQGEwJVUzEdMBsGA1UEChMUU3ltYW50ZWMgQ29ycG9yYXRpb24xHzAdBgNV
BAsTFlN5bWFudGVjIFRydXN0IE5ldHdvcmsxLzAtBgNVBAMTJlN5bWFudGVjIENs
YXNzIDMgU2VjdXJlIFNlcnZlciBDQSAtIEc0MIIBIjANBgkqhkiG9w0BAQEFAAOC
AQ8AMIIBCgKCAQEAstgFyhx0LbUXVjnFSlIJluhL2AzxaJ+aQihiw6UwU35VEYJb
A3oNL+F5BMm0lncZgQGUWfm893qZJ4Itt4PdWid/sgN6nFMl6UgfRk/InSn4vnlW
9vf92Tpo2otLgjNBEsPIPMzWlnqEIRoiBAMnF4scaGGTDw5RgDMdtLXO637QYqzu
s3sBdO9pNevK1T2p7peYyo2qRA4lmUoVlqTObQJUHypqJuIGOmNIrLRM0XWTUP8T
L9ba4cYY9Z/JJV3zADreJk20KQnNDz0jbxZKgRb78oMQw7jW2FUyPfG9D72MUpVK
Fpd6UiFjdS8W+cRmvvW1Cdj/JwDNRHxvSz+w9wIDAQABo4IBQDCCATwwHQYDVR0O
BBYEFF9gz2GQVd+EQxSKYCqy9Xr0QxjvMBIGA1UdEwEB/wQIMAYBAf8CAQAwawYD
VR0gBGQwYjBgBgpghkgBhvhFAQc2MFIwJgYIKwYBBQUHAgEWGmh0dHA6Ly93d3cu
c3ltYXV0aC5jb20vY3BzMCgGCCsGAQUFBwICMBwaGmh0dHA6Ly93d3cuc3ltYXV0
aC5jb20vcnBhMC8GA1UdHwQoMCYwJKAioCCGHmh0dHA6Ly9zLnN5bWNiLmNvbS9w
Y2EzLWczLmNybDAOBgNVHQ8BAf8EBAMCAQYwKQYDVR0RBCIwIKQeMBwxGjAYBgNV
BAMTEVN5bWFudGVjUEtJLTEtNTM0MC4GCCsGAQUFBwEBBCIwIDAeBggrBgEFBQcw
AYYSaHR0cDovL3Muc3ltY2QuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQBbF1K+1lZ7
9Pc0CUuWysf2IdBpgO/nmhnoJOJ/2S9h3RPrWmXk4WqQy04q6YoW51KN9kMbRwUN
gKOomv4p07wdKNWlStRxPA91xQtzPwBIZXkNq2oeJQzAAt5mrL1LBmuaV4oqgX5n
m7pSYHPEFfe7wVDJCKW6V0o6GxBzHOF7tpQDS65RsIJAOloknO4NWF2uuil6yjOe
soHCL47BJ89A8AShP/U3wsr8rFNtqVNpT+F2ZAwlgak3A/I5czTSwXx4GByoaxbn
5+CdKa/Y5Gk5eZVpuXtcXQGc1PfzSEUTZJXXCm5y2kMiJG8+WnDcwJLgLeVX+OQr
J+71/xuzAYN6
-----END CERTIFICATE-----
"""
VERISIGN_ROOT = """-----BEGIN CERTIFICATE-----
MIIEGjCCAwICEQCbfgZJoz5iudXukEhxKe9XMA0GCSqGSIb3DQEBBQUAMIHKMQsw
CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl
cmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu
LCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlT
aWduIENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3Jp
dHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQswCQYD
VQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlT
aWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJ
bmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWdu
IENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg
LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMu6nFL8eB8aHm8b
N3O9+MlrlBIwT/A2R/XQkQr1F8ilYcEWQE37imGQ5XYgwREGfassbqb1EUGO+i2t
KmFZpGcmTNDovFJbcCAEWNF6yaRpvIMXZK0Fi7zQWM6NjPXr8EJJC52XJ2cybuGu
kxUccLwgTS8Y3pKI6GyFVxEa6X7jJhFUokWWVYPKMIno3Nij7SqAP395ZVc+FSBm
CC+Vk7+qRy+oRpfwEuL+wgorUeZ25rdGt+INpsyow0xZVYnm6FNcHOqd8GIWC6fJ
Xwzw3sJ2zq/3avL6QaaiMxTJ5Xpj055iN9WFZZ4O5lMkdBteHRJTW8cs54NJOxWu
imi5V5cCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAERSWwauSCPc/L8my/uRan2Te
2yFPhpk0djZX3dAVL8WtfxUfN2JzPtTnX84XA9s1+ivbrmAJXx5fj267Cz3qWhMe
DGBvtcC1IyIuBwvLqXTLR7sdwdela8wv0kL9Sd2nic9TutoAWii/gt/4uhMdUIaC
/Y4wjylGsB49Ndo4YhYYSq3mtlFs3q9i6wHQHiT+eo8SGhJouPtmmRQURVyu565p
F4ErWjfJXir0xuKhXFSbplQAz/DxwceYMBo7Nhbbo27q/a2ywtrvAkcTisDxszGt
TxzhT5yvDwyd93gN2PQ1VoDat20Xj50egWTh/sVFuq1ruQp6Tk9LhO5L8X3dEQ==
-----END CERTIFICATE-----
"""

View File

@ -13,9 +13,8 @@ import xmltodict
from flask import current_app
from lemur.plugins.bases import IssuerPlugin
from lemur.plugins.bases import IssuerPlugin, SourcePlugin
from lemur.plugins import lemur_verisign as verisign
from lemur.plugins.lemur_verisign import constants
from lemur.common.utils import get_psuedo_random_string
@ -56,6 +55,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",
}
@ -131,7 +131,7 @@ class VerisignIssuerPlugin(IssuerPlugin):
version = verisign.VERSION
author = 'Kevin Glisson'
author_url = 'https://github.com/netflix/lemur'
author_url = 'https://github.com/netflix/lemur.git'
def __init__(self, *args, **kwargs):
self.session = requests.Session()
@ -146,7 +146,7 @@ class VerisignIssuerPlugin(IssuerPlugin):
:param issuer_options:
:return: :raise Exception:
"""
url = current_app.config.get("VERISIGN_URL") + '/enroll'
url = current_app.config.get("VERISIGN_URL") + '/rest/services/enroll'
data = process_options(issuer_options)
data['csr'] = csr
@ -155,7 +155,7 @@ class VerisignIssuerPlugin(IssuerPlugin):
response = self.session.post(url, data=data)
cert = handle_response(response.content)['Response']['Certificate']
return cert, constants.VERISIGN_INTERMEDIATE,
return cert, current_app.config.get('VERISIGN_INTERMEDIATE'),
@staticmethod
def create_authority(options):
@ -167,7 +167,7 @@ class VerisignIssuerPlugin(IssuerPlugin):
:return:
"""
role = {'username': '', 'password': '', 'name': 'verisign'}
return constants.VERISIGN_ROOT, "", [role]
return current_app.config.get('VERISIGN_ROOT'), "", [role]
def get_available_units(self):
"""
@ -176,6 +176,35 @@ class VerisignIssuerPlugin(IssuerPlugin):
:return:
"""
url = current_app.config.get("VERISIGN_URL") + '/getTokens'
url = current_app.config.get("VERISIGN_URL") + '/rest/services/getTokens'
response = self.session.post(url, headers={'content-type': 'application/x-www-form-urlencoded'})
return handle_response(response.content)['Response']['Order']
class VerisignSourcePlugin(SourcePlugin):
title = 'Verisign'
slug = 'verisign-source'
description = 'Allows for the polling of issued certificates from the VICE2.0 verisign API.'
version = verisign.VERSION
author = 'Kevin Glisson'
author_url = 'https://github.com/netflix/lemur.git'
def __init__(self, *args, **kwargs):
self.session = requests.Session()
self.session.cert = current_app.config.get('VERISIGN_PEM_PATH')
super(VerisignSourcePlugin, self).__init__(*args, **kwargs)
def get_certificates(self):
url = current_app.config.get('VERISIGN_URL') + '/reportingws'
end = arrow.now()
start = end.replace(years=-5)
data = {
'reportType': 'detail',
'startDate': start.format("MM/DD/YYYY"),
'endDate': end.format("MM/DD/YYYY"),
'structuredRecord': 'Y',
'certStatus': 'Valid',
}
current_app.logger.debug(data)
response = self.session.post(url, data=data)

View File

@ -0,0 +1 @@
from lemur.tests.conftest import * # noqa

View File

@ -0,0 +1,5 @@
def test_get_certificates(app):
from lemur.plugins.base import plugins
p = plugins.get('verisign-source')
p.get_certificates()

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',
@ -60,6 +60,15 @@ lemur.controller('datePickerController', function ($scope, $timeout){
};
});
lemur.service('DefaultService', function (LemurRestangular) {
var DefaultService = this;
DefaultService.get = function () {
return LemurRestangular.all('defaults').customGET().then(function (defaults) {
return defaults;
});
};
});
lemur.factory('LemurRestangular', function (Restangular, $location, $auth) {
return Restangular.withConfig(function (RestangularConfigurer) {
RestangularConfigurer.setBaseUrl('http://localhost:5000/api/1');

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

@ -30,6 +30,9 @@ angular.module('lemur')
.controller('AuthorityCreateController', function ($scope, $modalInstance, AuthorityService, LemurRestangular, RoleService, PluginService, WizardHandler) {
$scope.authority = LemurRestangular.restangularizeElement(null, {}, 'authorities');
// set the defaults
AuthorityService.getDefaults($scope.authority);
$scope.loading = false;
$scope.create = function (authority) {
WizardHandler.wizard().context.loading = true;

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

@ -16,7 +16,7 @@
Owner
</label>
<div class="col-sm-10">
<input type="email" name="ownerEmail" ng-model="authority.ownerEmail" placeholder="TeamDL@netflix.com" tooltip="This is the authorities team distribution list or the main point of contact for this authority" class="form-control" required/>
<input type="email" name="ownerEmail" ng-model="authority.ownerEmail" placeholder="TeamDL@example.com" tooltip="This is the authorities team distribution list or the main point of contact for this authority" class="form-control" required/>
<p ng-show="trackingForm.ownerEmail.$invalid && !trackingForm.ownerEmail.$pristine" class="help-block">You must enter an Certificate Authority owner</p>
</div>
</div>
@ -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

@ -56,7 +56,7 @@ angular.module('lemur')
});
return LemurRestangular.all('authorities');
})
.service('AuthorityService', function ($location, AuthorityApi, toaster) {
.service('AuthorityService', function ($location, AuthorityApi, DefaultService, toaster) {
var AuthorityService = this;
AuthorityService.findAuthorityByName = function (filterValue) {
return AuthorityApi.getList({'filter[name]': filterValue})
@ -117,6 +117,16 @@ angular.module('lemur')
});
};
AuthorityService.getDefaults = function (authority) {
return DefaultService.get().then(function (defaults) {
authority.caDN.country = defaults.country;
authority.caDN.state = defaults.state;
authority.caDN.location = defaults.location;
authority.caDN.organization = defaults.organization;
authority.caDN.organizationalUnit = defaults.organizationalUnit;
});
};
AuthorityService.getRoles = function (authority) {
return authority.getList('roles').then(function (roles) {
authority.roles = roles;

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

@ -4,7 +4,7 @@
</label>
<div class="col-sm-10">
<div class="input-group">
<input type="text" ng-model="certificate.selectedDestination" placeholder="AWS, TheSecret..."
<input type="text" ng-model="certificate.selectedDestination" placeholder="AWS..."
typeahead="destination.label for destination in destinationService.findDestinationsByName($viewValue)" typeahead-loading="loadingDestinations"
class="form-control input-md" typeahead-on-select="certificate.attachDestination($item)" typeahead-min-wait="50"
tooltip="Lemur can upload certificates to any pre-defined destination"

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

@ -10,7 +10,7 @@
Owner
</label>
<div class="col-sm-10">
<input type="email" name="owner" ng-model="certificate.owner" placeholder="owner@netflix.com"
<input type="email" name="owner" ng-model="certificate.owner" placeholder="owner@example.com"
class="form-control" required/>
<p ng-show="editForm.owner.$invalid && !editForm.owner.$pristine" class="help-block">Enter a valid

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

@ -6,7 +6,7 @@
Owner
</label>
<div class="col-sm-10">
<input type="email" name="ownerEmail" ng-model="certificate.owner" placeholder="TeamDL@netflix.com" tooltip="This is the certificates team distribution list or main point of contact" class="form-control" required/>
<input type="email" name="ownerEmail" ng-model="certificate.owner" placeholder="TeamDL@example.com" tooltip="This is the certificates team distribution list or main point of contact" class="form-control" required/>
<p ng-show="trackingForm.ownerEmail.$invalid && !trackingForm.ownerEmail.$pristine" class="help-block">You must enter an Certificate owner</p>
</div>
</div>
@ -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

@ -11,21 +11,30 @@
</label>
<div class="col-sm-10">
<input type="email" name="owner" ng-model="certificate.owner" placeholder="owner@netflix.com"
<input type="email" name="owner" ng-model="certificate.owner" placeholder="owner@example.com"
class="form-control" required/>
<p ng-show="uploadForm.owner.$invalid && !uploadForm.owner.$pristine" class="help-block">Enter a valid
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="the.example.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

@ -89,7 +89,7 @@ angular.module('lemur')
});
return LemurRestangular.all('certificates');
})
.service('CertificateService', function ($location, CertificateApi, LemurRestangular, toaster) {
.service('CertificateService', function ($location, CertificateApi, LemurRestangular, DefaultService, toaster) {
var CertificateService = this;
CertificateService.findCertificatesByName = function (filterValue) {
return CertificateApi.getList({'filter[name]': filterValue})
@ -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 DefaultService.get().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

@ -8,12 +8,5 @@
<div class="row featurette">
<div class="col-md-10">
<h2 class="featurette-heading">SSL In The Cloud <span class="text-muted">Encrypt it all </span></h2>
<p class="lead">The Security Operations team manages all of the SSL certificate generation at Netflix. This
portal was created to serve as both a self service application so that application owners can provision
their own certificates and to help enforce some key naming and security conventions, in order provide
Netflix with scalable and manageable SSL security.</p>
<p>See <a href="http://go/ssl">go/ssl</a> for more info.</p>
</div>
</div>

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

@ -1,33 +0,0 @@
"""
.. module: lemur.status.views
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
"""
import os
from flask import app, Blueprint, jsonify
from flask.ext.restful import Api
from lemur.auth.service import AuthenticatedResource
mod = Blueprint('status', __name__)
api = Api(mod)
class Status(AuthenticatedResource):
""" Defines the 'accounts' endpoint """
def __init__(self):
super(Status, self).__init__()
def get(self):
if not os.path.isdir(os.path.join(app.config.get("KEY_PATH"), "decrypted")):
return jsonify({
'environment': app.config.get('ENVIRONMENT'),
'status': 'degraded',
'message': "This Lemur instance is in a degraded state and is unable to issue certificates, please alert secops@netflix.com"})
else:
return jsonify({
'environment': app.config.get('ENVIRONMENT'),
'status': 'healthy',
'message': "This Lemur instance is healthy"})

View File

@ -8,6 +8,7 @@ from lemur.roles import service as role_service
def pytest_addoption(parser):
parser.addoption("--lemurconfig", help="override the default test config")
parser.addoption("--runslow", action="store_true", help="run slow tests")
@ -29,12 +30,15 @@ def pytest_runtest_makereport(item, call):
@pytest.yield_fixture(scope="session")
def app():
def app(request):
"""
Creates a new Flask application for a test duration.
Uses application factory `create_app`.
"""
_app = create_app(os.path.dirname(os.path.realpath(__file__)) + '/conf.py')
if request.config.getoption('--lemurconfig'):
_app = create_app(request.config.getoption('--lemurconfig'))
else:
_app = create_app(os.path.dirname(os.path.realpath(__file__)) + '/conf.py')
ctx = _app.app_context()
ctx.push()

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

@ -10,3 +10,6 @@ exclude = .tox,.git,*/migrations/*,lemur/static/*,docs/*
[wheel]
universal = 1
[metadata]
description-file = README.rst

View File

@ -9,14 +9,16 @@ Is an SSL management and orchestration tool.
"""
from __future__ import absolute_import
import json
import os.path
import datetime
from distutils import log
from distutils.core import Command
from setuptools.command.develop import develop
from setuptools.command.install import install
from setuptools.command.sdist import sdist
from setuptools import setup
from setuptools import setup, find_packages
from subprocess import check_output
ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__)))
@ -39,7 +41,7 @@ install_requires = [
'six==1.9.0',
'gunicorn==19.3.0',
'pycrypto==2.6.1',
'cryptography>=1.0dev',
'cryptography==1.0.1',
'pyopenssl==0.15.1',
'pyjwt==1.0.1',
'xmltodict==0.9.2',
@ -72,7 +74,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():
@ -87,9 +89,18 @@ class DevelopWithBuildStatic(develop):
class SdistWithBuildStatic(sdist):
def make_distribution(self):
def make_release_tree(self, *a, **kw):
dist_path = self.distribution.get_fullname()
sdist.make_release_tree(self, *a, **kw)
self.reinitialize_command('build_static', work_path=dist_path)
self.run_command('build_static')
return sdist.make_distribution(self)
with open(os.path.join(dist_path, 'lemur-package.json'), 'w') as fp:
json.dump({
'createdAt': datetime.datetime.utcnow().isoformat() + 'Z',
}, fp)
class BuildStatic(Command):
@ -100,7 +111,8 @@ class BuildStatic(Command):
pass
def run(self):
log.info("running [npm install --quiet]")
log.info("running [npm install --quiet] in {0}".format(ROOT))
check_output(['npm', 'install', '--quiet'], cwd=ROOT)
log.info("running [gulp build]")
@ -110,11 +122,14 @@ class BuildStatic(Command):
setup(
name='lemur',
version='0.1',
version='0.1.3',
author='Kevin Glisson',
author_email='kglisson@netflix.com',
long_description=open('README.rst').read(),
packages=['lemur'],
url='https://github.com/netflix/lemur',
download_url='https://github.com/Netflix/lemur/archive/0.1.3.tar.gz',
description='Certificate management and orchestration service',
long_description=open(os.path.join(ROOT, 'README.rst')).read(),
packages=find_packages(),
include_package_data=True,
zip_safe=False,
install_requires=install_requires,
@ -125,10 +140,8 @@ setup(
},
cmdclass={
'build_static': BuildStatic,
'develop': DevelopWithBuildStatic,
'sdist': SdistWithBuildStatic,
'install': SmartInstall
},
entry_points={
'console_scripts': [