Compare commits

..

150 Commits
0.1.3 ... 0.2

Author SHA1 Message Date
837bfc3aa5 Merge pull request #167 from forkd/master
minor changes in quickstart guide
2015-12-02 14:51:07 -08:00
5ba1176f14 Merge pull request #168 from kevgliss/changelog
Changelog
2015-12-02 14:50:42 -08:00
f08649b02d Updating change log 2015-12-02 14:50:14 -08:00
edbe5a254b minor changes in quickstart guide 2015-12-02 14:34:22 +00:00
cfedb30628 Merge pull request #166 from kevgliss/template
Making the notification email template cleaner
2015-12-01 18:16:54 -08:00
aa18b88a61 Making the notification email template cleaner 2015-12-01 17:13:43 -08:00
05962e71e3 Merge branch 'forkd-master' 2015-12-01 13:03:23 -08:00
bafc3d0082 minor adjustments 2015-12-01 13:03:08 -08:00
308f1b44c3 Merge branch 'master' of git://github.com/forkd/lemur into forkd-master 2015-12-01 13:01:54 -08:00
cd17789529 Removing unneeded import 2015-12-01 11:51:39 -08:00
bf988d89c4 updated quickstart guide 2015-12-01 19:03:17 +00:00
b1e842ae47 Merge pull request #162 from kevgliss/160-startup
Closes #160
2015-12-01 10:08:03 -08:00
fcc3c35ae2 Merge pull request #163 from kevgliss/docs
Updating docs
2015-12-01 10:07:37 -08:00
e2524e43cf adding exports 2015-12-01 09:44:41 -08:00
6aac2d62be Closes #160 2015-12-01 09:40:27 -08:00
95e2636f23 Updating docs 2015-12-01 09:15:53 -08:00
7565492bb9 Merge pull request #161 from kevgliss/docs
adding version.py
2015-12-01 08:34:25 -08:00
89f7f12f92 adding version.py 2015-12-01 08:33:37 -08:00
cdd15ca818 Merge pull request #159 from kevgliss/migrations
Adding current migration files.
2015-11-30 15:44:14 -08:00
11f2d88b16 Adding current migration files. 2015-11-30 15:43:38 -08:00
8066d540e0 Merge pull request #158 from kevgliss/fix
Fix
2015-11-30 14:27:23 -08:00
c3091a7346 Adding missing files. 2015-11-30 14:08:17 -08:00
9cadebcd50 adding example requests 2015-11-30 13:51:27 -08:00
3e54eb7520 adding closed 2015-11-30 12:51:28 -08:00
068f19c895 Merge pull request #156 from kevgliss/bump
adding automatic versioning
2015-11-30 12:09:33 -08:00
3651cce542 adding automatic versioning 2015-11-30 10:43:41 -08:00
9e0b9d9dda Merge pull request #154 from kevgliss/125-output-plugins
Initial work on #125
2015-11-30 10:31:25 -08:00
f194e2a1be Linting 2015-11-30 10:24:53 -08:00
f56c6f2836 Downgrading req to pass tests. 2015-11-30 10:10:50 -08:00
ec896461a7 Adding final touches to #125 2015-11-30 09:47:36 -08:00
8eeed821d3 Adding UI elements 2015-11-27 13:27:14 -08:00
80c1689b24 Merge pull request #155 from Joe8Bit/master
Fix requires.io badge
2015-11-27 10:21:33 -08:00
7c29b566be Update README.rst
Update README.rst to point to correct requires.io badge
2015-11-27 14:37:23 +00:00
920d595c12 Initial work on #125 2015-11-25 14:54:08 -08:00
5d7174b2a7 Merge pull request #153 from Netflix/requires-io-master
[requires.io] dependency update on master branch
2015-11-25 14:44:31 -08:00
3c60f47e3f [requires.io] dependency update 2015-11-25 14:18:01 -08:00
c4abc59673 [requires.io] dependency update 2015-11-25 14:18:00 -08:00
ff4cdd82ee Merge pull request #152 from kevgliss/144-search
Closes #144
2015-11-24 16:13:05 -08:00
1c6e9caa40 Closes #144 2015-11-24 16:07:44 -08:00
07ec04ddc6 Merge pull request #151 from kevgliss/122-replacement
Closes #122
2015-11-24 15:01:39 -08:00
d6b3f5af81 Closes #122 2015-11-24 14:53:22 -08:00
ce1fe9321c Merge pull request #150 from kevgliss/121-validation
121 validation
2015-11-23 16:48:40 -08:00
2c88e4e3ba fixing conflict 2015-11-23 16:42:14 -08:00
fed37c9dc0 Fixes badge 2015-11-23 16:41:31 -08:00
e14eefdc31 Added the ability to find an authority even if a user only types the name in and does not select it. 2015-11-23 16:41:31 -08:00
2525d369d4 Merge pull request #149 from kevgliss/requirements2
Updating requirements
2015-11-23 15:59:58 -08:00
0600481a67 Updating requirements 2015-11-23 15:41:11 -08:00
f0324e4755 Merge pull request #148 from kevgliss/120-error-length
Closes #120
2015-11-23 15:25:30 -08:00
00f0f957c0 Lint again 2015-11-23 15:13:18 -08:00
9c652d784d Merge pull request #143 from kevgliss/requirements
Updating requirements
2015-11-23 14:59:31 -08:00
eb2fa74661 Fixing test 2015-11-23 14:49:05 -08:00
146c599deb Lint cleanup 2015-11-23 14:47:34 -08:00
574c4033ab Closes #120 2015-11-23 14:30:23 -08:00
9f122eec18 Merge pull request #145 from kevgliss/140-permalink
Closes #140
2015-11-23 12:02:49 -08:00
eb0f6a04d8 Closes #140 2015-11-23 10:43:07 -08:00
c7230befe4 Merge pull request #142 from kevgliss/139-description
Closes #139
2015-11-23 10:27:04 -08:00
9a316ae1a9 Updating requirements 2015-11-23 10:23:23 -08:00
df4364714e Closes #139 2015-11-23 09:53:55 -08:00
a1cd2b39eb Merge pull request #135 from mikegrima/travis-tweak
Removed un-needed build step.
2015-10-29 13:34:50 -07:00
8a7a15a361 Merge pull request #132 from cloughrm/master
Use american english for consistency
2015-10-29 13:34:31 -07:00
2cdeecb6e0 Merge pull request #134 from Netflix/monkeysecurity-patch-1
Removing hyphen from in-active.
2015-10-29 13:33:49 -07:00
638e4a5ac1 Removed un-needed build step.
I hope they have size 'M'.
2015-10-29 13:02:58 -07:00
93b4ef5f17 Removing hyphen from in-active.
`inactive` is a word.  in-active is ... something else.
2015-10-29 11:54:00 -07:00
26d490f74a Merge pull request #133 from belladzaster/grammer
Fixing grammar
2015-10-28 21:53:19 -07:00
01a1190524 Fixing grammer 2015-10-28 19:55:08 -07:00
2073090628 Use american english for consistency 2015-10-28 19:39:10 -07:00
6d00cb208d Merge pull request #131 from belladzaster/master
Fixing Typos
2015-10-28 19:32:08 -07:00
13b9bf687d Fixing Typos 2015-10-28 18:24:31 -07:00
56f7da34d7 Merge pull request #129 from kevgliss/version
Version bump and needed documentation.
2015-10-26 11:59:16 -07:00
0f34440b64 Merge pull request #130 from kevgliss/createUser
Fixing issue where roles were not added correctly to user.
2015-10-26 11:58:36 -07:00
bbcc7cca4e Merge pull request #128 from kevgliss/fernet_migration
[WIP] Adding aes - fernet migration
2015-10-26 10:59:41 -07:00
0453afcb0e Fixing issuer where roles were not added correctly to user. 2015-10-26 10:59:20 -07:00
cafecd1e19 Version bump and needed documentation. 2015-10-24 11:18:27 -07:00
4b968a9474 Adding aes - fernet migration 2015-10-23 16:47:17 -07:00
9244945e69 Merge pull request #127 from monkeysecurity/update_cryptography_for_el_capitan
Updating cryptography to 1.0.2 for el capitan
2015-10-21 18:54:43 -07:00
78819c1733 Updating cryptography to 1.0.2 for el capitan 2015-10-21 18:45:50 -07:00
394e18f76e Merge pull request #123 from rpicard/master
Use MultiFernet for encryption
2015-10-14 15:50:33 -07:00
40eb950e94 Use MultiFernet for encryption
Facilitates key rotation and uses more secure encryption than what
sqlalchemy-utils does.

Fixes #117 and #119.
2015-10-13 16:58:58 -07:00
90636a5329 Merge pull request #118 from rpicard/master
Fix a handful of typos in documentation
2015-10-06 15:30:09 -07:00
2fc6d4cd21 Fix a handful of typos in documentation
As I was reading through the docs I made note of grammar issues and
typos I saw. Not a huge deal but might as well fix what I noticed.
2015-10-06 15:05:05 -07:00
b20bdf3c4e Merge pull request #116 from kevgliss/algo
Adding the ability to track a certificates signing key algorithm
2015-10-06 13:25:34 -07:00
a20726a301 Fixing python 3.x syntax error 2015-10-06 13:11:24 -07:00
39727a1c9f Fixing tests 2015-10-06 13:00:06 -07:00
168f46a436 Adding the ability to track a certificates signing key algorithm 2015-10-06 12:51:59 -07:00
4ec07a6dc7 Merge pull request #115 from kevgliss/destinations
Fixes destination stat
2015-10-06 09:48:03 -07:00
798a6295ee Fixes destination stat 2015-10-06 09:43:31 -07:00
73cb8da8c1 Merge pull request #114 from kevgliss/clipboard
Add clipboard functionality
2015-10-05 16:14:16 -07:00
3167ce9785 removing unneeded dep 2015-10-05 16:08:12 -07:00
63b7b71b49 adding clipboard functionality 2015-10-05 16:06:56 -07:00
9965af9ccd fixing links, and adding zeroclipboard 2015-10-05 09:48:52 -07:00
ba5d2c925a Merge pull request #113 from kevgliss/perma
Adding ui router and perma links to certificates and authorities
2015-10-05 09:30:52 -07:00
867be09e29 more double quotes 2015-10-05 09:24:11 -07:00
8362a92898 fixing double quotes 2015-10-05 09:19:14 -07:00
162482dbc4 Adding ui router and perma links to certificates and authorities 2015-10-05 09:00:51 -07:00
c0f14db5bb Merge pull request #112 from kevgliss/uitweaks
UI tweaks
2015-10-02 16:31:50 -07:00
3c561914c6 removing angular tour 2015-10-02 16:29:59 -07:00
34c6f1bf4d Merge pull request #111 from kevgliss/105
Closes #105
2015-10-02 16:26:02 -07:00
2187898494 adding copy and a better profile picture for non-sso users 2015-10-02 15:36:50 -07:00
d4bc6ae7a1 Fixes #105 2015-10-02 13:46:13 -07:00
81cdb15353 Merge pull request #108 from kevgliss/description
description should be optional
2015-09-29 16:56:58 -07:00
5cfa9d4bc5 description should be optional 2015-09-29 16:37:32 -07:00
92da453233 Merge pull request #107 from waffle-iron/master
waffle.io Badge
2015-09-29 13:39:36 -07:00
2aedfedbd3 add waffle.io badge 2015-09-29 13:50:33 -06:00
64c9b11c09 Merge pull request #106 from kevgliss/release
version bump
2015-09-28 14:55:24 -07:00
5f87c87751 version bump 2015-09-28 14:54:58 -07:00
70f9022aae Merge pull request #104 from kevgliss/guide
Adding connections in user guides
2015-09-24 16:28:52 -07:00
43683fe554 changing readme language 2015-09-24 16:09:34 -07:00
002de6f5e4 adding docker Link 2015-09-24 16:03:15 -07:00
63a388236e adding a link to our techblog 2015-09-24 14:36:14 -07:00
9560791002 Merge pull request #99 from pandragoq/patch-1
Update index.rst
2015-09-24 14:28:06 -07:00
ed93b5a2c5 SSL 2015-09-24 09:36:11 -07:00
21e4cc9f4d Adding connections in user guides 2015-09-24 09:21:08 -07:00
73e628cbdf Merge pull request #103 from kevgliss/required
Marking fields as required
2015-09-24 08:52:46 -07:00
7ebd0bf5d4 making fields required 2015-09-24 08:42:31 -07:00
3f1902e0fe Merge pull request #100 from ivuk/fix-typo
Fix typos in docs/administration/index.rst
2015-09-23 16:44:49 -07:00
3e546eaa21 Fix typos in docs/administration/index.rst 2015-09-23 21:00:52 +02:00
e70deb155d Update index.rst
Right package for postgres is postgresql in ubuntu.
2015-09-22 16:57:53 -07:00
4f289c790b Merge pull request #98 from stacybird/grammar_fix
Fix grammar in index.rst
2015-09-22 15:48:37 -07:00
c15f525167 Fix grammar in index.rst 2015-09-22 15:33:37 -07:00
bcbf642122 Merge pull request #96 from kevgliss/install
clearing up docs based on feedback
2015-09-22 14:54:11 -07:00
1559727f2d Making make build the static assets 2015-09-22 14:49:37 -07:00
a596793a9a clearing up docs based on feedback 2015-09-22 14:18:38 -07:00
862bf3f619 Merge pull request #94 from kevgliss/notifications
Notifications
2015-09-22 13:37:51 -07:00
83a86c06a4 Merge pull request #93 from pandragoq/patch-1
Update index.rst
2015-09-22 13:37:40 -07:00
06a69c09a0 Fixing a bug where notifications associated during certificate creation would not be respected. 2015-09-22 13:01:05 -07:00
6a24e88d9a removing pip install instructions until available 2015-09-22 10:22:12 -07:00
be6a5b859e adding notification example 2015-09-22 09:46:54 -07:00
2444191bf2 Update index.rst
Typo on nginx spelling
2015-09-21 17:43:56 -07:00
9226b1eb4a Merge pull request #92 from konklone/patch-1
Rename SSL to TLS in many places
2015-09-21 15:25:00 -07:00
3f53629175 Re 2015-09-21 18:16:40 -04:00
baef329a4d Rename SSL to TLS 2015-09-21 18:16:19 -04:00
b103fc7bfb Rename SSL to TLS 2015-09-21 18:16:04 -04:00
a3385bd2ac Rename SSL to TLS 2015-09-21 18:15:25 -04:00
7cb50c654b Rename SSL to TLS 2015-09-21 18:15:06 -04:00
52ba538037 Rename SSL to TLS 2015-09-21 18:14:12 -04:00
0a0460529f Merge pull request #89 from kevgliss/cleanup
Cleaning up unneed/unused files
2015-09-20 10:21:04 -07:00
fc0a884d5f Cleaning up unneed/unused files 2015-09-20 09:49:16 -07:00
dbbea29e75 Merge pull request #88 from kevgliss/requirements
adding additional requirements so rtd can build the documation correctly
2015-09-19 11:32:57 -07:00
bcd0aae8c6 adding additional requirements so rtd can build the documation correctly 2015-09-19 11:31:31 -07:00
50d3e6aff2 Merge pull request #87 from kevgliss/typo
fixing typo
2015-09-19 10:25:52 -07:00
1d45926122 fixing typo 2015-09-19 10:24:56 -07:00
45626c947c Merge pull request #86 from kevgliss/docs
More documentation fixes
2015-09-19 10:21:56 -07:00
d7ca6d4327 More documentation fixes 2015-09-19 10:12:12 -07:00
6411bd56e9 Merge pull request #85 from kevgliss/documentation
Documentation
2015-09-19 09:48:25 -07:00
1486e7b8f6 adding information about sub commands 2015-09-19 09:41:50 -07:00
e73f2bcb2b setting default theme 2015-09-19 09:38:39 -07:00
a412569ff7 aligning doc version with tagged version 2015-09-19 09:34:48 -07:00
387194d651 Merge pull request #84 from kevgliss/docs
Adding flask sphinx auto-docs
2015-09-18 17:29:17 -07:00
13d0359041 Adding flask sphinx auto-docs 2015-09-18 17:28:48 -07:00
365d927efb Update README.rst 2015-09-18 16:28:45 -07:00
89 changed files with 2663 additions and 1666 deletions

3
.gitattributes vendored
View File

@ -1 +1,2 @@
* text=auto * text=auto
version.py export-subst

View File

@ -23,9 +23,6 @@ env:
global: global:
- PIP_DOWNLOAD_CACHE=".pip_download_cache" - PIP_DOWNLOAD_CACHE=".pip_download_cache"
install:
- make dev-postgres
before_script: before_script:
- psql -c "create database lemur;" -U postgres - psql -c "create database lemur;" -U postgres
- psql -c "create user lemur with password 'lemur;'" -U postgres - psql -c "create user lemur with password 'lemur;'" -U postgres
@ -36,4 +33,4 @@ script:
notifications: notifications:
email: email:
kglisson@netflix.com kglisson@netflix.com

View File

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

32
CHANGELOG.rst Normal file
View File

@ -0,0 +1,32 @@
Changelog
=========
0.2.1 - `master` _
~~~~~~~~~~~~~~~~~~
.. note:: This version not yet released and is under active development
0.2.0 - 2015-12-02
~~~~~~~~~~~~~~~~~~~
* Closed #120 - Error messages not displaying long enough
* Closed #121 - Certificate create form should not be valid until a Certificate Authority object is available
* Closed #122 - Certificate API should allow for the specification of preceding certificates
You can now target a certificate(s) for replacement. When specified the replaced certificate will be marked as
'inactive'. This means that there will be no notifications for that certificate.
* Closed #139 - SubCA autogenerated descriptions for their certs are incorrect
* Closed #140 - Permalink does not change with filtering
* Closed #144 - Should be able to search certificates by domains covered, included wildcards
* Closed #165 - Cleaned up expiration notification template
* Closed #160 - Cleaned up quickstart documentation (thanks forkd!)
* Closed #144 - Now able to search by all domains in a given certificate, not just by common name
0.1.5 - 2015-10-26
~~~~~~~~~~~~~~~~~~~
* **SECURITY ISSUE**: Switched from use a AES static key to Fernet encryption.
Affects all versions prior to 0.1.5. If upgrading this will require a data migration.
see: `Upgrading Lemur <https://lemur.readthedocs.com/adminstration#UpgradingLemur>`_

View File

View File

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

View File

@ -9,6 +9,8 @@ develop: update-submodules setup-git
pip install -e . pip install -e .
pip install "file://`pwd`#egg=lemur[dev]" pip install "file://`pwd`#egg=lemur[dev]"
pip install "file://`pwd`#egg=lemur[tests]" pip install "file://`pwd`#egg=lemur[tests]"
node_modules/.bin/gulp build
node_modules/.bin/gulp package
@echo "" @echo ""
dev-docs: dev-docs:

View File

@ -13,17 +13,28 @@ Lemur
:target: https://lemur.readthedocs.org :target: https://lemur.readthedocs.org
:alt: Latest Docs :alt: Latest Docs
.. image:: https://magnum.travis-ci.com/Netflix/lemur.svg?branch=master .. image:: https://travis-ci.org/Netflix/lemur.svg
:target: https://magnum.travis-ci.com/Netflix/lemur :target: https://travis-ci.org/Netflix/lemur
.. image:: https://requires.io/github/Netflix/lemur/requirements.svg?branch=master
:target: https://requires.io/github/Netflix/lemur/requirements/?branch=master
:alt: Requirements Status
Lemur manages SSL certificate creation. It provides a central portal for developers to issuer their own SSL certificates with 'sane' defaults. .. image:: https://badge.waffle.io/Netflix/lemur.png?label=ready&title=Ready
:target: https://waffle.io/Netflix/lemur
:alt: 'Stories in Ready'
Lemur manages TLS certificate creation. While not able to issue certificates itself, Lemur acts as a broker between CAs
and environments providing a central portal for developers to issue TLS certificates with 'sane' defaults.
It works on CPython 2.7, 3.3, 3.4. We deploy on Ubuntu and develop on OS X. It works on CPython 2.7, 3.3, 3.4. We deploy on Ubuntu and develop on OS X.
Project resources Project resources
================= =================
- `Lemur Blog Post <http://techblog.netflix.com/2015/09/introducing-lemur.html>`_
- `Documentation <http://lemur.readthedocs.org/>`_ - `Documentation <http://lemur.readthedocs.org/>`_
- `Source code <https://github.com/netflix/lemur>`_ - `Source code <https://github.com/netflix/lemur>`_
- `Issue tracker <https://github.com/netflix/lemur/issues>`_ - `Issue tracker <https://github.com/netflix/lemur/issues>`_
- `Docker <https://github.com/Netflix/lemur-docker>`_

View File

@ -29,11 +29,16 @@
"angular-ui-switch": "~0.1.0", "angular-ui-switch": "~0.1.0",
"angular-chart.js": "~0.7.1", "angular-chart.js": "~0.7.1",
"satellizer": "~0.9.4", "satellizer": "~0.9.4",
"angularjs-toaster": "~0.4.14" "angularjs-toaster": "~0.4.14",
"ngletteravatar": "~3.0.1",
"angular-ui-router": "~0.2.15",
"angular-clipboard": "~1.1.1",
"angular-file-saver": "~1.0.1"
}, },
"devDependencies": { "devDependencies": {
"angular-mocks": "~1.3", "angular-mocks": "~1.3",
"angular-scenario": "~1.3" "angular-scenario": "~1.3",
"ngletteravatar": "~3.0.1"
}, },
"resolutions": { "resolutions": {
"bootstrap": "~3.3.1", "bootstrap": "~3.3.1",

View File

@ -2,7 +2,7 @@ Configuration
============= =============
.. warning:: .. warning::
There are many secrets that Lemur uses that must be protected. All of these options are set via the Lemur configruation There are many secrets that Lemur uses that must be protected. All of these options are set via the Lemur configuration
file. It is highly advised that you do not store your secrets in this file! Lemur provides functions file. It is highly advised that you do not store your secrets in this file! Lemur provides functions
that allow you to encrypt files at rest and decrypt them when it's time for deployment. See :ref:`Credential Management <CredentialManagement>` that allow you to encrypt files at rest and decrypt them when it's time for deployment. See :ref:`Credential Management <CredentialManagement>`
for more information. for more information.
@ -72,7 +72,7 @@ Basic Configuration
.. data:: LEMUR_TOKEN_SECRET .. data:: LEMUR_TOKEN_SECRET
:noindex: :noindex:
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. The TOKEN_SECRET is the secret used to create JWT tokens that are given out to users. This should be securely generated and kept private.
:: ::
@ -87,23 +87,29 @@ Basic Configuration
>>> secret_key = secret_key + ''.join(random.choice(string.digits) for x in range(6)) >>> secret_key = secret_key + ''.join(random.choice(string.digits) for x in range(6))
.. data:: LEMUR_ENCRYPTION_KEY .. data:: LEMUR_ENCRYPTION_KEYS
:noindex: :noindex:
The LEMUR_ENCRYPTION_KEY is used to encrypt data at rest within Lemur's database. Without this key Lemur will refuse The LEMUR_ENCRYPTION_KEYS is used to encrypt data at rest within Lemur's database. Without a key Lemur will refuse
to start. to start. Multiple keys can be provided to facilitate key rotation. The first key in the list is used for
encryption and all keys are tried for decryption until one works. Each key must be 32 URL safe base-64 encoded bytes.
See `LEMUR_TOKEN_SECRET` for methods of secure secret generation. Running lemur create_config will securely generate a key for your configuration file.
If you would like to generate your own, we recommend the following method:
>>> import os
>>> import base64
>>> base64.urlsafe_b64encode(os.urandom(32))
:: ::
LEMUR_ENCRYPTION_KEY = 'supersupersecret' LEMUR_ENCRYPTION_KEYS = ['1YeftooSbxCiX2zo8m1lXtpvQjy27smZcUUaGmffhMY=', 'LAfQt6yrkLqOK5lwpvQcT4jf2zdeTQJV1uYeh9coT5s=']
Certificate Default Options Certificate Default Options
--------------------------- ---------------------------
Lemur allows you to find tune your certificates to your organization. The following defaults are presented in the UI Lemur allows you to fine 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. and are used when Lemur creates the CSR for your certificates.
@ -151,13 +157,13 @@ Notification Options
-------------------- --------------------
Lemur currently has very basic support for notifications. Currently only expiration notifications are supported. Actual notification 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 is handled by the notification plugins that you have configured. Lemur ships with the 'Email' notification that allows expiration emails
to be sent to subscribers. to be sent to subscribers.
Templates for expiration emails are located under `lemur/plugins/lemur_email/templates` and can be modified for 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. 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 Certificates marked as inactive 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 according to the `LEMUR_DEFAULT_EXPIRATION_NOTIFICATION_INTERVALS` or 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. 30, 15, 2 days before expiration if no intervals are set.
@ -209,19 +215,20 @@ Lemur supports sending certification expiration notifications through SES and SM
Authority Options Authority Options
----------------- -----------------
Authorities will each have their own configuration options. There are currently two plugins bundled with Lemur, Authorities will each have their own configuration options. There is currently just one plugin bundled with Lemur,
Verisign/Symantec and CloudCA Verisign/Symantec. Additional plugins may define additional options. Refer to the plugin's own documentation
for those plugins.
.. data:: VERISIGN_URL .. data:: VERISIGN_URL
:noindex: :noindex:
This is the url for the verisign API This is the url for the Verisign API
.. data:: VERISIGN_PEM_PATH .. data:: VERISIGN_PEM_PATH
:noindex: :noindex:
This is the path to the mutual SSL certificate used for communicating with Verisign This is the path to the mutual TLS certificate used for communicating with Verisign
.. data:: VERISIGN_FIRST_NAME .. data:: VERISIGN_FIRST_NAME
@ -253,26 +260,9 @@ Verisign/Symantec and CloudCA
This is the root to be used for your CA chain This is the root to be used for your CA chain
.. data:: CLOUDCA_URL
:noindex:
This is the URL for CLoudCA API
.. data:: CLOUDCA_PEM_PATH
:noindex:
This is the path to the mutual SSL Certificate use for communicating with CLOUDCA
.. data:: CLOUDCA_BUNDLE
:noindex:
This is the path to the CLOUDCA certificate bundle
Authentication Authentication
-------------- --------------
Lemur currently supports Basic Authentication and Ping OAuth2 out of the box, additional flows can be added relatively easily 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. 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>`_ For more information about how to use social logins, see: `Satellizer <https://github.com/sahat/satellizer>`_
@ -311,9 +301,9 @@ For more information about how to use social logins, see: `Satellizer <https://g
AWS Plugin Configuration AWS Plugin Configuration
======================== ========================
In order for Lemur to manage it's own account and other accounts we must ensure it has the correct AWS permissions. In order for Lemur to manage its own account and other accounts we must ensure it has the correct AWS permissions.
.. note:: AWS usage is completely optional. Lemur can upload, find and manage SSL certificates in AWS. But is not required to do so. .. note:: AWS usage is completely optional. Lemur can upload, find and manage TLS certificates in AWS. But is not required to do so.
Setting up IAM roles Setting up IAM roles
-------------------- --------------------
@ -326,7 +316,7 @@ Lemur uses to STS to talk to different accounts. For managing one account this i
LemurInstanceProfile is the IAM role you will launch your instance with. It actually has almost no rights. In fact it should really only be able to use STS to assume role to the Lemur role. LemurInstanceProfile is the IAM role you will launch your instance with. It actually has almost no rights. In fact it should really only be able to use STS to assume role to the Lemur role.
Here is are example polices for the LemurInstanceProfile: Here are example policies for the LemurInstanceProfile:
SES-SendEmail SES-SendEmail
@ -364,11 +354,11 @@ STS-AssumeRole
Next we will create the the Lemur IAM role. Lemur Next we will create the the Lemur IAM role.
..note:: .. 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. 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: Here is an example policy for Lemur:
@ -486,7 +476,7 @@ The configuration::
LEMUR_MAIL = 'lemur.example.com' LEMUR_MAIL = 'lemur.example.com'
Will be sender of all notifications, so ensure that it is verified with AWS. Will be the sender of all notifications, so ensure that it is verified with AWS.
SES if the default notification gateway and will be used unless SMTP settings are configured in the application configuration SES if the default notification gateway and will be used unless SMTP settings are configured in the application configuration
settings. settings.
@ -495,7 +485,7 @@ Upgrading Lemur
=============== ===============
Lemur provides an easy way to upgrade between versions. Simply download the newest Lemur provides an easy way to upgrade between versions. Simply download the newest
version of Lemur from pypi and then apply any schema cahnges with the following command. version of Lemur from pypi and then apply any schema changes with the following command.
.. code-block:: bash .. code-block:: bash
@ -568,29 +558,11 @@ All commands default to `~/.lemur/lemur.conf.py` if a configuration is not speci
lemur db upgrade lemur db upgrade
.. data:: create_user
Creates new users within Lemur.
::
lemur create_user -u jim -e jim@example.com
.. data:: create_role
Creates new roles within Lemur.
::
lemur create_role -n example -d "a new role"
.. data:: check_revoked .. data:: check_revoked
Traverses every certificate that Lemur is aware of and attempts to understand it's validity. Traverses every certificate that Lemur is aware of and attempts to understand its validity.
It utilizes both OCSP and CRL. If Lemur is unable to come to a conclusion about a certificates It utilizes both OCSP and CRL. If Lemur is unable to come to a conclusion about a certificates
validity it's status is marked 'unknown' validity its status is marked 'unknown'
.. data:: sync .. data:: sync
@ -610,21 +582,41 @@ All commands default to `~/.lemur/lemur.conf.py` if a configuration is not speci
lemur sync -list lemur sync -list
Sub-commands
------------
Lemur includes several sub-commands for interacting with Lemur such as creating new users, creating new roles and even
issuing certificates.
The best way to discover these commands is by using the built in help pages
::
lemur --help
and to get help on sub-commands
::
lemur certificates --help
Identity and Access Management Identity and Access Management
============================== ==============================
Lemur uses a Role Based Access Control (RBAC) mechanism to control which users have access to which resources. When a Lemur uses a Role Based Access Control (RBAC) mechanism to control which users have access to which resources. When a
user is first created in Lemur the can be assigned one or more roles. These roles are typically dynamically created user is first created in Lemur they can be assigned one or more roles. These roles are typically dynamically created
depending on a external identity provider (Google, LDAP, etc.,) or are hardcoded within Lemur and associated with special depending on a external identity provider (Google, LDAP, etc.,) or are hardcoded within Lemur and associated with special
meaning. meaning.
Within Lemur there are three main permissions: AdminPermission, CreatorPermission, OwnerPermission. Sub-permissions such Within Lemur there are three main permissions: AdminPermission, CreatorPermission, OwnerPermission. Sub-permissions such
as ViewPrivateKeyPermission are compositions of these three main Permissions. as ViewPrivateKeyPermission are compositions of these three main Permissions.
Lets take a look at how these permissions used: Lets take a look at how these permissions are used:
Each `Authority` has a set of roles associated with it. If a user is also associated with the same roles Each `Authority` has a set of roles associated with it. If a user is also associated with the same roles
that the `Authority` is associated with it Lemur allows that user to user/view/update that `Authority`. that the `Authority` is associated with, Lemur allows that user to user/view/update that `Authority`.
This RBAC is also used when determining which users can access which certificate private key. Lemur's current permission 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 structure is setup such that if the user is a `Creator` or `Owner` of a given certificate they are allow to view that
@ -635,3 +627,34 @@ These permissions are applied to the user upon login and refreshed on every requ
.. seealso:: .. seealso::
`Flask-Principal <https://pythonhosted.org/Flask-Principal>`_ `Flask-Principal <https://pythonhosted.org/Flask-Principal>`_
Upgrading Lemur
===============
To upgrade Lemur to the newest release you will need to ensure you have the lastest code and have run any needed
database migrations.
To get the latest code from github run
::
cd <lemur-source-directory>
git pull -t <version>
python setup.py develop
.. note::
It's important to grab the latest release by specifying the release tag. This tags denote stable versions of Lemur.
If you want to try the bleeding edge version of Lemur you can by using the master branch.
After you have the latest version of the Lemur code base you must run any needed database migrations. To run migrations
::
cd <lemur-source-directory>/lemur
lemur db upgrade
This will ensure that any needed tables or columns are created or destroyed.

View File

@ -1,2 +1 @@
Change Log .. include:: ../CHANGELOG.rst
==========

View File

@ -11,7 +11,6 @@
# #
# All configuration values have a default; values that are commented out # All configuration values have a default; values that are commented out
# serve to show the default. # serve to show the default.
import sys import sys
import os import os
@ -54,10 +53,12 @@ copyright = u'2015, Netflix Inc.'
# |version| and |release|, also used in various other places throughout the # |version| and |release|, also used in various other places throughout the
# built documents. # built documents.
# #
# The short X.Y version. base_dir = os.path.join(os.path.dirname(__file__), os.pardir)
version = '0.1' about = {}
# The full version, including alpha/beta/rc tags. with open(os.path.join(base_dir, "lemur", "__about__.py")) as f:
release = '0.1.1' exec(f.read(), about)
version = release = about["__version__"]
# The language for content autogenerated by Sphinx. Refer to documentation # The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages. # for a list of supported languages.
@ -102,7 +103,7 @@ pygments_style = 'sphinx'
# The theme to use for HTML and HTML Help pages. See the documentation for # The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes. # a list of builtin themes.
html_theme = 'alabaster' html_theme = 'default'
# Theme options are theme-specific and customize the look and feel of a theme # Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the # further. For a list of options available for each theme, see the

View File

@ -1,20 +0,0 @@
lemur_cloudca Package
=====================
:mod:`lemur_cloudca` Package
----------------------------
.. automodule:: lemur.plugins.lemur_cloudca
:noindex:
:members:
:undoc-members:
:show-inheritance:
:mod:`plugin` Module
--------------------
.. automodule:: lemur.plugins.lemur_cloudca.plugin
:noindex:
:members:
:undoc-members:
:show-inheritance:

View File

@ -8,7 +8,7 @@ Several interfaces exist for extending Lemur:
* Source (lemur.plugins.base.source) * Source (lemur.plugins.base.source)
* Notification (lemur.plugins.base.notification) * Notification (lemur.plugins.base.notification)
Each interface has its own function that will need to be defined in order for Each interface has its own functions that will need to be defined in order for
your plugin to work correctly. See :ref:`Plugin Interfaces <PluginInterfaces>` for details. your plugin to work correctly. See :ref:`Plugin Interfaces <PluginInterfaces>` for details.
@ -91,7 +91,7 @@ Issuer
Issuer plugins are used when you have an external service that creates certificates or authorities. Issuer plugins are used when you have an external service that creates certificates or authorities.
In the simple case the third party only issues certificates (Verisign, DigiCert, etc.). In the simple case the third party only issues certificates (Verisign, DigiCert, etc.).
If you have a third party or internal service that creates authorities (CloudCA, EJBCA, etc.), Lemur has you covered, If you have a third party or internal service that creates authorities (EJBCA, etc.), Lemur has you covered,
it can treat any issuer plugin as both a source of creating new certificates as well as new authorities. it can treat any issuer plugin as both a source of creating new certificates as well as new authorities.

53
docs/doing-a-release.rst Normal file
View File

@ -0,0 +1,53 @@
Doing a release
===============
Doing a release of ``lemur`` requires a few steps.
Bumping the version number
--------------------------
The next step in doing a release is bumping the version number in the
software.
* Update the version number in ``lemur/__about__.py``.
* Set the release date in the :doc:`/changelog`.
* Do a commit indicating this.
* Send a pull request with this.
* Wait for it to be merged.
Performing the release
----------------------
The commit that merged the version number bump is now the official release
commit for this release. You will need to have ``gpg`` installed and a ``gpg``
key in order to do a release. Once this has happened:
* Run ``invoke release {version}``.
The release should now be available on PyPI and a tag should be available in
the repository.
Verifying the release
---------------------
You should verify that ``pip install lemur`` works correctly:
.. code-block:: pycon
>>> import lemur
>>> lemur.__version__
'...'
Verify that this is the version you just released.
Post-release tasks
------------------
* Update the version number to the next major (e.g. ``0.5.dev1``) in
``lemur/__about__.py`` and
* Add new :doc:`/changelog` entry with next version and note that it is under
active development
* Send a pull request with these items
* Check for any outstanding code undergoing a deprecation cycle by looking in
``lemur.utils`` for ``DeprecatedIn**`` definitions. If any exist open
a ticket to increment them for the next release.

View File

@ -4,8 +4,8 @@ Frequently Asked Questions
Common Problems Common Problems
--------------- ---------------
In my startup logs I see *'Aborting... Lemur cannot locate db encryption key, is LEMUR_ENCRYPTION_KEY set?'* In my startup logs I see *'Aborting... Lemur cannot locate db encryption key, is LEMUR_ENCRYPTION_KEYS set?'*
You likely have not correctly configured **LEMUR_ENCRYPTION_KEY**. See You likely have not correctly configured **LEMUR_ENCRYPTION_KEYS**. See
:doc:`administration/index` for more information. :doc:`administration/index` for more information.
@ -14,6 +14,27 @@ I am seeing Lemur's javascript load in my browser but not the CSS.
:doc:`production/index` for example configurations. :doc:`production/index` for example configurations.
After installing Lemur I am unable to login
Ensure that you are trying to login with the credentials you entered during `lemur init`. These are separate
from the postgres database credentials.
Running 'lemur db upgrade' seems stuck.
Most likely, the upgrade is stuck because an existing query on the database is holding onto a lock that the
migration needs.
To resolve, login to your lemur database and run:
SELECT * FROM pg_locks l INNER JOIN pg_stat_activity s ON (l.pid = s.pid) WHERE waiting AND NOT granted;
This will give you a list of queries that are currently waiting to be executed. From there attempt to idenity the PID
of the query blocking the migration. Once found execute:
select pg_terminate_backend(<blocking-pid>);
See `<http://stackoverflow.com/questions/22896496/alembic-migration-stuck-with-postgresql>`_ for more.
How do I How do I
-------- --------

View File

@ -1,261 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
sphinx-autopackage-script
This script parses a directory tree looking for python modules and packages and
creates ReST files appropriately to create code documentation with Sphinx.
It also creates a modules index (named modules.<suffix>).
"""
# Copyright 2008 Société des arts technologiques (SAT), http://www.sat.qc.ca/
# Copyright 2010 Thomas Waldmann <tw AT waldmann-edv DOT de>
# All rights reserved.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import optparse
# automodule options
OPTIONS = ['members',
'undoc-members',
# 'inherited-members', # disabled because there's a bug in sphinx
'show-inheritance',
]
INIT = '__init__.py'
def makename(package, module):
"""Join package and module with a dot."""
# Both package and module can be None/empty.
if package:
name = package
if module:
name += '.' + module
else:
name = module
return name
def write_file(name, text, opts):
"""Write the output file for module/package <name>."""
if opts.dryrun:
return
fname = os.path.join(opts.destdir, "%s.%s" % (name, opts.suffix))
if not opts.force and os.path.isfile(fname):
print 'File %s already exists, skipping.' % fname
else:
print 'Creating file %s.' % fname
f = open(fname, 'w')
f.write(text)
f.close()
def format_heading(level, text):
"""Create a heading of <level> [1, 2 or 3 supported]."""
underlining = ['=', '-', '~', ][level-1] * len(text)
return '%s\n%s\n\n' % (text, underlining)
def format_directive(module, package=None):
"""Create the automodule directive and add the options."""
directive = '.. automodule:: %s\n' % makename(package, module)
for option in OPTIONS:
directive += ' :%s:\n' % option
return directive
def create_module_file(package, module, opts):
"""Build the text of the file and write the file."""
text = format_heading(1, '%s Module' % module)
text += format_heading(2, ':mod:`%s` Module' % module)
text += format_directive(module, package)
write_file(makename(package, module), text, opts)
def create_package_file(root, master_package, subroot, py_files, opts, subs):
"""Build the text of the file and write the file."""
package = os.path.split(root)[-1]
text = format_heading(1, '%s Package' % package)
# add each package's module
for py_file in py_files:
if shall_skip(os.path.join(root, py_file)):
continue
is_package = py_file == INIT
py_file = os.path.splitext(py_file)[0]
py_path = makename(subroot, py_file)
if is_package:
heading = ':mod:`%s` Package' % package
else:
heading = ':mod:`%s` Module' % py_file
text += format_heading(2, heading)
text += format_directive(is_package and subroot or py_path, master_package)
text += '\n'
# build a list of directories that are packages (they contain an INIT file)
subs = [sub for sub in subs if os.path.isfile(os.path.join(root, sub, INIT))]
# if there are some package directories, add a TOC for theses subpackages
if subs:
text += format_heading(2, 'Subpackages')
text += '.. toctree::\n\n'
for sub in subs:
text += ' %s.%s\n' % (makename(master_package, subroot), sub)
text += '\n'
write_file(makename(master_package, subroot), text, opts)
def create_modules_toc_file(master_package, modules, opts, name='modules'):
"""
Create the module's index.
"""
text = format_heading(1, '%s Modules' % opts.header)
text += '.. toctree::\n'
text += ' :maxdepth: %s\n\n' % opts.maxdepth
modules.sort()
prev_module = ''
for module in modules:
# look if the module is a subpackage and, if yes, ignore it
if module.startswith(prev_module + '.'):
continue
prev_module = module
text += ' %s\n' % module
write_file(name, text, opts)
def shall_skip(module):
"""
Check if we want to skip this module.
"""
# skip it, if there is nothing (or just \n or \r\n) in the file
return os.path.getsize(module) < 3
def recurse_tree(path, excludes, opts):
"""
Look for every file in the directory tree and create the corresponding
ReST files.
"""
# use absolute path for root, as relative paths like '../../foo' cause
# 'if "/." in root ...' to filter out *all* modules otherwise
path = os.path.abspath(path)
# check if the base directory is a package and get is name
if INIT in os.listdir(path):
package_name = path.split(os.path.sep)[-1]
else:
package_name = None
toc = []
tree = os.walk(path, False)
for root, subs, files in tree:
# keep only the Python script files
py_files = sorted([f for f in files if os.path.splitext(f)[1] == '.py'])
if INIT in py_files:
py_files.remove(INIT)
py_files.insert(0, INIT)
# remove hidden ('.') and private ('_') directories
subs = sorted([sub for sub in subs if sub[0] not in ['.', '_']])
# check if there are valid files to process
# TODO: could add check for windows hidden files
if "/." in root or "/_" in root \
or not py_files \
or is_excluded(root, excludes):
continue
if INIT in py_files:
# we are in package ...
if (# ... with subpackage(s)
subs
or
# ... with some module(s)
len(py_files) > 1
or
# ... with a not-to-be-skipped INIT file
not shall_skip(os.path.join(root, INIT))
):
subroot = root[len(path):].lstrip(os.path.sep).replace(os.path.sep, '.')
create_package_file(root, package_name, subroot, py_files, opts, subs)
toc.append(makename(package_name, subroot))
elif root == path:
# if we are at the root level, we don't require it to be a package
for py_file in py_files:
if not shall_skip(os.path.join(path, py_file)):
module = os.path.splitext(py_file)[0]
create_module_file(package_name, module, opts)
toc.append(makename(package_name, module))
# create the module's index
if not opts.notoc:
create_modules_toc_file(package_name, toc, opts)
def normalize_excludes(rootpath, excludes):
"""
Normalize the excluded directory list:
* must be either an absolute path or start with rootpath,
* otherwise it is joined with rootpath
* with trailing slash
"""
sep = os.path.sep
f_excludes = []
for exclude in excludes:
if not os.path.isabs(exclude) and not exclude.startswith(rootpath):
exclude = os.path.join(rootpath, exclude)
if not exclude.endswith(sep):
exclude += sep
f_excludes.append(exclude)
return f_excludes
def is_excluded(root, excludes):
"""
Check if the directory is in the exclude list.
Note: by having trailing slashes, we avoid common prefix issues, like
e.g. an exlude "foo" also accidentally excluding "foobar".
"""
sep = os.path.sep
if not root.endswith(sep):
root += sep
for exclude in excludes:
if root.startswith(exclude):
return True
return False
def main():
"""
Parse and check the command line arguments.
"""
parser = optparse.OptionParser(usage="""usage: %prog [options] <package path> [exclude paths, ...]
Note: By default this script will not overwrite already created files.""")
parser.add_option("-n", "--doc-header", action="store", dest="header", help="Documentation Header (default=Project)", default="Project")
parser.add_option("-d", "--dest-dir", action="store", dest="destdir", help="Output destination directory", default="")
parser.add_option("-s", "--suffix", action="store", dest="suffix", help="module suffix (default=txt)", default="txt")
parser.add_option("-m", "--maxdepth", action="store", dest="maxdepth", help="Maximum depth of submodules to show in the TOC (default=4)", type="int", default=4)
parser.add_option("-r", "--dry-run", action="store_true", dest="dryrun", help="Run the script without creating the files")
parser.add_option("-f", "--force", action="store_true", dest="force", help="Overwrite all the files")
parser.add_option("-t", "--no-toc", action="store_true", dest="notoc", help="Don't create the table of content file")
(opts, args) = parser.parse_args()
if not args:
parser.error("package path is required.")
else:
rootpath, excludes = args[0], args[1:]
if os.path.isdir(rootpath):
# check if the output destination is a valid directory
if opts.destdir and os.path.isdir(opts.destdir):
excludes = normalize_excludes(rootpath, excludes)
recurse_tree(rootpath, excludes, opts)
else:
print '%s is not a valid output destination directory.' % opts.destdir
else:
print '%s is not a valid directory.' % rootpath
if __name__ == '__main__':
main()

View File

@ -3,6 +3,67 @@ User Guide
These guides are quick tutorials on how to perform basic tasks in Lemur. These guides are quick tutorials on how to perform basic tasks in Lemur.
Create a New Authority
~~~~~~~~~~~~~~~~~~~~~~
Before Lemur can issue certificates you must configure the authority you wish use. Lemur itself does
not issue certificates, it relies on external CAs and the plugins associated with those CAs to create the certificate
that Lemur can then manage.
.. 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.
Create a New Certificate
~~~~~~~~~~~~~~~~~~~~~~~~
.. figure:: create.png
In the certificate table select "Create"
.. figure:: create_certificate.png
Enter an 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.
Create a New User Create a New User
~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~
.. figure:: settings.png .. figure:: settings.png
@ -40,56 +101,3 @@ Create a New Role
users to your new role. users to your new role.
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.
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.

View File

@ -1,8 +1,8 @@
Lemur Lemur
===== =====
Lemur is a SSL management service. It attempts to help track and create certificates. By removing common issues with Lemur is a TLS management service. It attempts to help track and create certificates. By removing common issues with
CSR creation it gives normal developers 'sane' SSL defaults and helps security teams push SSL usage throughout an organization. CSR creation it gives normal developers 'sane' TLS defaults and helps security teams push TLS usage throughout an organization.
Installation Installation
------------ ------------

View File

@ -6,21 +6,22 @@ There are several steps needed to make Lemur production ready. Here we focus on
Basics Basics
====== ======
Because of the sensitivity of the information stored and maintain by Lemur it is important that you follow standard host hardening practices: Because of the sensitivity of the information stored and maintained by Lemur it is important that you follow standard host hardening practices:
- Run Lemur with a limited user - Run Lemur with a limited user
- Disabled any unneeded service - Disabled any unneeded services
- Enable remote logging - Enable remote logging
- Restrict access to host
.. _CredentialManagement: .. _CredentialManagement:
Credential Management Credential Management
--------------------- ---------------------
Lemur often contains credentials such as mutual SSL keys that are used to communicate with third party resources and for encrypting stored secrets. Lemur comes with the ability Lemur often contains credentials such as mutual TLS keys or API tokens that are used to communicate with third party resources and for encrypting stored secrets. Lemur comes with the ability
to automatically encrypt these keys such that your keys not be in clear text. to automatically encrypt these keys such that your keys not be in clear text.
The keys are located within lemur/keys and broken down by environment The keys are located within lemur/keys and broken down by environment.
To utilize this ability use the following commands: To utilize this ability use the following commands:
@ -30,7 +31,7 @@ and
``lemur unlock`` ``lemur unlock``
If you choose to use this feature ensure that the KEY are decrypted before Lemur starts as it will have trouble communicating with the database otherwise. If you choose to use this feature ensure that the keys are decrypted before Lemur starts as it will have trouble communicating with the database otherwise.
Entropy Entropy
------- -------
@ -56,8 +57,8 @@ For additional information about OpenSSL entropy issues:
- `Managing and Understanding Entropy Usage <https://www.blackhat.com/docs/us-15/materials/us-15-Potter-Understanding-And-Managing-Entropy-Usage.pdf>`_ - `Managing and Understanding Entropy Usage <https://www.blackhat.com/docs/us-15/materials/us-15-Potter-Understanding-And-Managing-Entropy-Usage.pdf>`_
SSL TLS/SSL
==== =======
Nginx Nginx
----- -----
@ -71,7 +72,7 @@ Nginx is a very popular choice to serve a Python project:
Nginx doesn't run any Python process, it only serves requests from outside to Nginx doesn't run any Python process, it only serves requests from outside to
the Python server. the Python server.
Therefor there are two steps: Therefore there are two steps:
- Run the Python process. - Run the Python process.
- Run Nginx. - Run Nginx.
@ -89,7 +90,7 @@ You must create a Nginx configuration file for Lemur. On GNU/Linux, they usually
go into /etc/nginx/conf.d/. Name it lemur.conf. go into /etc/nginx/conf.d/. Name it lemur.conf.
`proxy_pass` just passes the external request to the Python process. `proxy_pass` just passes the external request to the Python process.
The port much match the one used by the 0bin process of course. The port must match the one used by the Lemur process of course.
You can make some adjustments to get a better user experience:: You can make some adjustments to get a better user experience::
@ -127,10 +128,10 @@ You can make some adjustments to get a better user experience::
} }
This makes Nginx serve the favicon and static files which is is much better at than python. This makes Nginx serve the favicon and static files which it is much better at than python.
It is highly recommended that you deploy SSL when deploying Lemur. This may be obvious given Lemur's purpose but the It is highly recommended that you deploy TLS when deploying Lemur. This may be obvious given Lemur's purpose but the
sensitive nature of Lemur and what it controls makes this essential. This is a sample config for Lemur that also terminates SSL:: sensitive nature of Lemur and what it controls makes this essential. This is a sample config for Lemur that also terminates TLS::
server_tokens off; server_tokens off;
add_header X-Frame-Options DENY; add_header X-Frame-Options DENY;
@ -218,7 +219,7 @@ An example apache config::
... ...
</VirtualHost> </VirtualHost>
Also included in the configurations above are several best practices when it comes to deploying SSL. Things like enabling Also included in the configurations above are several best practices when it comes to deploying TLS. Things like enabling
HSTS, disabling vulnerable ciphers are all good ideas when it comes to deploying Lemur into a production environment. HSTS, disabling vulnerable ciphers are all good ideas when it comes to deploying Lemur into a production environment.
.. note:: .. note::
@ -270,7 +271,7 @@ Create a configuration file named supervisor.ini::
The 4 first entries are just boiler plate to get you started, you can copy The 4 first entries are just boiler plate to get you started, you can copy
them verbatim. them verbatim.
The last one define one (you can have many) process supervisor should manage. The last one defines one (you can have many) process supervisor should manage.
It means it will run the command:: It means it will run the command::
@ -292,6 +293,6 @@ Then you can manage the process by running::
supervisorctl -c /path/to/supervisor.ini supervisorctl -c /path/to/supervisor.ini
It will start a shell from were you can start/stop/restart the service It will start a shell from which you can start/stop/restart the service.
You can read all errors that might occurs from /tmp/lemur.log. You can read all errors that might occur from /tmp/lemur.log.

View File

@ -1,98 +1,93 @@
Quickstart 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 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.
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>`_. Pressed for time? See the Lemur docker file on `Github <https://github.com/Netflix/lemur-docker>`_.
Dependencies Dependencies
------------ ------------
Some basic prerequisites which you'll need in order to run Lemur: Some basic prerequisites which you'll need in order to run Lemur:
* A UNIX-based operating system. We test on Ubuntu, develop on OS X * A UNIX-based operating system (we test on Ubuntu, develop on OS X)
* Python 2.7 * Python 2.7
* PostgreSQL * PostgreSQL
* Ngnix * Nginx
.. note:: Lemur was built with in AWS in mind. This means that things such as databases (RDS), mail (SES), and SSL (ELB), .. note:: Lemur was built with in AWS in mind. This means that things such as databases (RDS), mail (SES), and TLS (ELB), 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.
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.
Installing Build Dependencies
-----------------------------
If installing Lemur on a bare Ubuntu OS you will need to grab the following packages so that Lemur can correctly build it's dependencies:
.. code-block:: bash
$ sudo apt-get update
$ sudo apt-get install install nodejs-legacy python-pip python-dev libpq-dev build-essential libssl-dev libffi-dev nginx git supervisor npm postgresql
.. note:: PostgreSQL is only required if your database is going to be on the same host as the webserver. npm is needed if you're installing Lemur from the source (e.g., from git).
Now, install Python ``virtualenv`` package:
.. code-block:: bash
$ sudo pip install -U virtualenv
Setting up an Environment Setting up an Environment
------------------------- -------------------------
The first thing you'll need is the Python ``virtualenv`` package. You probably already In this guide, Lemur will be installed in ``/www``, so you need to create that structure first:
have this, but if not, you can install it with::
pip install -U virtualenv
Once that's done, choose a location for the environment, and create it with the ``virtualenv``
command. For our guide, we're going to choose ``/www/lemur/``::
virtualenv /www/lemur/
Finally, activate your virtualenv::
source /www/lemur/bin/activate
.. note:: Activating the environment adjusts your PATH, so that things like pip now
install into the virtualenv by default.
Installing build dependencies
-----------------------------
If installing Lemur on truely bare Ubuntu OS you will need to grab the following packages so that Lemur can correctly build it's
dependencies::
$ sudo apt-get update
$ sudo apt-get install nodejs-legacy python-pip libpq-dev python-dev build-essential libssl-dev libffi-dev nginx git supervisor
And optionally if your database is going to be on the same host as the webserver::
$ sudo apt-get install postgres
Installing Lemur
----------------
Once you've got the environment setup, you can install Lemur and all its dependencies with
the same command you used to grab virtualenv::
pip install -U lemur
Once everything is installed, you should be able to execute the Lemur CLI, via ``lemur``, and get something
like the following:
.. code-block:: bash .. code-block:: bash
$ lemur $ sudo mkdir /www
usage: lemur [--config=/path/to/settings.py] [command] [options] $ cd /www
Clone Lemur inside the just created directory and give yourself write permission (we assume ``lemur`` is the user):
.. code-block:: bash
$ sudo git clone https://github.com/Netflix/lemur
$ sudo chown -R lemur lemur/
Create the virtual environment, activate it and enter the Lemur's directory:
.. code-block:: bash
$ virtualenv lemur
$ source /www/lemur/bin/activate
$ cd lemur
.. note:: Activating the environment adjusts your PATH, so that things like pip now install into the virtualenv by default.
Installing from Source Installing from Source
~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~
If you're installing the Lemur source (e.g. from git), you'll also need to install **npm**. Once your system is prepared, ensure that you are in the virtualenv:
Once your system is prepared, symlink your source into the virtualenv: .. code-block:: bash
$ which python
And then run:
.. code-block:: bash .. code-block:: bash
$ make develop $ make develop
.. Note:: This command will install npm dependencies as well as compile static assets. .. note:: This command will install npm dependencies as well as compile static assets.
Creating a configuration Creating a configuration
------------------------ ------------------------
Before we run Lemur we must create a valid configuration file for it. Before we run Lemur, we must create a valid configuration file for it. The Lemur command line interface comes with a simple command to get you up and running quickly.
The Lemur cli comes with a simple command to get you up and running quickly.
Simply run: Simply run:
@ -100,84 +95,85 @@ Simply run:
$ lemur create_config $ lemur create_config
.. Note:: This command will create a default configuration under `~/.lemur/lemur.conf.py` you .. note:: This command will create a default configuration under ``~/.lemur/lemur.conf.py`` you can specify this location by passing the ``config_path`` parameter to the ``create_config`` command.
can specify this location by passing the `config_path` parameter to the `create_config` command.
You can specify ``-c`` or ``--config`` to any Lemur command to specify the current environment you are working in. Lemur will also look under the environmental variable ``LEMUR_CONF`` should that be easier to setup in your environment.
You can specify `-c` or `--config` to any Lemur command to specify the current environment
you are working in. Lemur will also look under the environmental variable `LEMUR_CONF` should
that be easier to setup in your environment.
Update your configuration Update your configuration
------------------------- -------------------------
Once created you will need to update the configuration file with information about your environment, Once created, you will need to update the configuration file with information about your environment, such as which database to talk to, where keys are stored etc.
such as which database to talk to, where keys are stores etc..
.. note:: If you are unfamiliar with with the SQLALCHEMY_DATABASE_URI string it can be broken up like so:
``postgresql://userame:password@<database-fqdn>:<database-port>/<database-name>``
.. Note:: If you are unfamiliar with with the SQLALCHEMY_DATABASE_URI string it can be broken up like so:
postgresql://userame:password@databasefqdn:databaseport/databasename
Setup Postgres Setup Postgres
-------------- --------------
For production a dedicated database is recommended, for this guide we will assume postgres has been installed and is on For production, a dedicated database is recommended, for this guide we will assume postgres has been installed and is on the same machine that Lemur is installed on.
the same machine that Lemur is installed on.
First, set a password for the postgres user. For this guide, we will use **lemur** as an example but you should use the database password generated for by Lemur:: First, set a password for the postgres user. For this guide, we will use ``lemur`` as an example but you should use the database password generated by Lemur:
$ sudo -u postgres psql postgres .. code-block:: bash
# \password postgres
Enter new password: lemur
Enter it again: lemur
Type CTRL-D to exit psql once you have changed the password. $ sudo -u postgres psql postgres
# \password postgres
Enter new password: lemur
Enter it again: lemur
Next, we will create our a new database:: Once successful, type CTRL-D to exit the Postgres shell.
$ sudo -u postgres createdb lemur Next, we will create our new database:
.. code-block:: bash
$ sudo -u postgres createdb lemur
.. _InitializingLemur: .. _InitializingLemur:
Set a password for lemur user inside Postgres:
.. code-block:: bash
$ sudo -u postgres psql postgres
\password lemur
Enter new password: lemur
Enter it again: lemur
Again, enter CTRL-D to exit the Postgres shell.
Initializing Lemur Initializing Lemur
------------------ ------------------
Lemur provides a helpful command that will initialize your database for you. It creates a default user (lemur) that is Lemur provides a helpful command that will initialize your database for you. It creates a default user (``lemur``) that is used by Lemur to help associate certificates that do not currently have an owner. This is most commonly the case when Lemur has discovered certificates from a third party source. This is also a default user that can be used to administer Lemur.
used by Lemur to help associate certificates that do not currently have an owner. This is most commonly the case when
Lemur has discovered certificates from a third party source. This is also a default user that can be used to
administer Lemur.
In addition to create a new User, Lemur also creates a few default email notifications. These notifications are based In addition to creating a new user, Lemur also creates a few default email notifications. These notifications are based on a few configuration options such as ``LEMUR_SECURITY_TEAM_EMAIL``. They basically guarantee that every certificate within Lemur will send one expiration notification to the security team.
on a few configuration options such as `LEMUR_SECURITY_TEAM_EMAIL` they basically garentee that every cerificate within
Lemur will send one expiration notification to the security team.
Additional notifications can be created through the UI or API. Additional notifications can be created through the UI or API. See :ref:`Creating Notifications <CreatingNotifications>` and :ref:`Command Line Interface <CommandLineInterface>` for details.
See :ref:`Creating Notifications <CreatingNotifications>` and :ref:`Command Line Interface <CommandLineInterface>` for details.
**Make note of the password used as this will be used during first login to the Lemur UI** **Make note of the password used as this will be used during first login to the Lemur UI.**
.. code-block:: bash .. code-block:: bash
$ lemur db init $ lemur db init
.. code-block:: bash
$ lemur init $ lemur init
.. note:: It is recommended that once the 'lemur' user is created that you create individual users for every day access. .. note:: It is recommended that once the ``lemur`` user is created that you create individual users for every day access. There is currently no way for a user to self enroll for Lemur access, they must have an administrator create an account for them or be enrolled automatically through SSO. This can be done through the CLI or UI. See :ref:`Creating Users <CreatingUsers>` and :ref:`Command Line Interface <CommandLineInterface>` for details.
There is currently no way for a user to self enroll for Lemur access, they must have an administrator create an account
for them or be enrolled automatically through SSO. This can be done through the CLI or UI.
See :ref:`Creating Users <CreatingUsers>` and :ref:`Command Line Interface <CommandLineInterface>` for details
Setup a Reverse Proxy Setup a Reverse Proxy
--------------------- ---------------------
By default, Lemur runs on port 5000. Even if you change this, under normal conditions you won't be able to bind to By default, Lemur runs on port 8000. Even if you change this, under normal conditions you won't be able to bind to port 80. To get around this (and to avoid running Lemur as a privileged user, which you shouldn't), we need setup a simple web proxy. There are many different web servers you can use for this, we like and recommend Nginx.
port 80. To get around this (and to avoid running Lemur as a privileged user, which you shouldn't), we recommend
you setup a simple web proxy.
Proxying with Nginx Proxying with Nginx
~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~
You'll use the builtin HttpProxyModule within Nginx to handle proxying You'll use the builtin ``HttpProxyModule`` within Nginx to handle proxying. Edit the ``/etc/nginx/sites-available/default`` file according to the lines below
:: ::
@ -190,23 +186,29 @@ You'll use the builtin HttpProxyModule within Nginx to handle proxying
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
} }
location / { location / {
root /www/lemur/lemur/static/dist; root /www/lemur/lemur/static/dist;
include mime.types; include mime.types;
index index.html; index index.html;
} }
See :doc:`../production/index` for more details on using Nginx. .. note:: See :doc:`../production/index` for more details on using Nginx.
After making these changes, restart Nginx service to apply them:
.. code-block:: bash
$ sudo service nginx restart
Starting the Web Service Starting the Web Service
------------------------ ------------------------
Lemur provides a built-in webserver (powered by gunicorn and eventlet) to get you off the ground quickly. Lemur provides a built-in web server (powered by gunicorn and eventlet) to get you off the ground quickly.
To start the webserver, you simply use ``lemur start``. If you opted to use an alternative configuration path To start the web server, you simply use ``lemur start``. If you opted to use an alternative configuration path
you can pass that via the --config option. you can pass that via the ``--config`` option.
.. note:: .. note::
You can login with the default user created during :ref:`Initializing Lemur <InitializingLemur>` or any other You can login with the default user created during :ref:`Initializing Lemur <InitializingLemur>` or any other
@ -214,23 +216,23 @@ you can pass that via the --config option.
:: ::
# Lemur's server runs on port 5000 by default. Make sure your client reflects # Lemur's server runs on port 8000 by default. Make sure your client reflects
# the correct host and port! # the correct host and port!
lemur --config=/etc/lemur.conf.py start -b 127.0.0.1:5000 lemur --config=/etc/lemur.conf.py start -b 127.0.0.1:8000
You should now be able to test the web service by visiting ``http://localhost:5000/``.
You should now be able to test the web service by visiting `http://localhost:5000/`.
Running Lemur as a Service Running Lemur as a Service
--------------------------- --------------------------
We recommend using whatever software you are most familiar with for managing Lemur processes. One option is `Supervisor <http://supervisord.org/>`_.
We recommend using whatever software you are most familiar with for managing Lemur processes. One option is
`Supervisor <http://supervisord.org/>`_.
Configure ``supervisord`` Configure ``supervisord``
~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~
Configuring Supervisor couldn't be more simple. Just point it to the ``lemur`` executable in your virtualenv's bin/ Configuring Supervisor couldn't be more simple. Just point it to the ``lemur`` executable in your virtualenv's ``bin/`` folder and you're good to go.
folder and you're good to go.
:: ::
@ -245,11 +247,11 @@ folder and you're good to go.
See :ref:`Using Supervisor <UsingSupervisor>` for more details on using Supervisor. See :ref:`Using Supervisor <UsingSupervisor>` for more details on using Supervisor.
Syncing Syncing
------- -------
Lemur uses periodic sync tasks to make sure it is up-to-date with it's environment. As always things can change outside Lemur uses periodic sync tasks to make sure it is up-to-date with its environment. As always, things can change outside of Lemur, but we do our best to reconcile those changes, for example, using Cron:
of Lemur, but we do our best to reconcile those changes.
.. code-block:: bash .. code-block:: bash
@ -257,30 +259,30 @@ of Lemur, but we do our best to reconcile those changes.
* 3 * * * lemur sync --all * 3 * * * lemur sync --all
* 3 * * * lemur check_revoked * 3 * * * lemur check_revoked
Additional Utilities Additional Utilities
-------------------- --------------------
If you're familiar with Python you'll quickly find yourself at home, and even more so if you've used Flask. The If you're familiar with Python you'll quickly find yourself at home, and even more so if you've used Flask. The ``lemur`` command is just a simple wrapper around Flask's ``manage.py``, which means you get all of the power and flexibility that goes with it.
``lemur`` command is just a simple wrapper around Flask's ``manage.py``, which means you get all of the
power and flexibility that goes with it. Some of the features which you'll likely find useful are listed below.
Some of those which you'll likely find useful are:
lock lock
~~~~ ~~~~
Encrypts sensitive key material - This is most useful for storing encrypted secrets in source code. Encrypts sensitive key material - this is most useful for storing encrypted secrets in source code.
unlock unlock
~~~~~~ ~~~~~~
Decrypts sensitive key material - Used to decrypt the secrets stored in source during deployment. Decrypts sensitive key material - used to decrypt the secrets stored in source during deployment.
What's Next? What's Next?
------------ ------------
The above gets you going, but for production there are several different security considerations to take into account, Get familiar with how Lemur works by reviewing the :doc:`../guide/index`. When you're ready see :doc:`../production/index` for more details on how to configure Lemur for production.
remember Lemur is handling sensitive data and security is imperative.
See :doc:`../production/index` for more details on how to configure Lemur for production. The above just gets you going, but for production there are several different security considerations to take into account. Remember, Lemur is handling sensitive data and security is imperative.

View File

@ -2,4 +2,28 @@ Jinja2>=2.3
Pygments>=1.2 Pygments>=1.2
Sphinx>=1.3 Sphinx>=1.3
docutils>=0.7 docutils>=0.7
markupsafe markupsafe
sphinxcontrib-httpdomain
Flask==0.10.1
Flask-RESTful==0.3.3
Flask-SQLAlchemy==2.1
Flask-Script==2.0.5
Flask-Migrate==1.6.0
Flask-Bcrypt==0.7.1
Flask-Principal==0.4.0
Flask-Mail==0.9.1
SQLAlchemy-Utils==0.31.3
BeautifulSoup4
requests==2.8.1
psycopg2==2.6.1
arrow==0.7.0
boto==2.38.0 # we might make this optional
six==1.10.0
gunicorn==19.4.1
pycrypto==2.6.1
cryptography==1.1.1
pyopenssl==0.15.1
pyjwt==1.4.0
xmltodict==0.9.2
lockfile==0.12.2
future==0.15.2

66
docs/security.rst Normal file
View File

@ -0,0 +1,66 @@
Security
========
We take the security of ``lemur`` seriously. The following are a set of
policies we have adopted to ensure that security issues are addressed in a
timely fashion.
Reporting a security issue
--------------------------
We ask that you do not report security issues to our normal GitHub issue
tracker.
If you believe you've identified a security issue with ``lemur``, please
report it to ``cloudsecurity@netflix.com``.
Once you've submitted an issue via email, you should receive an acknowledgment
within 48 hours, and depending on the action to be taken, you may receive
further follow-up emails.
Supported Versions
------------------
At any given time, we will provide security support for the `master`_ branch
as well as the 2 most recent releases.
Disclosure Process
------------------
Our process for taking a security issue from private discussion to public
disclosure involves multiple steps.
Approximately one week before full public disclosure, we will send advance
notification of the issue to a list of people and organizations, primarily
composed of operating-system vendors and other distributors of
``lemur``. This notification will consist of an email message
containing:
* A full description of the issue and the affected versions of
``lemur``.
* The steps we will be taking to remedy the issue.
* The patches, if any, that will be applied to ``lemur``.
* The date on which the ``lemur`` team will apply these patches, issue
new releases, and publicly disclose the issue.
Simultaneously, the reporter of the issue will receive notification of the date
on which we plan to make the issue public.
On the day of disclosure, we will take the following steps:
* Apply the relevant patches to the ``lemur`` repository. The commit
messages for these patches will indicate that they are for security issues,
but will not describe the issue in any detail; instead, they will warn of
upcoming disclosure.
* Issue the relevant releases.
If a reported issue is believed to be particularly time-sensitive due to a
known exploit in the wild, for example the time between advance notification
and public disclosure may be shortened considerably.
The list of people and organizations who receives advanced notification of
security issues is not, and will not, be made public. This list generally
consists of high profile downstream distributors and is entirely at the
discretion of the ``lemur`` team.
.. _`master`: https://github.com/Netflix/lemur

View File

@ -72,7 +72,6 @@ gulp.task('dev:styles', function () {
}; };
var fileList = [ var fileList = [
'lemur/static/app/styles/lemur.css',
'bower_components/bootswatch/sandstone/bootswatch.less', 'bower_components/bootswatch/sandstone/bootswatch.less',
'bower_components/fontawesome/css/font-awesome.css', 'bower_components/fontawesome/css/font-awesome.css',
'bower_components/angular-spinkit/src/angular-spinkit.css', 'bower_components/angular-spinkit/src/angular-spinkit.css',
@ -81,7 +80,8 @@ gulp.task('dev:styles', function () {
'bower_components/angular-ui-switch/angular-ui-switch.css', 'bower_components/angular-ui-switch/angular-ui-switch.css',
'bower_components/angular-wizard/dist/angular-wizard.css', 'bower_components/angular-wizard/dist/angular-wizard.css',
'bower_components/ng-table/ng-table.css', 'bower_components/ng-table/ng-table.css',
'bower_components/angularjs-toaster/toaster.css' 'bower_components/angularjs-toaster/toaster.css',
'lemur/static/app/styles/lemur.css'
]; ];
return gulp.src(fileList) return gulp.src(fileList)
@ -238,8 +238,8 @@ gulp.task('build:images', function () {
gulp.task('package:strip', function () { gulp.task('package:strip', function () {
return gulp.src(['lemur/static/dist/scripts/main*']) return gulp.src(['lemur/static/dist/scripts/main*'])
.pipe(replace('http:\/\/localhost:5000', ''))
.pipe(replace('http:\/\/localhost:3000', '')) .pipe(replace('http:\/\/localhost:3000', ''))
.pipe(replace('http:\/\/localhost:8000', ''))
.pipe(useref()) .pipe(useref())
.pipe(revReplace()) .pipe(revReplace())
.pipe(gulp.dest('lemur/static/dist/scripts')) .pipe(gulp.dest('lemur/static/dist/scripts'))

18
lemur/__about__.py Normal file
View File

@ -0,0 +1,18 @@
from __future__ import absolute_import, division, print_function
__all__ = [
"__title__", "__summary__", "__uri__", "__version__", "__author__",
"__email__", "__license__", "__copyright__",
]
__title__ = "lemur"
__summary__ = ("Certificate management and orchestration service")
__uri__ = "https://github.com/Netflix/lemur"
__version__ = "0.2"
__author__ = "The Lemur developers"
__email__ = "security@netflix.com"
__license__ = "Apache License, Version 2.0"
__copyright__ = "Copyright 2015 {0}".format(__author__)

View File

@ -8,6 +8,8 @@
""" """
from __future__ import absolute_import, division, print_function
from lemur import factory from lemur import factory
from lemur.users.views import mod as users_bp from lemur.users.views import mod as users_bp
@ -22,6 +24,16 @@ from lemur.plugins.views import mod as plugins_bp
from lemur.notifications.views import mod as notifications_bp from lemur.notifications.views import mod as notifications_bp
from lemur.sources.views import mod as sources_bp from lemur.sources.views import mod as sources_bp
from lemur.__about__ import (
__author__, __copyright__, __email__, __license__, __summary__, __title__,
__uri__, __version__
)
__all__ = [
"__title__", "__summary__", "__uri__", "__version__", "__author__",
"__email__", "__license__", "__copyright__",
]
LEMUR_BLUEPRINTS = ( LEMUR_BLUEPRINTS = (
users_bp, users_bp,

View File

@ -35,7 +35,7 @@ class Login(Resource):
Authorization:Bearer <token> Authorization:Bearer <token>
Tokens have a set expiration date. You can inspect the token expiration be base64 decoding the token and inspecting Tokens have a set expiration date. You can inspect the token expiration by base64 decoding the token and inspecting
it's contents. it's contents.
.. note:: It is recommended that the token expiration is fairly short lived (hours not days). This will largely depend \ .. note:: It is recommended that the token expiration is fairly short lived (hours not days). This will largely depend \

View File

@ -14,7 +14,7 @@ from sqlalchemy import Column, Integer, String, Text, func, ForeignKey, DateTime
from sqlalchemy.dialects.postgresql import JSON from sqlalchemy.dialects.postgresql import JSON
from lemur.database import db from lemur.database import db
from lemur.certificates.models import cert_get_cn, cert_get_not_after, cert_get_not_before from lemur.certificates.models import get_cn, get_not_after, get_not_before
class Authority(db.Model): class Authority(db.Model):
@ -44,9 +44,9 @@ class Authority(db.Model):
self.owner = owner self.owner = owner
self.plugin_name = plugin_name self.plugin_name = plugin_name
cert = x509.load_pem_x509_certificate(str(body), default_backend()) cert = x509.load_pem_x509_certificate(str(body), default_backend())
self.cn = cert_get_cn(cert) self.cn = get_cn(cert)
self.not_before = cert_get_not_before(cert) self.not_before = get_not_before(cert)
self.not_after = cert_get_not_after(cert) self.not_after = get_not_after(cert)
self.roles = roles self.roles = roles
self.description = description self.description = description

View File

@ -58,7 +58,15 @@ def create(kwargs):
cert = Certificate(cert_body, chain=intermediate) cert = Certificate(cert_body, chain=intermediate)
cert.owner = kwargs['ownerEmail'] cert.owner = kwargs['ownerEmail']
cert.description = "This is the ROOT certificate for the {0} certificate authority".format(kwargs.get('caName'))
if kwargs['caType'] == 'subca':
cert.description = "This is the ROOT certificate for the {0} sub certificate authority the parent \
authority is {1}.".format(kwargs.get('caName'), kwargs.get('caParent'))
else:
cert.description = "This is the ROOT certificate for the {0} certificate authority.".format(
kwargs.get('caName')
)
cert.user = g.current_user cert.user = g.current_user
cert.notifications = notification_service.create_default_expiration_notifications( cert.notifications = notification_service.create_default_expiration_notifications(

View File

@ -13,9 +13,7 @@ from cryptography.hazmat.backends import default_backend
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from sqlalchemy import event, Integer, ForeignKey, String, DateTime, PassiveDefault, func, Column, Text, Boolean from sqlalchemy import event, Integer, ForeignKey, String, DateTime, PassiveDefault, func, Column, Text, Boolean
from sqlalchemy_utils import EncryptedType from lemur.utils import Vault
from lemur.utils import get_key
from lemur.database import db from lemur.database import db
from lemur.plugins.base import plugins from lemur.plugins.base import plugins
@ -24,7 +22,8 @@ from lemur.domains.models import Domain
from lemur.constants import SAN_NAMING_TEMPLATE, DEFAULT_NAMING_TEMPLATE from lemur.constants import SAN_NAMING_TEMPLATE, DEFAULT_NAMING_TEMPLATE
from lemur.models import certificate_associations, certificate_source_associations, \ from lemur.models import certificate_associations, certificate_source_associations, \
certificate_destination_associations, certificate_notification_associations certificate_destination_associations, certificate_notification_associations, \
certificate_replacement_associations
def create_name(issuer, not_before, not_after, subject, san): def create_name(issuer, not_before, not_after, subject, san):
@ -34,6 +33,11 @@ def create_name(issuer, not_before, not_after, subject, san):
useful information such as Common Name, Validation dates, useful information such as Common Name, Validation dates,
and Issuer. and Issuer.
:param san:
:param subject:
:param not_after:
:param issuer:
:param not_before:
:rtype : str :rtype : str
:return: :return:
""" """
@ -63,7 +67,11 @@ def create_name(issuer, not_before, not_after, subject, san):
return temp.replace(" ", "-") return temp.replace(" ", "-")
def cert_get_cn(cert): def get_signing_algorithm(cert):
return cert.signature_hash_algorithm.name
def get_cn(cert):
""" """
Attempts to get a sane common name from a given certificate. Attempts to get a sane common name from a given certificate.
@ -75,7 +83,7 @@ def cert_get_cn(cert):
)[0].value.strip() )[0].value.strip()
def cert_get_domains(cert): def get_domains(cert):
""" """
Attempts to get an domains listed in a certificate. Attempts to get an domains listed in a certificate.
If 'subjectAltName' extension is not available we simply If 'subjectAltName' extension is not available we simply
@ -96,7 +104,7 @@ def cert_get_domains(cert):
return domains return domains
def cert_get_serial(cert): def get_serial(cert):
""" """
Fetch the serial number from the certificate. Fetch the serial number from the certificate.
@ -106,7 +114,7 @@ def cert_get_serial(cert):
return cert.serial return cert.serial
def cert_is_san(cert): def is_san(cert):
""" """
Determines if a given certificate is a SAN certificate. Determines if a given certificate is a SAN certificate.
SAN certificates are simply certificates that cover multiple domains. SAN certificates are simply certificates that cover multiple domains.
@ -114,18 +122,18 @@ def cert_is_san(cert):
:param cert: :param cert:
:return: Bool :return: Bool
""" """
if len(cert_get_domains(cert)) > 1: if len(get_domains(cert)) > 1:
return True return True
def cert_is_wildcard(cert): def is_wildcard(cert):
""" """
Determines if certificate is a wildcard certificate. Determines if certificate is a wildcard certificate.
:param cert: :param cert:
:return: Bool :return: Bool
""" """
domains = cert_get_domains(cert) domains = get_domains(cert)
if len(domains) == 1 and domains[0][0:1] == "*": if len(domains) == 1 and domains[0][0:1] == "*":
return True return True
@ -133,7 +141,7 @@ def cert_is_wildcard(cert):
return True return True
def cert_get_bitstrength(cert): def get_bitstrength(cert):
""" """
Calculates a certificates public key bit length. Calculates a certificates public key bit length.
@ -143,7 +151,7 @@ def cert_get_bitstrength(cert):
return cert.public_key().key_size return cert.public_key().key_size
def cert_get_issuer(cert): def get_issuer(cert):
""" """
Gets a sane issuer from a given certificate. Gets a sane issuer from a given certificate.
@ -160,7 +168,7 @@ def cert_get_issuer(cert):
current_app.logger.error("Unable to get issuer! {0}".format(e)) current_app.logger.error("Unable to get issuer! {0}".format(e))
def cert_get_not_before(cert): def get_not_before(cert):
""" """
Gets the naive datetime of the certificates 'not_before' field. Gets the naive datetime of the certificates 'not_before' field.
This field denotes the first date in time which the given certificate This field denotes the first date in time which the given certificate
@ -172,7 +180,7 @@ def cert_get_not_before(cert):
return cert.not_valid_before return cert.not_valid_before
def cert_get_not_after(cert): def get_not_after(cert):
""" """
Gets the naive datetime of the certificates 'not_after' field. Gets the naive datetime of the certificates 'not_after' field.
This field denotes the last date in time which the given certificate This field denotes the last date in time which the given certificate
@ -209,7 +217,7 @@ class Certificate(db.Model):
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
owner = Column(String(128)) owner = Column(String(128))
body = Column(Text()) body = Column(Text())
private_key = Column(EncryptedType(String, get_key)) private_key = Column(Vault)
status = Column(String(128)) status = Column(String(128))
deleted = Column(Boolean, index=True) deleted = Column(Boolean, index=True)
name = Column(String(128)) name = Column(String(128))
@ -224,10 +232,16 @@ class Certificate(db.Model):
not_before = Column(DateTime) not_before = Column(DateTime)
not_after = Column(DateTime) not_after = Column(DateTime)
date_created = Column(DateTime, PassiveDefault(func.now()), nullable=False) date_created = Column(DateTime, PassiveDefault(func.now()), nullable=False)
signing_algorithm = Column(String(128))
user_id = Column(Integer, ForeignKey('users.id')) user_id = Column(Integer, ForeignKey('users.id'))
authority_id = Column(Integer, ForeignKey('authorities.id')) authority_id = Column(Integer, ForeignKey('authorities.id'))
notifications = relationship("Notification", secondary=certificate_notification_associations, backref='certificate') notifications = relationship("Notification", secondary=certificate_notification_associations, backref='certificate')
destinations = relationship("Destination", secondary=certificate_destination_associations, backref='certificate') destinations = relationship("Destination", secondary=certificate_destination_associations, backref='certificate')
replaces = relationship("Certificate",
secondary=certificate_replacement_associations,
primaryjoin=id == certificate_replacement_associations.c.certificate_id, # noqa
secondaryjoin=id == certificate_replacement_associations.c.replaced_certificate_id, # noqa
backref='replaced')
sources = relationship("Source", secondary=certificate_source_associations, backref='certificate') sources = relationship("Source", secondary=certificate_source_associations, backref='certificate')
domains = relationship("Domain", secondary=certificate_associations, backref="certificate") domains = relationship("Domain", secondary=certificate_associations, backref="certificate")
@ -237,16 +251,17 @@ class Certificate(db.Model):
self.private_key = private_key self.private_key = private_key
self.chain = chain self.chain = chain
cert = x509.load_pem_x509_certificate(str(self.body), default_backend()) cert = x509.load_pem_x509_certificate(str(self.body), default_backend())
self.bits = cert_get_bitstrength(cert) self.signing_algorithm = get_signing_algorithm(cert)
self.issuer = cert_get_issuer(cert) self.bits = get_bitstrength(cert)
self.serial = cert_get_serial(cert) self.issuer = get_issuer(cert)
self.cn = cert_get_cn(cert) self.serial = get_serial(cert)
self.san = cert_is_san(cert) self.cn = get_cn(cert)
self.not_before = cert_get_not_before(cert) self.san = is_san(cert)
self.not_after = cert_get_not_after(cert) self.not_before = get_not_before(cert)
self.not_after = get_not_after(cert)
self.name = create_name(self.issuer, self.not_before, self.not_after, self.cn, self.san) self.name = create_name(self.issuer, self.not_before, self.not_after, self.cn, self.san)
for domain in cert_get_domains(cert): for domain in get_domains(cert):
self.domains.append(Domain(name=domain)) self.domains.append(Domain(name=domain))
@property @property
@ -276,11 +291,44 @@ class Certificate(db.Model):
""" """
return "arn:aws:iam::{}:server-certificate/{}".format(account_number, self.name) return "arn:aws:iam::{}:server-certificate/{}".format(account_number, self.name)
def as_dict(self):
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
@event.listens_for(Certificate.destinations, 'append') @event.listens_for(Certificate.destinations, 'append')
def update_destinations(target, value, initiator): def update_destinations(target, value, initiator):
"""
Attempt to upload the new certificate to the new destination
:param target:
:param value:
:param initiator:
:return:
"""
destination_plugin = plugins.get(value.plugin_name) destination_plugin = plugins.get(value.plugin_name)
destination_plugin.upload(target.name, target.body, target.private_key, target.chain, value.options) destination_plugin.upload(target.name, target.body, target.private_key, target.chain, value.options)
@event.listens_for(Certificate.replaces, 'append')
def update_replacement(target, value, initiator):
"""
When a certificate is marked as 'replaced' it is then marked as in-active
:param target:
:param value:
:param initiator:
:return:
"""
value.active = False
@event.listens_for(Certificate, 'before_update')
def protect_active(mapper, connection, target):
"""
When a certificate has a replacement do not allow it to be marked as 'active'
:param connection:
:param mapper:
:param target:
:return:
"""
if target.active:
if target.replaced:
raise Exception("Cannot mark certificate as active, certificate has been marked as replaced.")

View File

@ -17,6 +17,7 @@ from lemur.certificates.models import Certificate
from lemur.destinations.models import Destination from lemur.destinations.models import Destination
from lemur.notifications.models import Notification from lemur.notifications.models import Notification
from lemur.authorities.models import Authority from lemur.authorities.models import Authority
from lemur.domains.models import Domain
from lemur.roles.models import Role from lemur.roles.models import Role
@ -76,13 +77,30 @@ def find_duplicates(cert_body):
return Certificate.query.filter_by(body=cert_body).all() return Certificate.query.filter_by(body=cert_body).all()
def update(cert_id, owner, description, active, destinations, notifications): def export(cert, export_plugin):
""" """
Updates a certificate. Exports a certificate to the requested format. This format
may be a binary format.
:param export_plugin:
:param cert:
:return:
"""
plugin = plugins.get(export_plugin['slug'])
return plugin.export(cert.body, cert.chain, cert.private_key, export_plugin['pluginOptions'])
def update(cert_id, owner, description, active, destinations, notifications, replaces):
"""
Updates a certificate
:param cert_id: :param cert_id:
:param owner: :param owner:
:param description:
:param active: :param active:
:param destinations:
:param notifications:
:param replaces:
:return: :return:
""" """
from lemur.notifications import service as notification_service from lemur.notifications import service as notification_service
@ -104,6 +122,7 @@ def update(cert_id, owner, description, active, destinations, notifications):
cert.notifications = new_notifications cert.notifications = new_notifications
database.update_list(cert, 'destinations', Destination, destinations) database.update_list(cert, 'destinations', Destination, destinations)
database.update_list(cert, 'replaces', Certificate, replaces)
cert.owner = owner cert.owner = owner
@ -165,6 +184,7 @@ def import_certificate(**kwargs):
notification_name = 'DEFAULT_SECURITY' 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'))
database.update_list(cert, 'replaces', Certificate, kwargs['replacements'])
cert.notifications = notifications cert.notifications = notifications
cert = database.create(cert) cert = database.create(cert)
@ -194,8 +214,8 @@ def upload(**kwargs):
g.user.certificates.append(cert) g.user.certificates.append(cert)
database.update_list(cert, 'destinations', Destination, kwargs.get('destinations')) database.update_list(cert, 'destinations', Destination, kwargs.get('destinations'))
database.update_list(cert, 'notifications', Notification, kwargs.get('notifications')) database.update_list(cert, 'notifications', Notification, kwargs.get('notifications'))
database.update_list(cert, 'replaces', Certificate, kwargs['replacements'])
# create default notifications for this certificate if none are provided # create default notifications for this certificate if none are provided
notifications = [] notifications = []
@ -228,11 +248,11 @@ def create(**kwargs):
# do this after the certificate has already been created because if it fails to upload to the third party # do this after the certificate has already been created because if it fails to upload to the third party
# we do not want to lose the certificate information. # we do not want to lose the certificate information.
database.update_list(cert, 'destinations', Destination, kwargs.get('destinations')) database.update_list(cert, 'destinations', Destination, kwargs.get('destinations'))
database.update_list(cert, 'replaces', Certificate, kwargs['replacements'])
database.update_list(cert, 'notifications', Notification, kwargs.get('notifications')) database.update_list(cert, 'notifications', Notification, kwargs.get('notifications'))
# create default notifications for this certificate if none are provided # create default notifications for this certificate if none are provided
notifications = [] notifications = cert.notifications
if not kwargs.get('notifications'): if not kwargs.get('notifications'):
notification_name = "DEFAULT_{0}".format(cert.owner.split('@')[0].upper()) notification_name = "DEFAULT_{0}".format(cert.owner.split('@')[0].upper())
notifications += notification_service.create_default_expiration_notifications(notification_name, [cert.owner]) notifications += notification_service.create_default_expiration_notifications(notification_name, [cert.owner])
@ -266,6 +286,7 @@ def render(args):
if filt: if filt:
terms = filt.split(';') terms = filt.split(';')
if 'issuer' in terms: if 'issuer' in terms:
# we can't rely on issuer being correct in the cert directly so we combine queries # we can't rely on issuer being correct in the cert directly so we combine queries
sub_query = database.session_query(Authority.id)\ sub_query = database.session_query(Authority.id)\
@ -280,10 +301,17 @@ def render(args):
) )
return database.sort_and_page(query, Certificate, args) return database.sort_and_page(query, Certificate, args)
if 'destination' in terms: elif 'destination' in terms:
query = query.filter(Certificate.destinations.any(Destination.id == terms[1])) query = query.filter(Certificate.destinations.any(Destination.id == terms[1]))
elif 'active' in filt: # this is really weird but strcmp seems to not work here?? elif 'active' in filt: # this is really weird but strcmp seems to not work here??
query = query.filter(Certificate.active == terms[1]) query = query.filter(Certificate.active == terms[1])
elif 'cn' in terms:
query = query.filter(
or_(
Certificate.cn.ilike('%{0}%'.format(terms[1])),
Certificate.domains.any(Domain.name.ilike('%{0}%'.format(terms[1])))
)
)
else: else:
query = database.filter(query, Certificate, terms) query = database.filter(query, Certificate, terms)

View File

@ -5,7 +5,6 @@
:license: Apache, see LICENSE for more details. :license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com> .. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
""" """
import os
import requests import requests
import subprocess import subprocess
from OpenSSL import crypto from OpenSSL import crypto
@ -13,20 +12,7 @@ from cryptography import x509
from cryptography.hazmat.backends import default_backend from cryptography.hazmat.backends import default_backend
from flask import current_app from flask import current_app
from lemur.utils import mktempfile
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): def ocsp_verify(cert_path, issuer_chain_path):

View File

@ -5,32 +5,24 @@
:license: Apache, see LICENSE for more details. :license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com> .. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
""" """
import base64
from builtins import str from builtins import str
from flask import Blueprint, make_response, jsonify from flask import Blueprint, make_response, jsonify
from flask.ext.restful import reqparse, Api, fields from flask.ext.restful import reqparse, Api, fields
from cryptography import x509 from cryptography import x509
from cryptography.hazmat.backends import default_backend from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives import serialization
from lemur.certificates import service from lemur.certificates import service
from lemur.authorities.models import Authority from lemur.authorities.models import Authority
from lemur.auth.service import AuthenticatedResource from lemur.auth.service import AuthenticatedResource
from lemur.auth.permissions import ViewKeyPermission, AuthorityPermission, UpdateCertificatePermission from lemur.auth.permissions import ViewKeyPermission, AuthorityPermission, UpdateCertificatePermission
from lemur.roles import service as role_service from lemur.roles import service as role_service
from lemur.common.utils import marshal_items, paginated_parser from lemur.common.utils import marshal_items, paginated_parser
from lemur.notifications.views import notification_list from lemur.notifications.views import notification_list
mod = Blueprint('certificates', __name__) mod = Blueprint('certificates', __name__)
api = Api(mod) api = Api(mod)
FIELDS = { FIELDS = {
'name': fields.String, 'name': fields.String,
'id': fields.Integer, 'id': fields.Integer,
@ -46,6 +38,7 @@ FIELDS = {
'notBefore': fields.DateTime(dt_format='iso8601', attribute='not_before'), 'notBefore': fields.DateTime(dt_format='iso8601', attribute='not_before'),
'notAfter': fields.DateTime(dt_format='iso8601', attribute='not_after'), 'notAfter': fields.DateTime(dt_format='iso8601', attribute='not_after'),
'cn': fields.String, 'cn': fields.String,
'signingAlgorithm': fields.String(attribute='signing_algorithm'),
'status': fields.String, 'status': fields.String,
'body': fields.String 'body': fields.String
} }
@ -102,6 +95,7 @@ def private_key_str(value, name):
class CertificatesList(AuthenticatedResource): class CertificatesList(AuthenticatedResource):
""" Defines the 'certificates' endpoint """ """ Defines the 'certificates' endpoint """
def __init__(self): def __init__(self):
self.reqparse = reqparse.RequestParser() self.reqparse = reqparse.RequestParser()
super(CertificatesList, self).__init__() super(CertificatesList, self).__init__()
@ -208,6 +202,46 @@ class CertificatesList(AuthenticatedResource):
"notAfter": "2015-06-17T15:21:08", "notAfter": "2015-06-17T15:21:08",
"description": "dsfdsf" "description": "dsfdsf"
}, },
"notifications": [
{
"description": "Default 30 day expiration notification",
"notificationOptions": [
{
"name": "interval",
"required": true,
"value": 30,
"helpMessage": "Number of days to be alert before expiration.",
"validation": "^\\d+$",
"type": "int"
},
{
"available": [
"days",
"weeks",
"months"
],
"name": "unit",
"required": true,
"value": "days",
"helpMessage": "Interval unit",
"validation": "",
"type": "select"
},
{
"name": "recipients",
"required": true,
"value": "bob@example.com",
"helpMessage": "Comma delimited list of email addresses",
"validation": "^([\\w+-.%]+@[\\w-.]+\\.[A-Za-z]{2,4},?)+$",
"type": "str"
}
],
"label": "DEFAULT_KGLISSON_30_DAY",
"pluginName": "email-notification",
"active": true,
"id": 7
}
],
"extensions": { "extensions": {
"basicConstraints": {}, "basicConstraints": {},
"keyUsage": { "keyUsage": {
@ -228,7 +262,10 @@ class CertificatesList(AuthenticatedResource):
}, },
"commonName": "test", "commonName": "test",
"validityStart": "2015-06-05T07:00:00.000Z", "validityStart": "2015-06-05T07:00:00.000Z",
"validityEnd": "2015-06-16T07:00:00.000Z" "validityEnd": "2015-06-16T07:00:00.000Z",
"replacements": [
{'id': 123}
]
} }
**Example response**: **Example response**:
@ -276,18 +313,18 @@ class CertificatesList(AuthenticatedResource):
self.reqparse.add_argument('extensions', type=dict, location='json') self.reqparse.add_argument('extensions', type=dict, location='json')
self.reqparse.add_argument('destinations', type=list, default=[], 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=list, default=[], location='json')
self.reqparse.add_argument('owner', type=str, location='json') self.reqparse.add_argument('replacements', type=list, default=[], location='json')
self.reqparse.add_argument('validityStart', type=str, location='json') # TODO validate self.reqparse.add_argument('validityStart', type=str, location='json') # TODO validate
self.reqparse.add_argument('validityEnd', type=str, location='json') # TODO validate self.reqparse.add_argument('validityEnd', type=str, location='json') # TODO validate
self.reqparse.add_argument('authority', type=valid_authority, location='json') self.reqparse.add_argument('authority', type=valid_authority, location='json', required=True)
self.reqparse.add_argument('description', type=str, location='json') self.reqparse.add_argument('description', type=str, location='json')
self.reqparse.add_argument('country', type=str, location='json') self.reqparse.add_argument('country', type=str, location='json', required=True)
self.reqparse.add_argument('state', type=str, location='json') self.reqparse.add_argument('state', type=str, location='json', required=True)
self.reqparse.add_argument('location', type=str, location='json') self.reqparse.add_argument('location', type=str, location='json', required=True)
self.reqparse.add_argument('organization', type=str, location='json') self.reqparse.add_argument('organization', type=str, location='json', required=True)
self.reqparse.add_argument('organizationalUnit', type=str, location='json') self.reqparse.add_argument('organizationalUnit', type=str, location='json', required=True)
self.reqparse.add_argument('owner', type=str, location='json') self.reqparse.add_argument('owner', type=str, location='json', required=True)
self.reqparse.add_argument('commonName', type=str, location='json') self.reqparse.add_argument('commonName', type=str, location='json', required=True)
args = self.reqparse.parse_args() args = self.reqparse.parse_args()
@ -309,6 +346,7 @@ class CertificatesList(AuthenticatedResource):
class CertificatesUpload(AuthenticatedResource): class CertificatesUpload(AuthenticatedResource):
""" Defines the 'certificates' upload endpoint """ """ Defines the 'certificates' upload endpoint """
def __init__(self): def __init__(self):
self.reqparse = reqparse.RequestParser() self.reqparse = reqparse.RequestParser()
super(CertificatesUpload, self).__init__() super(CertificatesUpload, self).__init__()
@ -335,6 +373,7 @@ class CertificatesUpload(AuthenticatedResource):
"privateKey": "---Begin Private..." "privateKey": "---Begin Private..."
"destinations": [], "destinations": [],
"notifications": [], "notifications": [],
"replacements": [],
"name": "cert1" "name": "cert1"
} }
@ -361,6 +400,7 @@ class CertificatesUpload(AuthenticatedResource):
"active": true, "active": true,
"notBefore": "2015-06-05T17:09:39", "notBefore": "2015-06-05T17:09:39",
"notAfter": "2015-06-10T17:09:39", "notAfter": "2015-06-10T17:09:39",
"signingAlgorithm": "sha2"
"cn": "example.com", "cn": "example.com",
"status": "unknown" "status": "unknown"
} }
@ -378,8 +418,9 @@ class CertificatesUpload(AuthenticatedResource):
self.reqparse.add_argument('owner', type=str, required=True, 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('name', type=str, location='json')
self.reqparse.add_argument('publicCert', type=pem_str, required=True, dest='public_cert', 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('destinations', type=list, default=[], location='json')
self.reqparse.add_argument('notifications', type=list, default=[], dest='notifications', location='json') self.reqparse.add_argument('notifications', type=list, default=[], location='json')
self.reqparse.add_argument('replacements', type=list, default=[], location='json')
self.reqparse.add_argument('intermediateCert', type=pem_str, dest='intermediate_cert', location='json') self.reqparse.add_argument('intermediateCert', type=pem_str, dest='intermediate_cert', location='json')
self.reqparse.add_argument('privateKey', type=private_key_str, dest='private_key', location='json') self.reqparse.add_argument('privateKey', type=private_key_str, dest='private_key', location='json')
@ -394,6 +435,7 @@ class CertificatesUpload(AuthenticatedResource):
class CertificatesStats(AuthenticatedResource): class CertificatesStats(AuthenticatedResource):
""" Defines the 'certificates' stats endpoint """ """ Defines the 'certificates' stats endpoint """
def __init__(self): def __init__(self):
self.reqparse = reqparse.RequestParser() self.reqparse = reqparse.RequestParser()
super(CertificatesStats, self).__init__() super(CertificatesStats, self).__init__()
@ -504,6 +546,7 @@ class Certificates(AuthenticatedResource):
"active": true, "active": true,
"notBefore": "2015-06-05T17:09:39", "notBefore": "2015-06-05T17:09:39",
"notAfter": "2015-06-10T17:09:39", "notAfter": "2015-06-10T17:09:39",
"signingAlgorithm": "sha2",
"cn": "example.com", "cn": "example.com",
"status": "unknown" "status": "unknown"
} }
@ -533,7 +576,8 @@ class Certificates(AuthenticatedResource):
"owner": "jimbob@example.com", "owner": "jimbob@example.com",
"active": false "active": false
"notifications": [], "notifications": [],
"destinations": [] "destinations": [],
"replacements": []
} }
**Example response**: **Example response**:
@ -572,6 +616,7 @@ class Certificates(AuthenticatedResource):
self.reqparse.add_argument('description', 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('destinations', type=list, default=[], location='json')
self.reqparse.add_argument('notifications', type=notification_list, default=[], location='json') self.reqparse.add_argument('notifications', type=notification_list, default=[], location='json')
self.reqparse.add_argument('replacements', type=list, default=[], location='json')
args = self.reqparse.parse_args() args = self.reqparse.parse_args()
cert = service.get(certificate_id) cert = service.get(certificate_id)
@ -586,7 +631,8 @@ class Certificates(AuthenticatedResource):
args['description'], args['description'],
args['active'], args['active'],
args['destinations'], args['destinations'],
args['notifications'] args['notifications'],
args['replacements']
) )
return dict(message='You are not authorized to update this certificate'), 403 return dict(message='You are not authorized to update this certificate'), 403
@ -594,6 +640,7 @@ class Certificates(AuthenticatedResource):
class NotificationCertificatesList(AuthenticatedResource): class NotificationCertificatesList(AuthenticatedResource):
""" Defines the 'certificates' endpoint """ """ Defines the 'certificates' endpoint """
def __init__(self): def __init__(self):
self.reqparse = reqparse.RequestParser() self.reqparse = reqparse.RequestParser()
super(NotificationCertificatesList, self).__init__() super(NotificationCertificatesList, self).__init__()
@ -638,6 +685,7 @@ class NotificationCertificatesList(AuthenticatedResource):
"active": true, "active": true,
"notBefore": "2015-06-05T17:09:39", "notBefore": "2015-06-05T17:09:39",
"notAfter": "2015-06-10T17:09:39", "notAfter": "2015-06-10T17:09:39",
"signingAlgorithm": "sha2",
"cn": "example.com", "cn": "example.com",
"status": "unknown" "status": "unknown"
} }
@ -668,9 +716,154 @@ class NotificationCertificatesList(AuthenticatedResource):
return service.render(args) return service.render(args)
class CertificatesReplacementsList(AuthenticatedResource):
def __init__(self):
self.reqparse = reqparse.RequestParser()
super(CertificatesReplacementsList, self).__init__()
@marshal_items(FIELDS)
def get(self, certificate_id):
"""
.. http:get:: /certificates/1/replacements
One certificate
**Example request**:
.. sourcecode:: http
GET /certificates/1/replacements HTTP/1.1
Host: example.com
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: text/javascript
[{
"id": 1,
"name": "cert1",
"description": "this is cert1",
"bits": 2048,
"deleted": false,
"issuer": "ExampeInc.",
"serial": "123450",
"chain": "-----Begin ...",
"body": "-----Begin ...",
"san": true,
"owner": "bob@example.com",
"active": true,
"notBefore": "2015-06-05T17:09:39",
"notAfter": "2015-06-10T17:09:39",
"signingAlgorithm": "sha2",
"cn": "example.com",
"status": "unknown"
}]
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
:statuscode 403: unauthenticated
"""
return service.get(certificate_id).replaces
class CertificateExport(AuthenticatedResource):
def __init__(self):
self.reqparse = reqparse.RequestParser()
super(CertificateExport, self).__init__()
def post(self, certificate_id):
"""
.. http:post:: /certificates/1/export
Export a certificate
**Example request**:
.. sourcecode:: http
PUT /certificates/1/export HTTP/1.1
Host: example.com
Accept: application/json, text/javascript
{
"export": {
"plugin": {
"pluginOptions": [{
"available": ["Java Key Store (JKS)"],
"required": true,
"type": "select",
"name": "type",
"helpMessage": "Choose the format you wish to export",
"value": "Java Key Store (JKS)"
}, {
"required": false,
"type": "str",
"name": "passphrase",
"validation": "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[$@$!%*#?&])[A-Za-z\\d$@$!%*#?&]{8,}$",
"helpMessage": "If no passphrase is given one will be generated for you, we highly recommend this. Minimum length is 8."
}, {
"required": false,
"type": "str",
"name": "alias",
"helpMessage": "Enter the alias you wish to use for the keystore."
}],
"version": "unknown",
"description": "Attempts to generate a JKS keystore or truststore",
"title": "Java",
"author": "Kevin Glisson",
"type": "export",
"slug": "java-export"
}
}
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: text/javascript
{
"data": "base64encodedstring",
"passphrase": "UAWOHW#&@_%!tnwmxh832025",
"extension": "jks"
}
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
:statuscode 403: unauthenticated
"""
self.reqparse.add_argument('export', type=dict, required=True, location='json')
args = self.reqparse.parse_args()
cert = service.get(certificate_id)
role = role_service.get_by_name(cert.owner)
permission = UpdateCertificatePermission(certificate_id, getattr(role, 'name', None))
if permission.can():
extension, passphrase, data = service.export(cert, args['export']['plugin'])
# we take a hit in message size when b64 encoding
return dict(extension=extension, passphrase=passphrase, data=base64.b64encode(data))
return dict(message='You are not authorized to export this certificate'), 403
api.add_resource(CertificatesList, '/certificates', endpoint='certificates') api.add_resource(CertificatesList, '/certificates', endpoint='certificates')
api.add_resource(Certificates, '/certificates/<int:certificate_id>', endpoint='certificate') api.add_resource(Certificates, '/certificates/<int:certificate_id>', endpoint='certificate')
api.add_resource(CertificatesStats, '/certificates/stats', endpoint='certificateStats') api.add_resource(CertificatesStats, '/certificates/stats', endpoint='certificateStats')
api.add_resource(CertificatesUpload, '/certificates/upload', endpoint='certificateUpload') api.add_resource(CertificatesUpload, '/certificates/upload', endpoint='certificateUpload')
api.add_resource(CertificatePrivateKey, '/certificates/<int:certificate_id>/key', endpoint='privateKeyCertificates') api.add_resource(CertificatePrivateKey, '/certificates/<int:certificate_id>/key', endpoint='privateKeyCertificates')
api.add_resource(NotificationCertificatesList, '/notifications/<int:notification_id>/certificates', endpoint='notificationCertificates') api.add_resource(CertificateExport, '/certificates/<int:certificate_id>/export', endpoint='exportCertificate')
api.add_resource(NotificationCertificatesList, '/notifications/<int:notification_id>/certificates',
endpoint='notificationCertificates')
api.add_resource(CertificatesReplacementsList, '/certificates/<int:certificate_id>/replacements',
endpoint='replacements')

View File

@ -22,7 +22,8 @@ class InstanceManager(object):
def add(self, class_path): def add(self, class_path):
self.cache = None self.cache = None
self.class_list.append(class_path) if class_path not in self.class_list:
self.class_list.append(class_path)
def remove(self, class_path): def remove(self, class_path):
self.cache = None self.cache = None

View File

@ -63,9 +63,9 @@ class marshal_items(object):
if hasattr(e, 'data'): if hasattr(e, 'data'):
return {'message': e.data['message']}, 400 return {'message': e.data['message']}, 400
else: else:
return {'message': 'unknown'}, 400 return {'message': {'exception': 'unknown'}}, 400
else: else:
return {'message': str(e)}, 400 return {'message': {'exception': str(e)}}, 400
return wrapper return wrapper

View File

@ -8,6 +8,7 @@
from sqlalchemy import func from sqlalchemy import func
from lemur import database from lemur import database
from lemur.models import certificate_destination_associations
from lemur.destinations.models import Destination from lemur.destinations.models import Destination
from lemur.certificates.models import Certificate from lemur.certificates.models import Certificate
@ -117,10 +118,9 @@ def stats(**kwargs):
:param kwargs: :param kwargs:
:return: :return:
""" """
attr = getattr(Destination, kwargs.get('metric')) items = database.db.session.query(Destination.label, func.count(certificate_destination_associations.c.certificate_id))\
query = database.db.session.query(attr, func.count(attr)) .join(certificate_destination_associations)\
.group_by(Destination.label).all()
items = query.group_by(attr).all()
keys = [] keys = []
values = [] values = []

View File

@ -72,7 +72,7 @@ SECRET_KEY = '{flask_secret_key}'
# You should consider storing these separately from your config # You should consider storing these separately from your config
LEMUR_TOKEN_SECRET = '{secret_token}' LEMUR_TOKEN_SECRET = '{secret_token}'
LEMUR_ENCRYPTION_KEY = '{encryption_key}' LEMUR_ENCRYPTION_KEYS = '{encryption_key}'
# this is a list of domains as regexes that only admins can issue # this is a list of domains as regexes that only admins can issue
LEMUR_RESTRICTED_DOMAINS = [] LEMUR_RESTRICTED_DOMAINS = []
@ -112,13 +112,6 @@ SQLALCHEMY_DATABASE_URI = 'postgresql://lemur:lemur@localhost:5432/lemur'
# These will be dependent on which 3rd party that Lemur is # These will be dependent on which 3rd party that Lemur is
# configured to use. # configured to use.
# CLOUDCA_URL = ''
# CLOUDCA_PEM_PATH = ''
# CLOUDCA_BUNDLE = ''
# number of years to issue if not specified
# CLOUDCA_DEFAULT_VALIDITY = 2
# VERISIGN_URL = '' # VERISIGN_URL = ''
# VERISIGN_PEM_PATH = '' # VERISIGN_PEM_PATH = ''
# VERISIGN_FIRST_NAME = '' # VERISIGN_FIRST_NAME = ''
@ -178,7 +171,9 @@ def generate_settings():
settings file. settings file.
""" """
output = CONFIG_TEMPLATE.format( output = CONFIG_TEMPLATE.format(
encryption_key=base64.b64encode(os.urandom(KEY_LENGTH)), # we use Fernet.generate_key to make sure that the key length is
# compatible with Fernet
encryption_key=Fernet.generate_key(),
secret_token=base64.b64encode(os.urandom(KEY_LENGTH)), secret_token=base64.b64encode(os.urandom(KEY_LENGTH)),
flask_secret_key=base64.b64encode(os.urandom(KEY_LENGTH)), flask_secret_key=base64.b64encode(os.urandom(KEY_LENGTH)),
) )
@ -321,7 +316,7 @@ class CreateUser(Command):
Option('-u', '--username', dest='username', required=True), Option('-u', '--username', dest='username', required=True),
Option('-e', '--email', dest='email', required=True), Option('-e', '--email', dest='email', required=True),
Option('-a', '--active', dest='active', default=True), Option('-a', '--active', dest='active', default=True),
Option('-r', '--roles', dest='roles', default=[]) Option('-r', '--roles', dest='roles', action='append', default=[])
) )
def run(self, username, email, active, roles): def run(self, username, email, active, roles):
@ -723,6 +718,24 @@ def publish_verisign_units():
requests.post('http://localhost:8078/metrics', data=json.dumps(metric)) requests.post('http://localhost:8078/metrics', data=json.dumps(metric))
@manager.command
def backfill_signing_algo():
"""
Will attempt to backfill the signing_algorithm column
:return:
"""
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from lemur.certificates.models import get_signing_algorithm
for c in cert_service.get_all_certs():
cert = x509.load_pem_x509_certificate(str(c.body), default_backend())
c.signing_algorithm = get_signing_algorithm(cert)
c.signing_algorithm
database.update(c)
print(c.signing_algorithm)
def main(): def main():
manager.add_command("start", LemurServer()) manager.add_command("start", LemurServer())
manager.add_command("runserver", Server(host='127.0.0.1')) manager.add_command("runserver", Server(host='127.0.0.1'))

View File

@ -0,0 +1,31 @@
"""Adding the ability to specify certificate replacements
Revision ID: 33de094da890
Revises: ed422fc58ba
Create Date: 2015-11-30 15:40:19.827272
"""
# revision identifiers, used by Alembic.
revision = '33de094da890'
down_revision = 'ed422fc58ba'
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
def upgrade():
### commands auto generated by Alembic - please adjust! ###
op.create_table('certificate_replacement_associations',
sa.Column('replaced_certificate_id', sa.Integer(), nullable=True),
sa.Column('certificate_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['certificate_id'], ['certificates.id'], ondelete='cascade'),
sa.ForeignKeyConstraint(['replaced_certificate_id'], ['certificates.id'], ondelete='cascade')
)
### end Alembic commands ###
def downgrade():
### commands auto generated by Alembic - please adjust! ###
op.drop_table('certificate_replacement_associations')
### end Alembic commands ###

View File

@ -0,0 +1,26 @@
"""Adding certificate signing algorithm
Revision ID: 4bcfa2c36623
Revises: 1ff763f5b80b
Create Date: 2015-10-06 10:03:47.993204
"""
# revision identifiers, used by Alembic.
revision = '4bcfa2c36623'
down_revision = '1ff763f5b80b'
from alembic import op
import sqlalchemy as sa
def upgrade():
### commands auto generated by Alembic - please adjust! ###
op.add_column('certificates', sa.Column('signing_algorithm', sa.String(length=128), nullable=True))
### end Alembic commands ###
def downgrade():
### commands auto generated by Alembic - please adjust! ###
op.drop_column('certificates', 'signing_algorithm')
### end Alembic commands ###

View File

@ -0,0 +1,255 @@
"""Migrates the private key encrypted column from AES to fernet encryption scheme.
Revision ID: ed422fc58ba
Revises: 4bcfa2c36623
Create Date: 2015-10-23 09:19:28.654126
"""
import base64
# revision identifiers, used by Alembic.
revision = 'ed422fc58ba'
down_revision = '4bcfa2c36623'
import six
from StringIO import StringIO
from alembic import op
import sqlalchemy as sa
from sqlalchemy.sql import text
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.fernet import Fernet, MultiFernet
from flask import current_app
from lemur.common.utils import get_psuedo_random_string
conn = op.get_bind()
op.drop_table('encrypted_keys')
op.drop_table('encrypted_passwords')
# helper tables to migrate data
temp_key_table = op.create_table('encrypted_keys',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('aes', sa.Binary()),
sa.Column('fernet', sa.Binary()),
sa.PrimaryKeyConstraint('id')
)
# helper table to migrate data
temp_password_table = op.create_table('encrypted_passwords',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('aes', sa.Binary()),
sa.Column('fernet', sa.Binary()),
sa.PrimaryKeyConstraint('id')
)
# From http://sqlalchemy-utils.readthedocs.org/en/latest/_modules/sqlalchemy_utils/types/encrypted.html#EncryptedType
# for migration purposes only
class EncryptionDecryptionBaseEngine(object):
"""A base encryption and decryption engine.
This class must be sub-classed in order to create
new engines.
"""
def _update_key(self, key):
if isinstance(key, six.string_types):
key = key.encode()
digest = hashes.Hash(hashes.SHA256(), backend=default_backend())
digest.update(key)
engine_key = digest.finalize()
self._initialize_engine(engine_key)
def encrypt(self, value):
raise NotImplementedError('Subclasses must implement this!')
def decrypt(self, value):
raise NotImplementedError('Subclasses must implement this!')
class AesEngine(EncryptionDecryptionBaseEngine):
"""Provide AES encryption and decryption methods."""
BLOCK_SIZE = 16
PADDING = six.b('*')
def _initialize_engine(self, parent_class_key):
self.secret_key = parent_class_key
self.iv = self.secret_key[:16]
self.cipher = Cipher(
algorithms.AES(self.secret_key),
modes.CBC(self.iv),
backend=default_backend()
)
def _pad(self, value):
"""Pad the message to be encrypted, if needed."""
BS = self.BLOCK_SIZE
P = self.PADDING
padded = (value + (BS - len(value) % BS) * P)
return padded
def encrypt(self, value):
if not isinstance(value, six.string_types):
value = repr(value)
if isinstance(value, six.text_type):
value = str(value)
value = value.encode()
value = self._pad(value)
encryptor = self.cipher.encryptor()
encrypted = encryptor.update(value) + encryptor.finalize()
encrypted = base64.b64encode(encrypted)
return encrypted
def decrypt(self, value):
if isinstance(value, six.text_type):
value = str(value)
decryptor = self.cipher.decryptor()
decrypted = base64.b64decode(value)
decrypted = decryptor.update(decrypted) + decryptor.finalize()
decrypted = decrypted.rstrip(self.PADDING)
if not isinstance(decrypted, six.string_types):
decrypted = decrypted.decode('utf-8')
return decrypted
def migrate_to_fernet(aes_encrypted, old_key, new_key):
"""
Will attempt to migrate an aes encrypted to fernet encryption
:param aes_encrypted:
:return: fernet encrypted value
"""
engine = AesEngine()
engine._update_key(old_key)
if not isinstance(aes_encrypted, six.string_types):
return
aes_decrypted = engine.decrypt(aes_encrypted)
fernet_encrypted = MultiFernet([Fernet(k) for k in new_key]).encrypt(bytes(aes_decrypted))
# sanity check
fernet_decrypted = MultiFernet([Fernet(k) for k in new_key]).decrypt(fernet_encrypted)
if fernet_decrypted != aes_decrypted:
raise Exception("WARNING: Decrypted values do not match!")
return fernet_encrypted
def migrate_from_fernet(fernet_encrypted, old_key, new_key):
"""
Will attempt to migrate from a fernet encryption to aes
:param fernet_encrypted:
:return:
"""
engine = AesEngine()
engine._update_key(new_key)
fernet_decrypted = MultiFernet([Fernet(k) for k in old_key]).decrypt(fernet_encrypted)
aes_encrypted = engine.encrypt(fernet_decrypted)
# sanity check
aes_decrypted = engine.decrypt(aes_encrypted)
if fernet_decrypted != aes_decrypted:
raise Exception("WARNING: Decrypted values do not match!")
return aes_encrypted
def upgrade():
old_key = current_app.config.get('LEMUR_ENCRYPTION_KEY')
print "Using: {0} as decryption key".format(old_key)
# generate a new fernet token
if current_app.config.get('LEMUR_ENCRYPTION_KEYS'):
new_key = current_app.config.get('LEMUR_ENCRYPTION_KEYS')
else:
new_key = [Fernet.generate_key()]
print "Using: {0} as new encryption key, save this and place it in your configuration!".format(new_key)
# migrate private_keys
temp_keys = []
for id, private_key in conn.execute(text('select id, private_key from certificates where private_key is not null')):
aes_encrypted = StringIO(private_key).read()
fernet_encrypted = migrate_to_fernet(aes_encrypted, old_key, new_key)
temp_keys.append({'id': id, 'aes': aes_encrypted, 'fernet': fernet_encrypted})
op.bulk_insert(temp_key_table, temp_keys)
for id, fernet in conn.execute(text('select id, fernet from encrypted_keys')):
stmt = text("update certificates set private_key=:key where id=:id")
stmt = stmt.bindparams(key=fernet, id=id)
op.execute(stmt)
print "Certificate {0} has been migrated".format(id)
# migrate role_passwords
temp_passwords = []
for id, password in conn.execute(text('select id, password from roles where password is not null')):
aes_encrypted = StringIO(password).read()
fernet_encrypted = migrate_to_fernet(aes_encrypted, old_key, new_key)
temp_passwords.append({'id': id, 'aes': aes_encrypted, 'fernet': fernet_encrypted})
op.bulk_insert(temp_password_table, temp_passwords)
for id, fernet in conn.execute(text('select id, fernet from encrypted_passwords')):
stmt = text("update roles set password=:password where id=:id")
stmt = stmt.bindparams(password=fernet, id=id)
print stmt
op.execute(stmt)
print "Password {0} has been migrated".format(id)
op.drop_table('encrypted_keys')
op.drop_table('encrypted_passwords')
def downgrade():
old_key = current_app.config.get('LEMUR_ENCRYPTION_KEYS')
print "Using: {0} as decryption key(s)".format(old_key)
# generate aes valid key
if current_app.config.get('LEMUR_ENCRYPTION_KEY'):
new_key = current_app.config.get('LEMUR_ENCRYPTION_KEY')
else:
new_key = get_psuedo_random_string()
print "Using: {0} as the encryption key, save this and place it in your configuration!".format(new_key)
# migrate keys
temp_keys = []
for id, private_key in conn.execute(text('select id, private_key from certificates where private_key is not null')):
fernet_encrypted = StringIO(private_key).read()
aes_encrypted = migrate_from_fernet(fernet_encrypted, old_key, new_key)
temp_keys.append({'id': id, 'aes': aes_encrypted, 'fernet': fernet_encrypted})
op.bulk_insert(temp_key_table, temp_keys)
for id, aes in conn.execute(text('select id, aes from encrypted_keys')):
stmt = text("update certificates set private_key=:key where id=:id")
stmt = stmt.bindparams(key=aes, id=id)
print stmt
op.execute(stmt)
print "Certificate {0} has been migrated".format(id)
# migrate role_passwords
temp_passwords = []
for id, password in conn.execute(text('select id, password from roles where password is not null')):
fernet_encrypted = StringIO(password).read()
aes_encrypted = migrate_from_fernet(fernet_encrypted, old_key, new_key)
temp_passwords.append({'id': id, 'aes': aes_encrypted, 'fernet': fernet_encrypted})
op.bulk_insert(temp_password_table, temp_passwords)
for id, aes in conn.execute(text('select id, aes from encrypted_passwords')):
stmt = text("update roles set password=:password where id=:id")
stmt = stmt.bindparams(password=aes, id=id)
op.execute(stmt)
print "Password {0} has been migrated".format(id)
op.drop_table('encrypted_keys')
op.drop_table('encrypted_passwords')

View File

@ -36,6 +36,14 @@ certificate_notification_associations = db.Table('certificate_notification_assoc
Column('certificate_id', Integer, Column('certificate_id', Integer,
ForeignKey('certificates.id', ondelete='cascade')) ForeignKey('certificates.id', ondelete='cascade'))
) )
certificate_replacement_associations = db.Table('certificate_replacement_associations',
Column('replaced_certificate_id', Integer,
ForeignKey('certificates.id', ondelete='cascade')),
Column('certificate_id', Integer,
ForeignKey('certificates.id', ondelete='cascade'))
)
roles_users = db.Table('roles_users', roles_users = db.Table('roles_users',
Column('user_id', Integer, ForeignKey('users.id')), Column('user_id', Integer, ForeignKey('users.id')),
Column('role_id', Integer, ForeignKey('roles.id')) Column('role_id', Integer, ForeignKey('roles.id'))

View File

@ -108,10 +108,11 @@ class IPlugin(local):
""" """
return self.resource_links return self.resource_links
def get_option(self, name, options): @staticmethod
def get_option(name, options):
for o in options: for o in options:
if o.get('name') == name: if o.get('name') == name:
return o['value'] return o.get('value')
class Plugin(IPlugin): class Plugin(IPlugin):

View File

@ -2,3 +2,4 @@ from .destination import DestinationPlugin # noqa
from .issuer import IssuerPlugin # noqa from .issuer import IssuerPlugin # noqa
from .source import SourcePlugin # noqa from .source import SourcePlugin # noqa
from .notification import NotificationPlugin, ExpirationNotificationPlugin # noqa from .notification import NotificationPlugin, ExpirationNotificationPlugin # noqa
from .export import ExportPlugin # noqa

View File

@ -0,0 +1,20 @@
"""
.. module: lemur.bases.export
:platform: Unix
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
from lemur.plugins.base import Plugin
class ExportPlugin(Plugin):
"""
This is the base class from which all supported
exporters will inherit from.
"""
type = 'export'
def export(self):
raise NotImplemented

View File

@ -1,364 +0,0 @@
"""
.. module: lemur.common.services.issuers.plugins.cloudca
:platform: Unix
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
import re
import ssl
import base64
from json import dumps
import arrow
import requests
from requests.adapters import HTTPAdapter
from requests.exceptions import ConnectionError
from flask import current_app
from lemur.exceptions import LemurException
from lemur.plugins.bases import IssuerPlugin, SourcePlugin
from lemur.plugins import lemur_cloudca as cloudca
from lemur.authorities import service as authority_service
class CloudCAException(LemurException):
def __init__(self, message):
self.message = message
current_app.logger.error(self)
def __str__(self):
return repr("CloudCA request failed: {0}".format(self.message))
class CloudCAHostNameCheckingAdapter(HTTPAdapter):
def cert_verify(self, conn, url, verify, cert):
super(CloudCAHostNameCheckingAdapter, self).cert_verify(conn, url, verify, cert)
conn.assert_hostname = False
def remove_none(options):
"""
Simple function that traverse the options and removed any None items
CloudCA really dislikes null values.
:param options:
:return:
"""
new_dict = {}
for k, v in options.items():
if v:
new_dict[k] = v
# this is super hacky and gross, cloudca doesn't like null values
if new_dict.get('extensions'):
if len(new_dict['extensions']['subAltNames']['names']) == 0:
del new_dict['extensions']['subAltNames']
return new_dict
def get_default_issuance(options):
"""
Gets the default time range for certificates
:param options:
:return:
"""
if not options.get('validityStart') and not options.get('validityEnd'):
start = arrow.utcnow()
options['validityStart'] = start.floor('second').isoformat()
options['validityEnd'] = start.replace(years=current_app.config.get('CLOUDCA_DEFAULT_VALIDITY'))\
.ceil('second').isoformat()
return options
def convert_to_pem(der):
"""
Converts DER to PEM Lemur uses PEM internally
:param der:
:return:
"""
decoded = base64.b64decode(der)
return ssl.DER_cert_to_PEM_cert(decoded)
def convert_date_to_utc_time(date):
"""
Converts a python `datetime` object to the current date + current time in UTC.
:param date:
:return:
"""
d = arrow.get(date)
return arrow.utcnow().replace(year=d.naive.year).replace(month=d.naive.month).replace(day=d.naive.day)\
.replace(microsecond=0)
def process_response(response):
"""
Helper function that processes responses from CloudCA.
:param response:
:return: :raise CloudCAException:
"""
if response.status_code == 200:
res = response.json()
if res['returnValue'] != 'success':
current_app.logger.debug(res)
if res.get('data'):
raise CloudCAException(" ".join([res['returnMessage'], res['data']['dryRunResultMessage']]))
else:
raise CloudCAException(res['returnMessage'])
else:
raise CloudCAException("There was an error with your request: {0}".format(response.status_code))
return response.json()
def get_auth_data(ca_name):
"""
Creates the authentication record needed to authenticate a user request to CloudCA.
:param ca_name:
:return: :raise CloudCAException:
"""
role = authority_service.get_authority_role(ca_name)
if role:
return {
"authInfo": {
"credType": "password",
"credentials": {
"username": role.username,
"password": role.password # we only decrypt when we need to
}
}
}
raise CloudCAException("You do not have the required role to issue certificates from {0}".format(ca_name))
class CloudCA(object):
def __init__(self, *args, **kwargs):
self.session = requests.Session()
self.session.mount('https://', CloudCAHostNameCheckingAdapter())
self.url = current_app.config.get('CLOUDCA_URL')
if current_app.config.get('CLOUDCA_PEM_PATH') and current_app.config.get('CLOUDCA_BUNDLE'):
self.session.cert = current_app.config.get('CLOUDCA_PEM_PATH')
self.ca_bundle = current_app.config.get('CLOUDCA_BUNDLE')
else:
current_app.logger.warning(
"No CLOUDCA credentials found, lemur will be unable to request certificates from CLOUDCA"
)
super(CloudCA, self).__init__(*args, **kwargs)
def post(self, endpoint, data):
"""
HTTP POST to CloudCA
:param endpoint:
:param data:
:return:
"""
data = dumps(dict(data.items() + get_auth_data(data['caName']).items()))
# we set a low timeout, if cloudca is down it shouldn't bring down
# lemur
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):
"""
HTTP GET to CloudCA
:param endpoint:
:return:
"""
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):
"""
Uses CloudCA as a decent source of randomness.
:param length:
:return:
"""
endpoint = '/v1/random/{0}'.format(length)
response = self.session.get(self.url + endpoint, verify=self.ca_bundle)
return response
def get_authorities(self):
"""
Retrieves authorities that were made outside of Lemur.
:return:
"""
endpoint = '{0}/listCAs'.format(current_app.config.get('CLOUDCA_API_ENDPOINT'))
authorities = []
for ca in self.get(endpoint)['data']['caList']:
try:
authorities.append(ca['caName'])
except AttributeError:
current_app.logger.error("No authority has been defined for {}".format(ca['caName']))
return authorities
class CloudCAIssuerPlugin(IssuerPlugin, CloudCA):
title = 'CloudCA'
slug = 'cloudca-issuer'
description = 'Enables the creation of certificates from the cloudca API.'
version = cloudca.VERSION
author = 'Kevin Glisson'
author_url = 'https://github.com/netflix/lemur'
def create_authority(self, options):
"""
Creates a new certificate authority
:param options:
:return:
"""
# this is weird and I don't like it
endpoint = '{0}/createCA'.format(current_app.config.get('CLOUDCA_API_ENDPOINT'))
options['caDN']['email'] = options['ownerEmail']
if options['caType'] == 'subca':
options = dict(options.items() + self.auth_data(options['caParent']).items())
options['validityStart'] = convert_date_to_utc_time(options['validityStart']).isoformat()
options['validityEnd'] = convert_date_to_utc_time(options['validityEnd']).isoformat()
options['description'] = re.sub(r'[^a-zA-Z0-9]', '', options['caDescription'])
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 = []
for cred in json['data']['authInfo']:
role = {
'username': cred['credentials']['username'],
'password': cred['credentials']['password'],
'name': "_".join([options['caName'], cred['credentials']['username']])
}
roles.append(role)
if options['caType'] == 'subca':
cert = convert_to_pem(json['data']['certificate'])
else:
cert = convert_to_pem(json['data']['rootCertificate'])
intermediates = []
for i in json['data']['intermediateCertificates']:
intermediates.append(convert_to_pem(i))
return cert, "".join(intermediates), roles,
def create_certificate(self, csr, options):
"""
Creates a new certificate from cloudca
If no start and end date are specified the default issue range
will be used.
:param csr:
:param options:
"""
endpoint = '{0}/enroll'.format(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)
cloudca_options = {
'extensions': options['extensions'],
'validityStart': convert_date_to_utc_time(options['validityStart']).isoformat(),
'validityEnd': convert_date_to_utc_time(options['validityEnd']).isoformat(),
'creator': options['creator'],
'ownerEmail': options['owner'],
'caName': options['authority'].name,
'csr': csr,
'comment': re.sub(r'[^a-zA-Z0-9]', '', options['description'])
}
response = self.post(endpoint, remove_none(cloudca_options))
# we return a concatenated list of intermediate because that is what aws
# expects
cert = convert_to_pem(response['data']['certificate'])
intermediates = [convert_to_pem(response['data']['rootCertificate'])]
for i in response['data']['intermediateCertificates']:
intermediates.append(convert_to_pem(i))
return cert, "".join(intermediates),
class CloudCASourcePlugin(SourcePlugin, CloudCA):
title = 'CloudCA'
slug = 'cloudca-source'
description = 'Discovers all SSL certificates in CloudCA'
version = cloudca.VERSION
author = 'Kevin Glisson'
author_url = 'https://github.com/netflix/lemur'
options = {
'pollRate': {'type': 'int', 'default': '60'}
}
def get_certificates(self, options, **kwargs):
certs = []
for authority in self.get_authorities():
certs += self.get_cert(ca_name=authority)
return certs
def get_cert(self, ca_name=None, cert_handle=None):
"""
Returns a given cert from CloudCA.
:param ca_name:
:param cert_handle:
:return:
"""
endpoint = '{0}/getCert'.format(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)
certs = []
for c in raw['data']['certList']:
cert = convert_to_pem(c['certValue'])
intermediates = []
for i in c['intermediateCertificates']:
intermediates.append(convert_to_pem(i))
certs.append({
'public_certificate': cert,
'intermediate_certificate': "\n".join(intermediates),
'owner': c['ownerEmail']
})
return certs

View File

@ -1,5 +1,12 @@
import os import os
import arrow
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
loader = FileSystemLoader(searchpath=os.path.dirname(os.path.realpath(__file__))) loader = FileSystemLoader(searchpath=os.path.dirname(os.path.realpath(__file__)))
env = Environment(loader=loader) env = Environment(loader=loader)
def human_time(time):
return arrow.get(time).format('dddd, MMMM D, YYYY')
env.filters['time'] = human_time

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,16 @@
from lemur.plugins.lemur_email.templates.config import env
import os.path
def test_render():
messages = [{
'name': 'a-really-really-long-certificate-name',
'owner': 'bob@example.com',
'not_after': '2015-12-14 23:59:59'
}] * 10
template = env.get_template('{}.html'.format('expiration'))
body = template.render(dict(messages=messages, hostname='lemur.test.example.com'))
with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'email.html'), 'w+') as f:
f.write(body.encode('utf8'))

View File

@ -0,0 +1,175 @@
"""
.. module: lemur.plugins.lemur_java.plugin
:platform: Unix
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
import subprocess
from flask import current_app
from lemur.utils import mktempfile, mktemppath
from lemur.plugins.bases import ExportPlugin
from lemur.plugins import lemur_java as java
from lemur.common.utils import get_psuedo_random_string
def run_process(command):
"""
Runs a given command with pOpen and wraps some
error handling around it.
:param command:
:return:
"""
p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, stderr = p.communicate()
if p.returncode != 0:
current_app.logger.debug(" ".join(command))
current_app.logger.error(stderr)
raise Exception(stderr)
def split_chain(chain):
"""
Split the chain into individual certificates for import into keystore
:param chain:
:return:
"""
certs = []
lines = chain.split('\n')
cert = []
for line in lines:
cert.append(line + '\n')
if line == '-----END CERTIFICATE-----':
certs.append("".join(cert))
cert = []
return certs
class JavaExportPlugin(ExportPlugin):
title = 'Java'
slug = 'java-export'
description = 'Attempts to generate a JKS keystore or truststore'
version = java.VERSION
author = 'Kevin Glisson'
author_url = 'https://github.com/netflix/lemur'
options = [
{
'name': 'type',
'type': 'select',
'required': True,
'available': ['Java Key Store (JKS)'],
'helpMessage': 'Choose the format you wish to export',
},
{
'name': 'passphrase',
'type': 'str',
'required': False,
'helpMessage': 'If no passphrase is given one will be generated for you, we highly recommend this. Minimum length is 8.',
'validation': '^(?=.*[A-Za-z])(?=.*\d)(?=.*[$@$!%*#?&])[A-Za-z\d$@$!%*#?&]{8,}$'
},
{
'name': 'alias',
'type': 'str',
'required': False,
'helpMessage': 'Enter the alias you wish to use for the keystore.',
}
]
def export(self, body, chain, key, options, **kwargs):
"""
Generates a Java Keystore or Truststore
:param key:
:param chain:
:param body:
:param options:
:param kwargs:
"""
if self.get_option('passphrase', options):
passphrase = self.get_option('passphrase', options)
else:
passphrase = get_psuedo_random_string()
if self.get_option('alias', options):
alias = self.get_option('alias', options)
else:
alias = "blah"
if not key:
raise Exception("Unable to export, no private key found.")
with mktempfile() as cert_tmp:
with open(cert_tmp, 'w') as f:
f.write(body)
with mktempfile() as key_tmp:
with open(key_tmp, 'w') as f:
f.write(key)
# Create PKCS12 keystore from private key and public certificate
with mktempfile() as p12_tmp:
run_process([
"openssl",
"pkcs12",
"-export",
"-name", alias,
"-in", cert_tmp,
"-inkey", key_tmp,
"-out", p12_tmp,
"-password", "pass:{}".format(passphrase)
])
# Convert PKCS12 keystore into a JKS keystore
with mktemppath() as jks_tmp:
run_process([
"keytool",
"-importkeystore",
"-destkeystore", jks_tmp,
"-srckeystore", p12_tmp,
"-srcstoretype", "PKCS12",
"-alias", alias,
"-srcstorepass", passphrase,
"-deststorepass", passphrase
])
# Import leaf cert in to JKS keystore
run_process([
"keytool",
"-importcert",
"-file", cert_tmp,
"-keystore", jks_tmp,
"-alias", "{0}_cert".format(alias),
"-storepass", passphrase,
"-noprompt"
])
# Import the entire chain
for idx, cert in enumerate(split_chain(chain)):
with mktempfile() as c_tmp:
with open(c_tmp, 'w') as f:
f.write(cert)
# Import signed cert in to JKS keystore
run_process([
"keytool",
"-importcert",
"-file", c_tmp,
"-keystore", jks_tmp,
"-alias", "{0}_cert_{1}".format(alias, idx),
"-storepass", passphrase,
"-noprompt"
])
with open(jks_tmp, 'rb') as f:
raw = f.read()
return "jks", passphrase, raw

View File

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

View File

@ -0,0 +1,63 @@
PRIVATE_KEY_STR = b"""
-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEAsXn+QZRATxryRmGXI4fdI+0a2oBwuVh8fC/9bcqX6c5eDmgc
rj6esmc1hpIFxMM3DvkFXX6xISkU6B5fmYDEGZLi7NvcXF3+EoA/SCkP1MFlvqhn
EvNhb0t1fBLs0i/0gfTS/FHBZY1ekHisd/sUetCDZ7F11RxMwws0Oc8bl7j1TpRc
awXFAsh/aWwQOwFeyWU7TtZeAE7sMyWXInBg37tKk1wlv+mN+27WijI091+amkVy
zIV6mA5OHfqbjuqV8uQflN8jE244Qr7shtSk7LpBpWf0M6dC7dXbuUctHFhqcDjy
3IRUl+NisKRoMtq+a0uehfmpFNSUD7F4gdUtSwIDAQABAoIBAGITsZ+aBuPwVzzv
x286MMoeyL1BR4oVzU1v09Rtpf/uLGo3vMnKDzc19A12+rseynl6wi1FyysxIb2Y
s2oID9a2JrOQWLmus66TsuT01CvV6J0xQSzm1MyFXdqANuF84NlEa6hGoeK1+jFK
jr0LQukP+9484oovxnfu5CCiRHRWNZmeuekuYhI1SJf343Tr6jwvyr6KZpnIy0Yt
axuuIZdCfY9ZV2vFG89GwwgwVQrhf14Kv5vBMZrNh1lRGsr0Sqlx5cGkPRAy90lg
HjrRMogrtXr3AR5Pk2qqAYXzZBU2EFhJ3k2njpwOzlSj0r0ZwTmejZ89cco0sW5j
+eQ6aRECgYEA1tkNW75fgwU52Va5VETCzG8II/pZdqNygnoc3z8EutN+1w8f6Tr+
PdpKSICW0z7Iq4f5k/4wrA5xw1vy5RBMH0ZP29GwHTvCPiTBboR9vWvxQvZn1jb9
wvKa0RxE18KcF0YIyTnZMubkA17QTFlvCNyZg0iCqeyFYPyqVE+R4AkCgYEA03h1
XrqECZDDbG9HLUdGbkZNk4VzTcF6dQ3GAPY8M/H7rw5BbvH0RZLOrzl46DDVzKTg
B1VOReAHsxBKFdkqeq1A99CLDow6vHTIEG8DwxkA7/2QPkt8MybwdApUyYnQh5/v
CxwkRt4Mm+EiYfn5iyL8yI+vaQSRToVO/3BND7MCgYAJQSpBJG8qzqPSR9kN1zRo
5/N60ULfSGUbV7U8rJNAlPGmw+EFA+SFt4xxmRBmIxMzyFSo2k8waiLeXmyVD2Go
CzhPaLXkXHmegajPYOelrCulTcXlRVMi/Z5LmaMhhCGDIyInwNUpSybROllQoJ2W
zSHTtODj/usz5U5U+WR4OQKBgHQRosI6t2wUo96peTS18UdnmP7GeZINBuymga5X
eJW+VLkxpuKBNOTW/lCYx+8Rlte7CyebP9oEa9VxtGgniTRKUeVy9lAm0bpMkt7K
QBNebvBKiVhX0DS3Q7U9UmpIFUfLlcXQTW0ERYFtYZTLQpeGvZ5LlyiaFDM34jM7
7WAXAoGANDPJdQLEuimCOAMx/xoecNWeZIP6ieB0hVBrwLNxsaZlkn1KodUMuvla
VEowbtPRdc9o3VZRh4q9cEakssTvOD70hgUZCFcMarmc37RgRvvD2fsZmDZF6qd3
QfHplREs9F0sW+eiirczG7up4XL+CA162TtZxW+2GAiQhwhE5jA=
-----END RSA PRIVATE KEY-----
"""
EXTERNAL_VALID_STR = b"""
-----BEGIN CERTIFICATE-----
MIID2zCCAsOgAwIBAgICA+0wDQYJKoZIhvcNAQELBQAwgZcxCzAJBgNVBAYTAlVT
MRMwEQYDVQQIDApDYWxpZm9ybmlhMRIwEAYDVQQHDAlMb3MgR2F0b3MxDTALBgNV
BAMMBHRlc3QxFjAUBgNVBAoMDU5ldGZsaXgsIEluYy4xEzARBgNVBAsMCk9wZXJh
dGlvbnMxIzAhBgkqhkiG9w0BCQEWFGtnbGlzc29uQG5ldGZsaXguY29tMB4XDTE1
MTEyMzIxNDIxMFoXDTE1MTEyNjIxNDIxMFowcjENMAsGA1UEAwwEdGVzdDEWMBQG
A1UECgwNTmV0ZmxpeCwgSW5jLjETMBEGA1UECwwKT3BlcmF0aW9uczELMAkGA1UE
BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExEjAQBgNVBAcMCUxvcyBHYXRvczCC
ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALF5/kGUQE8a8kZhlyOH3SPt
GtqAcLlYfHwv/W3Kl+nOXg5oHK4+nrJnNYaSBcTDNw75BV1+sSEpFOgeX5mAxBmS
4uzb3Fxd/hKAP0gpD9TBZb6oZxLzYW9LdXwS7NIv9IH00vxRwWWNXpB4rHf7FHrQ
g2exddUcTMMLNDnPG5e49U6UXGsFxQLIf2lsEDsBXsllO07WXgBO7DMllyJwYN+7
SpNcJb/pjftu1ooyNPdfmppFcsyFepgOTh36m47qlfLkH5TfIxNuOEK+7IbUpOy6
QaVn9DOnQu3V27lHLRxYanA48tyEVJfjYrCkaDLavmtLnoX5qRTUlA+xeIHVLUsC
AwEAAaNVMFMwUQYDVR0fBEowSDBGoESgQoZAaHR0cDovL3Rlc3QuY2xvdWRjYS5j
cmwubmV0ZmxpeC5jb20vdGVzdERlY3JpcHRpb25DQVJvb3QvY3JsLnBlbTANBgkq
hkiG9w0BAQsFAAOCAQEAiHREBKg7zhlQ/N7hDIkxgodRSWD7CVbJGSCdkR3Pvr6+
jHBVNTJUrYqy7sL2pIutoeiSTQEH65/Gbm30mOnNu+lvFKxTxzof6kNYv8cyc8sX
eBuBfSrlTodPFSHXQIpOexZgA0f30LOuXegqzxgXkKg+uMXOez5Zo5pNjTUow0He
oe+V1hfYYvL1rocCmBOkhIGWz7622FxKDawRtZTGVsGsMwMIWyvS3+KQ04K8yHhp
bQOg9zZAoYQuHY1inKBnA0II8eW0hPpJrlZoSqN8Tp0NSBpFiUk3m7KNFP2kITIf
tTneAgyUsgfDxNDifZryZSzg7MH31sTBcYaotSmTXw==
-----END CERTIFICATE-----
"""
def test_export_certificate_to_jks(app):
from lemur.plugins.base import plugins
p = plugins.get('java-export')
options = {'passphrase': 'test1234'}
raw = p.export(EXTERNAL_VALID_STR, "", PRIVATE_KEY_STR, options)
assert raw != b""

View File

@ -86,7 +86,7 @@ class PluginsList(AuthenticatedResource):
if args['type']: if args['type']:
return list(plugins.all(plugin_type=args['type'])) return list(plugins.all(plugin_type=args['type']))
return plugins.all() return list(plugins.all())
class Plugins(AuthenticatedResource): class Plugins(AuthenticatedResource):

View File

@ -12,9 +12,8 @@
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from sqlalchemy import Column, Integer, String, Text, ForeignKey from sqlalchemy import Column, Integer, String, Text, ForeignKey
from sqlalchemy_utils import EncryptedType
from lemur.database import db from lemur.database import db
from lemur.utils import get_key from lemur.utils import Vault
from lemur.models import roles_users from lemur.models import roles_users
@ -23,7 +22,7 @@ class Role(db.Model):
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
name = Column(String(128), unique=True) name = Column(String(128), unique=True)
username = Column(String(128)) username = Column(String(128))
password = Column(EncryptedType(String, get_key)) password = Column(Vault)
description = Column(Text) description = Column(Text)
authority_id = Column(Integer, ForeignKey('authorities.id')) authority_id = Column(Integer, ForeignKey('authorities.id'))
user_id = Column(Integer, ForeignKey('users.id')) user_id = Column(Integer, ForeignKey('users.id'))

View File

@ -2,7 +2,7 @@
var lemur = angular var lemur = angular
.module('lemur', [ .module('lemur', [
'ngRoute', 'ui.router',
'ngTable', 'ngTable',
'ngAnimate', 'ngAnimate',
'chart.js', 'chart.js',
@ -13,15 +13,18 @@ var lemur = angular
'toaster', 'toaster',
'uiSwitch', 'uiSwitch',
'mgo-angular-wizard', 'mgo-angular-wizard',
'satellizer' 'satellizer',
'ngLetterAvatar',
'angular-clipboard',
'ngFileSaver'
]) ])
.config(function ($routeProvider, $authProvider) { .config(function ($stateProvider, $urlRouterProvider, $authProvider) {
$routeProvider $urlRouterProvider.otherwise('/welcome');
.when('/', {
$stateProvider
.state('welcome', {
url: '/welcome',
templateUrl: 'angular/welcome/welcome.html' templateUrl: 'angular/welcome/welcome.html'
})
.otherwise({
redirectTo: '/'
}); });
$authProvider.oauth2({ $authProvider.oauth2({
@ -71,7 +74,7 @@ lemur.service('DefaultService', function (LemurRestangular) {
lemur.factory('LemurRestangular', function (Restangular, $location, $auth) { lemur.factory('LemurRestangular', function (Restangular, $location, $auth) {
return Restangular.withConfig(function (RestangularConfigurer) { return Restangular.withConfig(function (RestangularConfigurer) {
RestangularConfigurer.setBaseUrl('http://localhost:5000/api/1'); RestangularConfigurer.setBaseUrl('http://localhost:8000/api/1');
RestangularConfigurer.setDefaultHttpFields({withCredentials: true}); RestangularConfigurer.setDefaultHttpFields({withCredentials: true});
RestangularConfigurer.addResponseInterceptor(function (data, operation) { RestangularConfigurer.addResponseInterceptor(function (data, operation) {
@ -85,9 +88,22 @@ lemur.factory('LemurRestangular', function (Restangular, $location, $auth) {
} else { } else {
extractedData = data; extractedData = data;
} }
return extractedData; return extractedData;
}); });
RestangularConfigurer.setErrorInterceptor(function(response) {
if (response.status === 400) {
if (response.data.message) {
var data = '';
_.each(response.data.message, function (value, key) {
data = data + ' ' + key + ' ' + value;
});
response.data.message = data;
}
}
});
RestangularConfigurer.addFullRequestInterceptor(function (element, operation, route, url, headers, params) { RestangularConfigurer.addFullRequestInterceptor(function (element, operation, route, url, headers, params) {
// We want to make sure the user is auth'd before any requests // We want to make sure the user is auth'd before any requests
if (!$auth.isAuthenticated()) { if (!$auth.isAuthenticated()) {

View File

@ -1,8 +1,9 @@
'use strict'; 'use strict';
angular.module('lemur') angular.module('lemur')
.config(function config($routeProvider) { .config(function config($stateProvider) {
$routeProvider.when('/login', { $stateProvider.state('login', {
url: '/login',
templateUrl: '/angular/authentication/login/login.tpl.html', templateUrl: '/angular/authentication/login/login.tpl.html',
controller: 'LoginController' controller: 'LoginController'
}); });

View File

@ -1,9 +1,10 @@
'use strict'; 'use strict';
angular.module('lemur') angular.module('lemur')
.config(function config($routeProvider) { .config(function config($stateProvider) {
$routeProvider.when('/logout', { $stateProvider.state('logout', {
controller: 'LogoutCtrl' controller: 'LogoutCtrl',
url: '/logout'
}); });
}) })
.controller('LogoutCtrl', function ($scope, $location, lemurRestangular, userService) { .controller('LogoutCtrl', function ($scope, $location, lemurRestangular, userService) {

View File

@ -2,24 +2,32 @@
angular.module('lemur') angular.module('lemur')
.controller('AuthorityEditController', function ($scope, $modalInstance, AuthorityApi, AuthorityService, RoleService, editId){ .controller('AuthorityEditController', function ($scope, $modalInstance, AuthorityApi, AuthorityService, RoleService, toaster, editId){
AuthorityApi.get(editId).then(function (authority) { AuthorityApi.get(editId).then(function (authority) {
AuthorityService.getRoles(authority); AuthorityService.getRoles(authority);
$scope.authority = authority; $scope.authority = authority;
}); });
$scope.authorityService = AuthorityService;
$scope.roleService = RoleService; $scope.roleService = RoleService;
$scope.save = function (authority) { $scope.save = function (authority) {
AuthorityService.update(authority).then( AuthorityService.update(authority).then(
function () { function () {
toaster.pop({
type: 'success',
title: authority.name,
body: 'Successfully updated!'
});
$modalInstance.close(); $modalInstance.close();
}, },
function () { function (response) {
toaster.pop({
} type: 'error',
); title: authority.name,
body: 'Update Failed! ' + response.data.message,
timeout: 100000
});
});
}; };
$scope.cancel = function () { $scope.cancel = function () {
@ -27,18 +35,31 @@ angular.module('lemur')
}; };
}) })
.controller('AuthorityCreateController', function ($scope, $modalInstance, AuthorityService, LemurRestangular, RoleService, PluginService, WizardHandler) { .controller('AuthorityCreateController', function ($scope, $modalInstance, AuthorityService, LemurRestangular, RoleService, PluginService, WizardHandler, toaster) {
$scope.authority = LemurRestangular.restangularizeElement(null, {}, 'authorities'); $scope.authority = LemurRestangular.restangularizeElement(null, {}, 'authorities');
// set the defaults // set the defaults
AuthorityService.getDefaults($scope.authority); AuthorityService.getDefaults($scope.authority);
$scope.loading = false;
$scope.create = function (authority) { $scope.create = function (authority) {
WizardHandler.wizard().context.loading = true; WizardHandler.wizard().context.loading = true;
AuthorityService.create(authority).then(function () { AuthorityService.create(authority).then(
WizardHandler.wizard().context.loading = false; function () {
$modalInstance.close(); toaster.pop({
type: 'success',
title: authority.name,
body: 'Was created!'
});
$modalInstance.close();
},
function (response) {
toaster.pop({
type: 'error',
title: authority.name,
body: 'Was not created! ' + response.data.message,
timeout: 100000
});
WizardHandler.wizard().context.loading = false;
}); });
}; };

View File

@ -56,7 +56,7 @@ angular.module('lemur')
}); });
return LemurRestangular.all('authorities'); return LemurRestangular.all('authorities');
}) })
.service('AuthorityService', function ($location, AuthorityApi, DefaultService, toaster) { .service('AuthorityService', function ($location, AuthorityApi, DefaultService) {
var AuthorityService = this; var AuthorityService = this;
AuthorityService.findAuthorityByName = function (filterValue) { AuthorityService.findAuthorityByName = function (filterValue) {
return AuthorityApi.getList({'filter[name]': filterValue}) return AuthorityApi.getList({'filter[name]': filterValue})
@ -80,41 +80,11 @@ angular.module('lemur')
AuthorityService.create = function (authority) { AuthorityService.create = function (authority) {
authority.attachSubAltName(); authority.attachSubAltName();
return AuthorityApi.post(authority).then( return AuthorityApi.post(authority);
function () {
toaster.pop({
type: 'success',
title: authority.name,
body: 'Successfully created!'
});
$location.path('/authorities');
},
function (response) {
toaster.pop({
type: 'error',
title: authority.name,
body: 'Was not created! ' + response.data.message
});
});
}; };
AuthorityService.update = function (authority) { AuthorityService.update = function (authority) {
return authority.put().then( return authority.put();
function () {
toaster.pop({
type: 'success',
title: authority.name,
body: 'Successfully updated!'
});
$location.path('/authorities');
},
function (response) {
toaster.pop({
type: 'error',
title: authority.name,
body: 'Update Failed! ' + response.data.message
});
});
}; };
AuthorityService.getDefaults = function (authority) { AuthorityService.getDefaults = function (authority) {
@ -134,20 +104,7 @@ angular.module('lemur')
}; };
AuthorityService.updateActive = function (authority) { AuthorityService.updateActive = function (authority) {
return authority.put().then( return authority.put();
function () {
toaster.pop({
type: 'success',
title: authority.name,
body: 'Successfully updated!'
});
},
function (response) {
toaster.pop({
type: 'error',
title: authority.name,
body: 'Update Failed! ' + response.data.message
});
});
}; };
}); });

View File

@ -2,15 +2,22 @@
angular.module('lemur') angular.module('lemur')
.config(function config($routeProvider) { .config(function config($stateProvider) {
$routeProvider.when('/authorities', { $stateProvider
templateUrl: '/angular/authorities/view/view.tpl.html', .state('authorities', {
controller: 'AuthoritiesViewController' url: '/authorities',
}); templateUrl: '/angular/authorities/view/view.tpl.html',
controller: 'AuthoritiesViewController'
})
.state('authority', {
url: '/authorities/:name',
templateUrl: '/angular/authorities/view/view.tpl.html',
controller: 'AuthoritiesViewController'
});
}) })
.controller('AuthoritiesViewController', function ($scope, $q, $modal, AuthorityApi, AuthorityService, ngTableParams) { .controller('AuthoritiesViewController', function ($scope, $q, $modal, $stateParams, AuthorityApi, AuthorityService, ngTableParams, toaster) {
$scope.filter = {}; $scope.filter = $stateParams;
$scope.authoritiesTable = new ngTableParams({ $scope.authoritiesTable = new ngTableParams({
page: 1, // show first page page: 1, // show first page
count: 10, // count per page count: 10, // count per page
@ -31,7 +38,24 @@ angular.module('lemur')
} }
}); });
$scope.authorityService = AuthorityService; $scope.updateActive = function (authority) {
AuthorityService.updateActive(authority).then(
function () {
toaster.pop({
type: 'success',
title: authority.name,
body: 'Successfully updated!'
});
},
function (response) {
toaster.pop({
type: 'error',
title: authority.name,
body: 'Update Failed! ' + response.data.message,
timeout: 100000
});
});
};
$scope.getAuthorityStatus = function () { $scope.getAuthorityStatus = function () {
var def = $q.defer(); var def = $q.defer();

View File

@ -24,7 +24,7 @@
</td> </td>
<td data-title="'Active'" filter="{ 'active': 'select' }" filter-data="getAuthorityStatus()"> <td data-title="'Active'" filter="{ 'active': 'select' }" filter-data="getAuthorityStatus()">
<form> <form>
<switch ng-change="authorityService.updateActive(authority)" id="status" name="status" ng-model="authority.active" class="green small"></switch> <switch ng-change="updateActive(authority)" id="status" name="status" ng-model="authority.active" class="green small"></switch>
</form> </form>
</td> </td>
<td data-title="'Roles'"> <!--filter="{ 'select': 'role' }" filter-data="roleService.getRoleDropDown()">--> <td data-title="'Roles'"> <!--filter="{ 'select': 'role' }" filter-data="roleService.getRoleDropDown()">-->
@ -35,7 +35,8 @@
</div> </div>
</td> </td>
<td data-title="''"> <td data-title="''">
<div class="btn-group-vertical pull-right"> <div class="btn-group pull-right">
<a class="btn btn-sm btn-default" ui-sref="authority({name: authority.name})">Permalink</a>
<button tooltip="Edit Authority" ng-click="edit(authority.id)" class="btn btn-sm btn-info"> <button tooltip="Edit Authority" ng-click="edit(authority.id)" class="btn btn-sm btn-info">
Edit Edit
</button> </button>

View File

@ -1,10 +1,57 @@
'use strict'; 'use strict';
angular.module('lemur') angular.module('lemur')
.controller('CertificateEditController', function ($scope, $modalInstance, CertificateApi, CertificateService, DestinationService, NotificationService, editId) { .controller('CertificateExportController', function ($scope, $modalInstance, CertificateApi, CertificateService, PluginService, FileSaver, Blob, toaster, editId) {
CertificateApi.get(editId).then(function (certificate) {
$scope.certificate = certificate;
});
PluginService.getByType('export').then(function (plugins) {
$scope.plugins = plugins;
});
$scope.cancel = function () {
$modalInstance.dismiss('cancel');
};
$scope.save = function (certificate) {
CertificateService.export(certificate).then(
function (response) {
var byteCharacters = atob(response.data);
var byteArrays = [];
for (var offset = 0; offset < byteCharacters.length; offset += 512) {
var slice = byteCharacters.slice(offset, offset + 512);
var byteNumbers = new Array(slice.length);
for (var i = 0; i < slice.length; i++) {
byteNumbers[i] = slice.charCodeAt(i);
}
var byteArray = new Uint8Array(byteNumbers);
byteArrays.push(byteArray);
}
var blob = new Blob(byteArrays, {type: 'application/octet-stream'});
FileSaver.saveAs(blob, certificate.name + '.' + response.extension);
$scope.passphrase = response.passphrase;
},
function (response) {
toaster.pop({
type: 'error',
title: certificate.name,
body: 'Failed to export ' + response.data.message,
timeout: 100000
});
});
};
})
.controller('CertificateEditController', function ($scope, $modalInstance, CertificateApi, CertificateService, DestinationService, NotificationService, toaster, editId) {
CertificateApi.get(editId).then(function (certificate) { CertificateApi.get(editId).then(function (certificate) {
CertificateService.getNotifications(certificate); CertificateService.getNotifications(certificate);
CertificateService.getDestinations(certificate); CertificateService.getDestinations(certificate);
CertificateService.getReplacements(certificate);
$scope.certificate = certificate; $scope.certificate = certificate;
}); });
@ -13,16 +60,31 @@ angular.module('lemur')
}; };
$scope.save = function (certificate) { $scope.save = function (certificate) {
CertificateService.update(certificate).then(function () { CertificateService.update(certificate).then(
$modalInstance.close(); function () {
}); toaster.pop({
type: 'success',
title: certificate.name,
body: 'Successfully updated!'
});
$modalInstance.close();
},
function (response) {
toaster.pop({
type: 'error',
title: certificate.name,
body: 'Failed to update ' + response.data.message,
timeout: 100000
});
});
}; };
$scope.certificateService = CertificateService;
$scope.destinationService = DestinationService; $scope.destinationService = DestinationService;
$scope.notificationService = NotificationService; $scope.notificationService = NotificationService;
}) })
.controller('CertificateCreateController', function ($scope, $modalInstance, CertificateApi, CertificateService, DestinationService, AuthorityService, PluginService, MomentService, WizardHandler, LemurRestangular, NotificationService) { .controller('CertificateCreateController', function ($scope, $modalInstance, CertificateApi, CertificateService, DestinationService, AuthorityService, PluginService, MomentService, WizardHandler, LemurRestangular, NotificationService, toaster) {
$scope.certificate = LemurRestangular.restangularizeElement(null, {}, 'certificates'); $scope.certificate = LemurRestangular.restangularizeElement(null, {}, 'certificates');
// set the defaults // set the defaults
@ -30,10 +92,24 @@ angular.module('lemur')
$scope.create = function (certificate) { $scope.create = function (certificate) {
WizardHandler.wizard().context.loading = true; WizardHandler.wizard().context.loading = true;
CertificateService.create(certificate).then(function () { CertificateService.create(certificate).then(
WizardHandler.wizard().context.loading = false; function () {
$modalInstance.close(); toaster.pop({
}); type: 'success',
title: certificate.name,
body: 'Successfully created!'
});
$modalInstance.close();
},
function (response) {
toaster.pop({
type: 'error',
title: certificate.name,
body: 'Was not created! ' + response.data.message,
timeout: 100000
});
WizardHandler.wizard().context.loading = false;
});
}; };
$scope.templates = [ $scope.templates = [
@ -95,6 +171,7 @@ angular.module('lemur')
$scope.plugins = plugins; $scope.plugins = plugins;
}); });
$scope.certificateService = CertificateService;
$scope.authorityService = AuthorityService; $scope.authorityService = AuthorityService;
$scope.destinationService = DestinationService; $scope.destinationService = DestinationService;
$scope.notificationService = NotificationService; $scope.notificationService = NotificationService;

View File

@ -23,10 +23,11 @@
Description Description
</label> </label>
<div class="col-sm-10"> <div class="col-sm-10">
<textarea name="description" ng-model="certificate.description" placeholder="Something elegant" class="form-control" ng-pattern="/^[\w\-\s]+$/" required></textarea> <textarea name="description" ng-model="certificate.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> <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> </div>
<div ng-include="'angular/certificates/certificate/replaces.tpl.html'"></div>
<div ng-include="'angular/certificates/certificate/notifications.tpl.html'"></div> <div ng-include="'angular/certificates/certificate/notifications.tpl.html'"></div>
<div ng-include="'angular/certificates/certificate/destinations.tpl.html'"></div> <div ng-include="'angular/certificates/certificate/destinations.tpl.html'"></div>
</form> </form>

View File

@ -0,0 +1,42 @@
<div class="modal-header">
<div class="modal-title">
<h3 class="modal-header">Export <span class="text-muted"><small>{{ certificate.name }}</small></span></h3>
</div>
<div class="modal-body">
<form ng-show="!passphrase" name="exportForm" class="form-horizontal" role="form" novalidate>
<div class="form-group">
<label class="control-label col-sm-2">
Plugin
</label>
<div class="col-sm-10">
<select class="form-control" ng-model="certificate.export.plugin" ng-options="plugin.title for plugin in plugins" required></select>
</div>
</div>
<div class="form-group" ng-repeat="item in certificate.export.plugin.pluginOptions">
<ng-form name="subForm" class="form-horizontal" role="form" novalidate>
<div ng-class="{'has-error': subForm.sub.$invalid, 'has-success': !subForm.sub.$invalid&&subForm.sub.$dirty}">
<label class="control-label col-sm-2">
{{ ::item.name | titleCase }}
</label>
<div class="col-sm-10">
<input name="sub" ng-if="item.type == 'int'" type="number" ng-pattern="/^[0-9]{12,12}$/" class="form-control" ng-model="item.value"/>
<select name="sub" ng-if="item.type == 'select'" class="form-control" ng-options="i for i in item.available" ng-model="item.value"></select>
<input name="sub" ng-if="item.type == 'bool'" class="form-control" type="checkbox" ng-model="item.value">
<input name="sub" ng-if="item.type == 'str'" type="text" class="form-control" ng-model="item.value" ng-pattern="{{ ::item.validation }}"/>
<p ng-show="subForm.sub.$invalid && !subForm.sub.$pristine" class="help-block">{{ ::item.helpMessage }}</p>
</div>
</div>
</ng-form>
</div>
</form>
<div ng-show="passphrase">
<h3>Successfully exported!</h3>
<h4>You're passphrase is: <strong>{{ passphrase }}</strong></h4>
<p ng-show="additional">{{ additional }}</p>
</div>
</div>
<div class="modal-footer">
<button type="submit" ng-show="!passphrase" ng-click="save(certificate)" ng-disabled="exportForm.$invalid" class="btn btn-success">Export</button>
<button ng-click="cancel()" class="btn btn-danger">{{ passphrase ? "Close" : "Cancel" }}</button>
</div>
</div>

View File

@ -0,0 +1,28 @@
<div class="form-group">
<label class="control-label col-sm-2">
Replaces
</label>
<div class="col-sm-10">
<div class="input-group">
<input type="text" ng-model="certificate.selectedReplacement" placeholder="Certificate123..."
typeahead="certificate.name for certificate in certificateService.findCertificatesByName($viewValue)" typeahead-loading="loadingCertificates"
class="form-control input-md" typeahead-on-select="certificate.attachReplacement($item)" typeahead-min-wait="100"
tooltip="Lemur will mark any certificates being replaced as 'inactive'"
tooltip-trigger="focus" tooltip-placement="top">
<span class="input-group-btn">
<button ng-model="replacements.show" class="btn btn-md btn-default" btn-checkbox btn-checkbox-true="1" btn-checkbox-false="0">
<span class="badge">{{ certificate.replacements.length || 0 }}</span>
</button>
</span>
</div>
<table class="table">
<tr ng-repeat="replacement in certificate.replacements track by $index">
<td><a class="btn btn-sm btn-info">{{ replacement.name }}</a></td>
<td><span class="text-muted">{{ replacement.description }}</span></td>
<td>
<button type="button" ng-click="certificate.removeReplacement($index)" class="btn btn-danger btn-sm pull-right">Remove</button>
</td>
</tr>
</table>
</div>
</div>

View File

@ -1,84 +1,118 @@
<form name="trackingForm" novalidate> <form name="trackingForm" novalidate>
<div class="form-horizontal"> <div class="form-horizontal">
<div class="form-group" <div class="form-group"
ng-class="{'has-error': trackingForm.ownerEmail.$invalid, 'has-success': !trackingForm.$invalid&&trackingForm.ownerEmail.$dirty}"> ng-class="{'has-error': trackingForm.ownerEmail.$invalid, 'has-success': !trackingForm.$invalid&&trackingForm.ownerEmail.$dirty}">
<label class="control-label col-sm-2"> <label class="control-label col-sm-2">
Owner Owner
</label> </label>
<div class="col-sm-10">
<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/> <div class="col-sm-10">
<p ng-show="trackingForm.ownerEmail.$invalid && !trackingForm.ownerEmail.$pristine" class="help-block">You must enter an Certificate owner</p> <input type="email" name="ownerEmail" ng-model="certificate.owner" placeholder="TeamDL@example.com"
</div> tooltip="This is the certificates team distribution list or main point of contact" class="form-control"
</div> required/>
<div class="form-group"
ng-class="{'has-error': trackingForm.description.$invalid, 'has-success': !trackingForm.$invalid&&trackingForm.description.$dirty}"> <p ng-show="trackingForm.ownerEmail.$invalid && !trackingForm.ownerEmail.$pristine" class="help-block">You must
<label class="control-label col-sm-2"> enter an Certificate owner</p>
Description </div>
</label>
<div class="col-sm-10">
<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"
ng-class="{'has-error': trackingForm.selectedAuthority.$invalid, 'has-success': !trackingForm.$invalid&&trackingForm.selectedAuthority.$dirty}">
<label class="control-label col-sm-2">
Certificate Authority
</label>
<div class="col-sm-10">
<div class="input-group col-sm-12">
<input name="selectedAuthority" tooltip="If you are unsure which authority you need; you most likely want to use 'verisign'" type="text" ng-model="certificate.selectedAuthority" placeholder="Authority Name" typeahead-on-select="certificate.attachAuthority($item)"
typeahead="authority.name for authority in authorityService.findActiveAuthorityByName($viewValue)" typeahead-loading="loadingAuthorities"
class="form-control" typeahead-wait-ms="100" typeahead-template-url="angular/authorities/authority/select.tpl.html" required>
</div>
</div>
</div>
<div ng-show="certificate.authority" class="form-group">
<label class="control-label col-sm-2">
Certificate Template
</label>
<div class="col-sm-10">
<select class="form-control" ng-change="certificate.useTemplate()" name="certificateTemplate" ng-model="certificate.template" ng-options="template.name for template in templates"></select>
</div>
</div>
<div class="form-group"
ng-class="{'has-error': trackingForm.commonName.$invalid, 'has-success': !trackingForm.$invalid&&trackingForm.commonName.$dirty}">
<label class="control-label col-sm-2">
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 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">
<label class="control-label col-sm-2" tooltip="If no date is selected Lemur attempts to issue a 2 year certificate">
Validity Range <span class="glyphicon glyphicon-question-sign"></span>
</label>
<div class="col-sm-4">
<div>
<div class="input-group">
<input tooltip="Starting Date (yyyy/MM/dd)" class="form-control" datepicker-popup="yyyy/MM/dd" is-open="$parent.openNotBefore.isOpen" min-date="certificate.authority.notBefore" max-date="certificate.authority.maxDate" ng-model="certificate.validityStart" />
<span class="input-group-btn">
<button class="btn btn-default" ng-click="openNotBefore($event)"><i class="glyphicon glyphicon-calendar"></i></button>
</span>
</div>
</div>
</div>
<span style="padding-top: 15px" class="text-center col-sm-2"><label><span class="glyphicon glyphicon-resize-horizontal"></span></label></span>
<div class="col-sm-4">
<div>
<div class="input-group">
<input tooltip="Ending Date (yyyy/MM/dd)" class="form-control" datepicker-popup="yyyy/MM/dd" is-open="$parent.openNotAfter.isOpen" min-date="certificate.authority.notBefore" max-date="certificate.authority.maxDate" ng-model="certificate.validityEnd" />
<span class="input-group-btn">
<button class="btn btn-default" ng-click="openNotAfter($event)"><i class="glyphicon glyphicon-calendar"></i></button>
</span>
</div>
</div>
</div>
</div>
<div ng-include="'angular/certificates/certificate/notifications.tpl.html'"></div>
<div ng-include="'angular/certificates/certificate/destinations.tpl.html'"></div>
</div> </div>
<div class="form-group"
ng-class="{'has-error': trackingForm.description.$invalid, 'has-success': !trackingForm.$invalid&&trackingForm.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" 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"
ng-class="{'has-error': trackingForm.selectedAuthority.$invalid, 'has-success': !trackingForm.$invalid&&trackingForm.selectedAuthority.$dirty}">
<label class="control-label col-sm-2">
Certificate Authority
</label>
<div class="col-sm-10">
<div class="input-group col-sm-12">
<input name="selectedAuthority"
tooltip="If you are unsure which authority you need; you most likely want to use 'verisign'"
type="text" ng-model="certificate.selectedAuthority" placeholder="Authority Name"
typeahead-on-select="certificate.attachAuthority($item)"
typeahead="authority.name for authority in authorityService.findActiveAuthorityByName($viewValue)"
typeahead-loading="loadingAuthorities"
class="form-control" typeahead-wait-ms="1000"
typeahead-template-url="angular/authorities/authority/select.tpl.html" required>
</div>
</div>
</div>
<div ng-show="certificate.authority" class="form-group">
<label class="control-label col-sm-2">
Certificate Template
</label>
<div class="col-sm-10">
<select class="form-control" ng-change="certificate.useTemplate()" name="certificateTemplate"
ng-model="certificate.template" ng-options="template.name for template in templates"></select>
</div>
</div>
<div class="form-group"
ng-class="{'has-error': trackingForm.commonName.$invalid, 'has-success': !trackingForm.$invalid&&trackingForm.commonName.$dirty}">
<label class="control-label col-sm-2">
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 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">
<label class="control-label col-sm-2"
tooltip="If no date is selected Lemur attempts to issue a 2 year certificate">
Validity Range <span class="glyphicon glyphicon-question-sign"></span>
</label>
<div class="col-sm-4">
<div>
<div class="input-group">
<input tooltip="Starting Date (yyyy/MM/dd)" class="form-control" datepicker-popup="yyyy/MM/dd"
is-open="$parent.openNotBefore.isOpen" min-date="certificate.authority.notBefore"
max-date="certificate.authority.maxDate" ng-model="certificate.validityStart"/>
<span class="input-group-btn">
<button class="btn btn-default" ng-click="openNotBefore($event)"><i
class="glyphicon glyphicon-calendar"></i></button>
</span>
</div>
</div>
</div>
<span style="padding-top: 15px" class="text-center col-sm-2"><label><span
class="glyphicon glyphicon-resize-horizontal"></span></label></span>
<div class="col-sm-4">
<div>
<div class="input-group">
<input tooltip="Ending Date (yyyy/MM/dd)" class="form-control" datepicker-popup="yyyy/MM/dd"
is-open="$parent.openNotAfter.isOpen" min-date="certificate.authority.notBefore"
max-date="certificate.authority.maxDate" ng-model="certificate.validityEnd"/>
<span class="input-group-btn">
<button class="btn btn-default" ng-click="openNotAfter($event)"><i
class="glyphicon glyphicon-calendar"></i></button>
</span>
</div>
</div>
</div>
</div>
<div ng-include="'angular/certificates/certificate/replaces.tpl.html'"></div>
<div ng-include="'angular/certificates/certificate/notifications.tpl.html'"></div>
<div ng-include="'angular/certificates/certificate/destinations.tpl.html'"></div>
</div>
</form> </form>

View File

@ -2,21 +2,36 @@
angular.module('lemur') angular.module('lemur')
.controller('CertificateUploadController', function ($scope, $modalInstance, CertificateService, LemurRestangular, DestinationService, NotificationService, PluginService) { .controller('CertificateUploadController', function ($scope, $modalInstance, CertificateService, LemurRestangular, DestinationService, NotificationService, PluginService, toaster) {
$scope.certificate = LemurRestangular.restangularizeElement(null, {}, 'certificates'); $scope.certificate = LemurRestangular.restangularizeElement(null, {}, 'certificates');
$scope.upload = CertificateService.upload; $scope.upload = CertificateService.upload;
$scope.destinationService = DestinationService; $scope.destinationService = DestinationService;
$scope.notificationService = NotificationService; $scope.notificationService = NotificationService;
$scope.certificateService = CertificateService;
PluginService.getByType('destination').then(function (plugins) { PluginService.getByType('destination').then(function (plugins) {
$scope.plugins = plugins; $scope.plugins = plugins;
}); });
$scope.save = function (certificate) { $scope.save = function (certificate) {
CertificateService.upload(certificate).then(function () { CertificateService.upload(certificate).then(
$modalInstance.close(); function () {
}); toaster.pop({
type: 'success',
title: certificate.name,
body: 'Successfully uploaded!'
});
$modalInstance.close();
},
function (response) {
toaster.pop({
type: 'error',
title: certificate.name,
body: 'Failed to upload ' + response.data.message,
timeout: 100000
});
});
}; };
$scope.cancel = function () { $scope.cancel = function () {

View File

@ -80,6 +80,7 @@
class="help-block">Enter a valid certificate.</p> class="help-block">Enter a valid certificate.</p>
</div> </div>
</div> </div>
<div ng-include="'angular/certificates/certificate/replaces.tpl.html'"></div>
<div ng-include="'angular/certificates/certificate/notifications.tpl.html'"></div> <div ng-include="'angular/certificates/certificate/notifications.tpl.html'"></div>
<div ng-include="'angular/certificates/certificate/destinations.tpl.html'"></div> <div ng-include="'angular/certificates/certificate/destinations.tpl.html'"></div>
</form> </form>

View File

@ -67,6 +67,16 @@ angular.module('lemur')
removeDestination: function (index) { removeDestination: function (index) {
this.destinations.splice(index, 1); this.destinations.splice(index, 1);
}, },
attachReplacement: function (replacement) {
this.selectedReplacement = null;
if (this.replacements === undefined) {
this.replacements = [];
}
this.replacements.push(replacement);
},
removeReplacement: function (index) {
this.replacements.splice(index, 1);
},
attachNotification: function (notification) { attachNotification: function (notification) {
this.selectedNotification = null; this.selectedNotification = null;
if (this.notifications === undefined) { if (this.notifications === undefined) {
@ -89,7 +99,7 @@ angular.module('lemur')
}); });
return LemurRestangular.all('certificates'); return LemurRestangular.all('certificates');
}) })
.service('CertificateService', function ($location, CertificateApi, LemurRestangular, DefaultService, toaster) { .service('CertificateService', function ($location, CertificateApi, AuthorityService, LemurRestangular, DefaultService) {
var CertificateService = this; var CertificateService = this;
CertificateService.findCertificatesByName = function (filterValue) { CertificateService.findCertificatesByName = function (filterValue) {
return CertificateApi.getList({'filter[name]': filterValue}) return CertificateApi.getList({'filter[name]': filterValue})
@ -100,80 +110,23 @@ angular.module('lemur')
CertificateService.create = function (certificate) { CertificateService.create = function (certificate) {
certificate.attachSubAltName(); certificate.attachSubAltName();
return CertificateApi.post(certificate).then( // Help users who may have just typed in their authority
function () { if (!certificate.authority) {
toaster.pop({ AuthorityService.findActiveAuthorityByName(certificate.selectedAuthority).then(function (authorities) {
type: 'success', if (authorities.length > 0) {
title: certificate.name, certificate.authority = authorities[0];
body: 'Successfully created!' }
}); });
}, }
function (response) { return CertificateApi.post(certificate);
toaster.pop({
type: 'error',
title: certificate.name,
body: 'Was not created! ' + response.data.message
});
}
);
}; };
CertificateService.update = function (certificate) { CertificateService.update = function (certificate) {
return LemurRestangular.copy(certificate).put().then( return LemurRestangular.copy(certificate).put();
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
});
});
}; };
CertificateService.upload = function (certificate) { CertificateService.upload = function (certificate) {
return CertificateApi.customPOST(certificate, 'upload').then( return CertificateApi.customPOST(certificate, 'upload');
function () {
toaster.pop({
type: 'success',
title: certificate.name,
body: 'Successfully uploaded!'
});
},
function (response) {
toaster.pop({
type: 'error',
title: certificate.name,
body: 'Failed to upload ' + response.data.message
});
});
};
CertificateService.loadPrivateKey = function (certificate) {
return certificate.customGET('key').then(
function (response) {
if (response.key === null) {
toaster.pop({
type: 'warning',
title: certificate.name,
body: 'No private key found!'
});
} else {
certificate.privateKey = response.key;
}
},
function () {
toaster.pop({
type: 'error',
title: certificate.name,
body: 'You do not have permission to view this key!'
});
});
}; };
CertificateService.getAuthority = function (certificate) { CertificateService.getAuthority = function (certificate) {
@ -206,6 +159,12 @@ angular.module('lemur')
}); });
}; };
CertificateService.getReplacements = function (certificate) {
return certificate.getList('replacements').then(function (replacements) {
certificate.replacements = replacements;
});
};
CertificateService.getDefaults = function (certificate) { CertificateService.getDefaults = function (certificate) {
return DefaultService.get().then(function (defaults) { return DefaultService.get().then(function (defaults) {
certificate.country = defaults.country; certificate.country = defaults.country;
@ -216,22 +175,16 @@ angular.module('lemur')
}); });
}; };
CertificateService.loadPrivateKey = function (certificate) {
return certificate.customGET('key');
};
CertificateService.updateActive = function (certificate) { CertificateService.updateActive = function (certificate) {
return certificate.put().then( return certificate.put();
function () { };
toaster.pop({
type: 'success', CertificateService.export = function (certificate) {
title: certificate.name, return certificate.customPOST(certificate.exportOptions, 'export');
body: 'Successfully updated!'
});
},
function (response) {
toaster.pop({
type: 'error',
title: certificate.name,
body: 'Was not updated! ' + response.data.message
});
});
}; };
return CertificateService; return CertificateService;

View File

@ -2,15 +2,23 @@
angular.module('lemur') angular.module('lemur')
.config(function config($routeProvider) { .config(function config($stateProvider) {
$routeProvider.when('/certificates', {
templateUrl: '/angular/certificates/view/view.tpl.html', $stateProvider
controller: 'CertificatesViewController' .state('certificates', {
}); url: '/certificates',
templateUrl: '/angular/certificates/view/view.tpl.html',
controller: 'CertificatesViewController'
})
.state('certificate', {
url: '/certificates/:name',
templateUrl: '/angular/certificates/view/view.tpl.html',
controller: 'CertificatesViewController'
});
}) })
.controller('CertificatesViewController', function ($q, $scope, $modal, CertificateApi, CertificateService, MomentService, ngTableParams) { .controller('CertificatesViewController', function ($q, $scope, $modal, $stateParams, CertificateApi, CertificateService, MomentService, ngTableParams, toaster) {
$scope.filter = {}; $scope.filter = $stateParams;
$scope.certificateTable = new ngTableParams({ $scope.certificateTable = new ngTableParams({
page: 1, // show first page page: 1, // show first page
count: 10, // count per page count: 10, // count per page
@ -28,6 +36,7 @@ angular.module('lemur')
CertificateService.getDomains(certificate); CertificateService.getDomains(certificate);
CertificateService.getDestinations(certificate); CertificateService.getDestinations(certificate);
CertificateService.getNotifications(certificate); CertificateService.getNotifications(certificate);
CertificateService.getReplacements(certificate);
CertificateService.getAuthority(certificate); CertificateService.getAuthority(certificate);
CertificateService.getCreator(certificate); CertificateService.getCreator(certificate);
}); });
@ -37,15 +46,65 @@ angular.module('lemur')
} }
}); });
$scope.certificateService = CertificateService;
$scope.momentService = MomentService; $scope.momentService = MomentService;
$scope.remove = function (certificate) { $scope.remove = function (certificate) {
certificate.remove().then(function () { certificate.remove().then(
$scope.certificateTable.reload(); function () {
}); $scope.certificateTable.reload();
},
function (response) {
toaster.pop({
type: 'error',
title: certificate.name,
body: 'Unable to remove certificate! ' + response.data.message,
timeout: 100000
});
});
}; };
$scope.loadPrivateKey = function (certificate) {
CertificateService.loadPrivateKey(certificate).then(
function (response) {
if (response.key === null) {
toaster.pop({
type: 'warning',
title: certificate.name,
body: 'No private key found!'
});
} else {
certificate.privateKey = response.key;
}
},
function () {
toaster.pop({
type: 'error',
title: certificate.name,
body: 'You do not have permission to view this key!',
timeout: 100000
});
});
};
$scope.updateActive = function (certificate) {
CertificateService.updateActive(certificate).then(
function () {
toaster.pop({
type: 'success',
title: certificate.name,
body: 'Updated!'
});
},
function (response) {
toaster.pop({
type: 'error',
title: certificate.name,
body: 'Unable to update! ' + response.data.message,
timeout: 100000
});
certificate.active = false;
});
};
$scope.getCertificateStatus = function () { $scope.getCertificateStatus = function () {
var def = $q.defer(); var def = $q.defer();
def.resolve([{'title': 'Active', 'id': true}, {'title': 'Inactive', 'id': false}]); def.resolve([{'title': 'Active', 'id': true}, {'title': 'Inactive', 'id': false}]);
@ -104,4 +163,18 @@ angular.module('lemur')
$scope.certificateTable.reload(); $scope.certificateTable.reload();
}); });
}; };
$scope.export = function (certificateId) {
$modal.open({
animation: true,
controller: 'CertificateExportController',
templateUrl: '/angular/certificates/certificate/export.tpl.html',
size: 'lg',
resolve: {
editId: function () {
return certificateId;
}
}
});
};
}); });

View File

@ -5,12 +5,10 @@
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<div class="btn-group pull-right"> <div class="btn-group pull-right">
<button data-placement="left" data-title="Create Certificate" bs-tooltip ng-click="create()" <button ng-click="create()" class="btn btn-primary">
class="btn btn-primary">
Create Create
</button> </button>
<button data-placement="left" data-title="Import Certificate" bs-tooltip ng-click="import()" <button ng-click="import()" class="btn btn-info">
class="btn btn-info">
Import Import
</button> </button>
</div> </div>
@ -26,120 +24,156 @@
<tr ng-class="{'even-row': $even }" ng-repeat-start="certificate in $data track by $index"> <tr ng-class="{'even-row': $even }" ng-repeat-start="certificate in $data track by $index">
<td data-title="'Name'" sortable="'name'" filter="{ 'name': 'text' }"> <td data-title="'Name'" sortable="'name'" filter="{ 'name': 'text' }">
<ul class="list-unstyled"> <ul class="list-unstyled">
<li>{{ certificate.name }}</li> <li>{{ ::certificate.name }}</li>
<li><span class="text-muted">{{ certificate.owner }}</span></li> <li><span class="text-muted">{{ ::certificate.owner }}</span></li>
</ul> </ul>
</td> </td>
<td data-title="'Active'" filter="{ 'active': 'select' }" filter-data="getCertificateStatus()"> <td data-title="'Active'" filter="{ 'active': 'select' }" filter-data="getCertificateStatus()">
<form> <form>
<switch ng-change="certificateService.updateActive(certificate)" id="status" name="status" ng-model="certificate.active" class="green small"></switch> <switch ng-change="updateActive(certificate)" id="status" name="status"
ng-model="certificate.active" class="green small"></switch>
</form> </form>
</td> </td>
<td data-title="'Issuer'" sortable="'issuer'" filter="{ 'issuer': 'text' }"> <td data-title="'Issuer'" sortable="'issuer'" filter="{ 'issuer': 'text' }">
{{ certificate.authority.name || certificate.issuer }} {{ ::certificate.authority.name || certificate.issuer }}
</td> </td>
<td data-title="'Common Name'" filter="{ 'cn': 'text'}"> <td data-title="'Domains'" filter="{ 'cn': 'text'}">
{{ certificate.cn }} {{ ::certificate.cn }}
</td> </td>
<td data-title="''"> <td class="col-md-2" data-title="''">
<div class="btn-group pull-right"> <div class="btn-group pull-right">
<button ng-model="certificate.toggle" class="btn btn-sm btn-info" btn-checkbox btn-checkbox-true="1" butn-checkbox-false="0">More</button> <a class="btn btn-sm btn-default" ui-sref="certificate({name: certificate.name})">Permalink</a>
<button class="btn btn-sm btn-warning" ng-click="edit(certificate.id)">Edit</button> <button ng-model="certificate.toggle" class="btn btn-sm btn-info" btn-checkbox btn-checkbox-true="1"
butn-checkbox-false="0">More
</button>
<button ng-click="export(certificate.id)" class="btn btn-sm btn-success">
Export
</button>
<button class="btn btn-sm btn-warning" ng-click="edit(certificate.id)">Edit</button>
</div> </div>
</td> </td>
</tr> </tr>
<tr class="warning" ng-show="certificate.toggle" ng-repeat-end> <tr class="warning" ng-show="certificate.toggle" ng-repeat-end>
<td colspan="6"> <td colspan="6">
<tabset justified="true" class="col-md-6"> <tabset justified="true" class="col-md-6">
<tab heading="Basic Info"> <tab>
<ul class="list-group"> <tab-heading>Basic Info</tab-heading>
<li class="list-group-item"> <ul class="list-group">
<strong>Creator</strong> <li class="list-group-item">
<span class="pull-right"> <strong>Creator</strong>
{{ certificate.creator.email }} <span class="pull-right">
</span> {{ ::certificate.creator.email }}
</li> </span>
<li class="list-group-item"> </li>
<strong>Not Before</strong> <li class="list-group-item">
<span class="pull-right" tooltip="{{ certificate.notBefore }}"> <strong>Not Before</strong>
{{ momentService.createMoment(certificate.notBefore) }} <span class="pull-right" tooltip="{{ ::certificate.notBefore }}">
</span> {{ ::momentService.createMoment(certificate.notBefore) }}
</li> </span>
<li class="list-group-item"> </li>
<strong>Not After</strong> <li class="list-group-item">
<span class="pull-right" tooltip="{{ certificate.notAfter }}"> <strong>Not After</strong>
{{ momentService.createMoment(certificate.notAfter) }} <span class="pull-right" tooltip="{{ ::certificate.notAfter }}">
</span> {{ ::momentService.createMoment(certificate.notAfter) }}
</li> </span>
<li class="list-group-item"> </li>
<strong>San</strong> <li class="list-group-item">
<span class="pull-right"> <strong>San</strong>
<i class="glyphicon glyphicon-ok" ng-show="certificate.san"></i> <span class="pull-right">
<i class="glyphicon glyphicon-remove" ng-show="!certificate.san"></i> <i class="glyphicon glyphicon-ok" ng-show="certificate.san"></i>
</span> <i class="glyphicon glyphicon-remove" ng-show="!certificate.san"></i>
</li> </span>
<li class="list-group-item"> </li>
<strong>Bits</strong> <li class="list-group-item">
<span class="pull-right">{{ certificate.bits }}</span> <strong>Bits</strong>
</li> <span class="pull-right">{{ ::certificate.bits }}</span>
<li class="list-group-item"> </li>
<strong>Serial</strong> <li class="list-group-item">
<span class="pull-right">{{ certificate.serial }}</span> <strong>Signing Algorithm</strong>
</li> <span class="pull-right">{{ ::certificate.signingAlgorithm }}</span>
<li tooltip="Lemur will attempt to check a certificates validity, this is used to track whether a certificate as been revoked" class="list-group-item"> </li>
<strong>Validity</strong> <li class="list-group-item">
<span class="pull-right"> <strong>Serial</strong>
<span ng-show="!certificate.status" class="label label-warning">Unknown</span> <span class="pull-right">{{ ::certificate.serial }}</span>
<span ng-show="certificate.status == 'revoked'" class="label label-danger">Revoked</span> </li>
<span ng-show="certificate.status == 'valid'" class="label label-success">Valid</span> <li
</span> tooltip="Lemur will attempt to check a certificates validity, this is used to track whether a certificate as been revoked"
</li> class="list-group-item">
<li class="list-group-item"> <strong>Validity</strong>
<strong>Description</strong> <span class="pull-right">
<span class="pull-right">{{ certificate.description }}</span> <span ng-show="!certificate.status" class="label label-warning">Unknown</span>
</li> <span ng-show="certificate.status == 'revoked'" class="label label-danger">Revoked</span>
</ul> <span ng-show="certificate.status == 'valid'" class="label label-success">Valid</span>
</tab> </span>
<tab heading="Notifications"> </li>
<ul class="list-group"> <li class="list-group-item">
<li class="list-group-item" ng-repeat="notification in certificate.notifications"> <strong>Description</strong>
<strong>{{ notification.label }}</strong> <p>{{ ::certificate.description }}</p>
<span class="pull-right">{{ notification.description}}</span> </li>
</li> </ul>
</ul> </tab>
</tab> <tab>
<tab heading="Destinations"> <tab-heading>Notifications</tab-heading>
<ul class="list-group"> <ul class="list-group">
<li class="list-group-item" ng-repeat="destination in certificate.destinations"> <li class="list-group-item" ng-repeat="notification in certificate.notifications">
<strong>{{ destination.label }}</strong> <strong>{{ ::notification.label }}</strong>
<span class="pull-right">{{ destination.description }}</span> <span class="pull-right">{{ ::notification.description}}</span>
</li> </li>
</ul> </ul>
</tab> </tab>
<tab heading="Domains"> <tab>
<div class="list-group"> <tab-heading>Destinations</tab-heading>
<a href="#/domains/{{ domain.id }}" class="list-group-item" ng-repeat="domain in certificate.domains">{{ domain.name }}</a> <ul class="list-group">
</div> <li class="list-group-item" ng-repeat="destination in certificate.destinations">
</tab> <strong>{{ ::destination.label }}</strong>
<span class="pull-right">{{ ::destination.description }}</span>
</li>
</ul>
</tab>
<tab>
<tab-heading>Domains</tab-heading>
<div class="list-group">
<a href="#/domains/{{ ::domain.id }}" class="list-group-item"
ng-repeat="domain in certificate.domains">{{ ::domain.name }}</a>
</div>
</tab>
<tab>
<tab-heading>Replaces</tab-heading>
<ul class="list-group">
<li class="list-group-item" ng-repeat="replacement in certificate.replacements">
<strong>{{ ::replacement.name }}</strong>
<p>{{ ::replacement.description}}</p>
</li>
</ul>
</tab>
</tabset> </tabset>
<tabset justified="true" class="col-md-6"> <tabset justified="true" class="col-md-6">
<tab heading="Chain"> <tab>
<p> <tab-heading>
<pre style="width: 550px">{{ certificate.chain }}</pre> Chain
</p> <button class="btn btn-xs btn-default clipboard-btn glyphicon glyphicon-copy"
tooltip="Copy chain to clipboard" tooltip-trigger="mouseenter" clipboard
text="certificate.chain"></button>
</tab-heading>
<pre style="width: 100%">{{ ::certificate.chain }}</pre>
</tab> </tab>
<tab heading="Public Certificate"> <tab>
<p> <tab-heading>
<pre style="width: 550px">{{ certificate.body }}</pre> Public Certificate
</p> <button class="btn btn-xs btn-default clipboard-btn glyphicon glyphicon-copy"
tooltip="Copy certificate to clipboard" tooltip-trigger="mouseenter" clipboard
text="certificate.body"></button>
</tab-heading>
<pre style="width: 100%">{{ ::certificate.body }}</pre>
</tab> </tab>
<tab ng-click="certificateService.loadPrivateKey(certificate)"> <tab ng-click="loadPrivateKey(certificate)">
<tab-heading> <tab-heading>
Private Key Private Key
<button class="btn btn-xs btn-default clipboard-btn glyphicon glyphicon-copy"
tooltip="Copy key to clipboard" tooltip-trigger="mouseenter" clipboard
text="certificate.privateKey"></button>
</tab-heading> </tab-heading>
<p> <pre style="width: 100%">{{ ::certificate.privateKey }}</pre>
<pre style="width: 550px">{{ certificate.privateKey }}</pre>
</p>
</tab> </tab>
</tabset> </tabset>
</td> </td>

View File

@ -1,15 +1,16 @@
'use strict'; 'use strict';
angular.module('lemur') angular.module('lemur')
.config(function config($routeProvider) { .config(function config($stateProvider) {
$routeProvider.when('/dashboard', { $stateProvider.state('dashboard', {
url: '/dashboard',
templateUrl: '/angular/dashboard/dashboard.tpl.html', templateUrl: '/angular/dashboard/dashboard.tpl.html',
controller: 'DashboardController' controller: 'DashboardController'
}); });
}) })
.controller('DashboardController', function ($scope, $rootScope, $filter, $location, LemurRestangular) { .controller('DashboardController', function ($scope, $rootScope, $filter, $location, LemurRestangular) {
$scope.colours = [ $scope.colors = [
{ {
fillColor: 'rgba(41, 171, 224, 0.2)', fillColor: 'rgba(41, 171, 224, 0.2)',
strokeColor: 'rgba(41, 171, 224, 1)', strokeColor: 'rgba(41, 171, 224, 1)',
@ -78,13 +79,18 @@ angular.module('lemur')
$scope.bits = data.items; $scope.bits = data.items;
}); });
LemurRestangular.all('certificates').customGET('stats', {metric: 'signing_algorithm'})
.then(function (data) {
$scope.algos = data.items;
});
LemurRestangular.all('certificates').customGET('stats', {metric: 'not_after'}) LemurRestangular.all('certificates').customGET('stats', {metric: 'not_after'})
.then(function (data) { .then(function (data) {
$scope.expiring = {labels: data.items.labels, values: [data.items.values]}; $scope.expiring = {labels: data.items.labels, values: [data.items.values]};
}); });
LemurRestangular.all('destinations').customGET('stats', {metric: 'certificates'}) LemurRestangular.all('destinations').customGET('stats', {metric: 'certificate'})
.then(function (data) { .then(function (data) {
$scope.destinations = {labels: data.items.labels, values: [data.items.values]}; $scope.destinations = data.items;
}); });
}); });

View File

@ -10,43 +10,54 @@
<h3 class="panel-title">Expiring Certificates</h3> <h3 class="panel-title">Expiring Certificates</h3>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<canvas id="expiringBar" class="chart chart-bar" data="expiring.values" labels="expiring.labels" colours="colours"></canvas> <canvas id="expiringBar" class="chart chart-bar" data="expiring.values" labels="expiring.labels"
colors="colors"></canvas>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="row"></div> <div class="row"></div>
<div class="col-md-6"> <div class="col-md-6">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<h3 class="panel-title">Issuers</h3> <h3 class="panel-title">Issuers</h3>
</div>
<div class="panel-body">
<canvas id="issuersPie" class="chart chart-pie" data="issuers.values" labels="issuers.labels" colours="colours" legend="true"></canvas>
</div>
</div> </div>
</div> <div class="panel-body">
<div class="col-md-6"> <canvas id="issuersPie" class="chart chart-pie" data="issuers.values" labels="issuers.labels" colors="colors"></canvas>
<div class="panel panel-default"> </div>
<div class="panel-heading"> </div>
<h3 class="panel-title">Bit Strength</h3> </div>
</div> <div class="col-md-6">
<div class="panel-body"> <div class="panel panel-default">
<canvas id="bitsPie" class="chart chart-pie" data="bits.values" labels="bits.labels" colours="colours" legend="true"></canvas> <div class="panel-heading">
</div> <h3 class="panel-title">Bit Strength</h3>
</div>
<div class="panel-body">
<canvas id="bitsPie" class="chart chart-pie" data="bits.values" labels="bits.labels" colors="colors"></canvas>
</div>
</div>
</div>
<div class="row"></div>
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Destinations</h3>
</div>
<div class="panel-body">
<canvas id="destinationPie" class="chart chart-pie" data="destinations.values" labels="destinations.labels"
colors="colors"></canvas>
</div>
</div>
</div>
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Signing Algorithms</h3>
</div>
<div class="panel-body">
<canvas id="signingPie" class="chart chart-pie" data="algos.values" labels="algos.labels"
colors="colors"></canvas>
</div> </div>
</div> </div>
<div class="row"></div>
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Destinations</h3>
</div>
<div class="panel-body">
<canvas id="destinationPie" class="chart chart-pie" data="destinations.values" labels="destinations.labels" colours="colours" legend="true"></canvas>
</div>
</div>
</div>
</div> </div>
<!-- /.row -->
</div> </div>

View File

@ -2,8 +2,9 @@
angular.module('lemur') angular.module('lemur')
.config(function config($routeProvider) { .config(function config($stateProvider) {
$routeProvider.when('/destinations', { $stateProvider.state('destinations', {
url: '/destinations',
templateUrl: '/angular/destinations/view/view.tpl.html', templateUrl: '/angular/destinations/view/view.tpl.html',
controller: 'DestinationsViewController' controller: 'DestinationsViewController'
}); });

View File

@ -2,8 +2,9 @@
angular.module('lemur') angular.module('lemur')
.config(function config($routeProvider) { .config(function config($stateProvider) {
$routeProvider.when('/domains', { $stateProvider.state('domains', {
url: '/domains',
templateUrl: '/angular/domains/view/view.tpl.html', templateUrl: '/angular/domains/view/view.tpl.html',
controller: 'DomainsViewController' controller: 'DomainsViewController'
}); });

View File

@ -2,8 +2,9 @@
angular.module('lemur') angular.module('lemur')
.config(function config($routeProvider) { .config(function config($stateProvider) {
$routeProvider.when('/notifications', { $stateProvider.state('notifications', {
url: '/notifications',
templateUrl: '/angular/notifications/view/view.tpl.html', templateUrl: '/angular/notifications/view/view.tpl.html',
controller: 'NotificationsViewController' controller: 'NotificationsViewController'
}); });

View File

@ -2,8 +2,9 @@
angular.module('lemur') angular.module('lemur')
.config(function config($routeProvider) { .config(function config($stateProvider) {
$routeProvider.when('/roles', { $stateProvider.state('roles', {
url: '/roles',
templateUrl: '/angular/roles/view/view.tpl.html', templateUrl: '/angular/roles/view/view.tpl.html',
controller: 'RolesViewController' controller: 'RolesViewController'
}); });

View File

@ -2,8 +2,9 @@
angular.module('lemur') angular.module('lemur')
.config(function config($routeProvider) { .config(function config($stateProvider) {
$routeProvider.when('/sources', { $stateProvider.state('sources', {
url: '/sources',
templateUrl: '/angular/sources/view/view.tpl.html', templateUrl: '/angular/sources/view/view.tpl.html',
controller: 'SourcesViewController' controller: 'SourcesViewController'
}); });

View File

@ -2,8 +2,9 @@
angular.module('lemur') angular.module('lemur')
.config(function config($routeProvider) { .config(function config($stateProvider) {
$routeProvider.when('/users', { $stateProvider.state('users', {
url: '/users',
templateUrl: '/angular/users/view/view.tpl.html', templateUrl: '/angular/users/view/view.tpl.html',
controller: 'UsersViewController' controller: 'UsersViewController'
}); });

View File

@ -1,12 +1,26 @@
<div class="jumbotron"> <div class="jumbotron">
<span class="pull-right"><button class="btn btn-sm btn-primary">First Time? Take the Tour!</button></span>
<h1>Hey there!</h1> <h1>Hey there!</h1>
<p>Welcome to Lemur! A central portal for all (most) of your TLS certificate needs. With Lemur you are able to create, deploy and track the TLS certificates in your environment. Lets get started!</p>
<p>Welcome to Lemur! A central portal for all (most) of your SSL needs.</p>
<p><a href="/#/certificates/create" class="btn btn-primary btn-lg" role="button">Create a Certificate</a></p>
</div> </div>
<div class="row featurette">
<div class="col-md-10"> <div class="container marketing">
<h2 class="featurette-heading">SSL In The Cloud <span class="text-muted">Encrypt it all </span></h2> <!-- Three columns of text below the carousel -->
</div> <div class="row">
<div class="col-lg-4">
<h2>Create</h2>
<p>With Lemur you can create certificates from any authority; internal or external! Lemur does not issue certificates itself. Instead it acts as a broker, creating private keys and CSRs that are sent to external services.</p>
<p><a class="btn btn-default" ui-sref="certificates" role="button">View certificates &raquo;</a></p>
</div><!-- /.col-lg-4 -->
<div class="col-lg-4">
<h2>Deploy</h2>
<p>Once certificates have been created with Lemur, you can put them to use! Lemur has the ability to create destinations for certificates that allow them to be uploaded to and used by a variety of environments.</p>
<p><a class="btn btn-default" ui-sref="destinations" role="button">View Destinations &raquo;</a></p>
</div><!-- /.col-lg-4 -->
<div class="col-lg-4">
<h2>Authority</h2>
<p>Have an internal Certificate Authority? Need an easy way to create an manage those authorities? Lemur has you covered!</p>
<p><a class="btn btn-default" ui-sref="authorities" role="button">View Authorities &raquo;</a></p>
</div><!-- /.col-lg-4 -->
</div><!-- /.row -->
</div> </div>

View File

@ -5,7 +5,7 @@
<div class="modal-footer"> <div class="modal-footer">
<input ng-hide="currentStepNumber() == 1" class="btn btn-default pull-left" type="submit" wz-previous value="Previous" /> <input ng-hide="currentStepNumber() == 1" class="btn btn-default pull-left" type="submit" wz-previous value="Previous" />
<input ng-show="currentStepNumber() != steps.length" class="btn btn-default pull-right" type="submit" wz-next value="Next" /> <input ng-show="currentStepNumber() != steps.length" class="btn btn-default pull-right" type="submit" wz-next value="Next" />
<input ng-show="!context.loading" class="btn btn-success pull-right" type="submit" wz-finish value="Create" /> <input ng-show="!context.loading" ng-class="{disabled: trackingForm.invalid}" class="btn btn-success pull-right" type="submit" wz-finish value="Create" />
<button ng-show="context.loading" class="btn btn-success pull-right disabled"><wave-spinner></wave-spinner></button> <button ng-show="context.loading" class="btn btn-success pull-right disabled"><wave-spinner></wave-spinner></button>
<div class="clearfix"></div> <div class="clearfix"></div>
</div> </div>

View File

@ -49,45 +49,53 @@
</div> </div>
<div class="navbar-collapse collapse" ng-controller="LoginController"> <div class="navbar-collapse collapse" ng-controller="LoginController">
<ul class="nav navbar-nav navbar-left"> <ul class="nav navbar-nav navbar-left">
<li><a href="/#/dashboard">Dashboard</a></li> <li><a ui-sref="dashboard">Dashboard</a></li>
<li><a href="/#/certificates">Certificates</a></li> <li><a ui-sref="certificates">Certificates</a></li>
<li><a href="/#/authorities">Authorities</a></li> <li><a ui-sref="authorities">Authorities</a></li>
<li><a href="/#/notifications">Notifications</a></li> <li><a ui-sref="notifications">Notifications</a></li>
<li><a href="/#/destinations">Destinations</a></li> <li><a ui-sref="destinations">Destinations</a></li>
<li><a href="/#/sources">Sources</a></li> <li><a ui-sref="sources">Sources</a></li>
<li></li> <li></li>
<li class="dropdown" dropdown on-toggle="toggled(open)"> <li class="dropdown" dropdown on-toggle="toggled(open)">
<a href class="dropdown-toggle" dropdown-toggle>Settings <span class="caret"></span></a> <a href class="dropdown-toggle" dropdown-toggle>Settings <span class="caret"></span></a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a href="/#/roles">Roles</a></li> <li><a ui-sref="roles">Roles</a></li>
<li><a href="/#/users">Users</a></li> <li><a ui-sref="users">Users</a></li>
<li><a href="/#/domains">Domains</a></li> <li><a ui-sref="domains">Domains</a></li>
</ul> </ul>
</li> </li>
</ul> </ul>
<ul ng-show="!currentUser.username" class="nav navbar-nav navbar-right"> <ul ng-show="!currentUser.username" class="nav navbar-nav navbar-right">
<li><a href="/#/login">Login</a></li> <li><a ui-sref="login">Login</a></li>
</ul> </ul>
<ul ng-show="currentUser.username" class="nav navbar-nav navbar-right"> <ul ng-show="currentUser.username" class="nav navbar-nav navbar-right">
<li class="dropdown" dropdown on-toggle="toggled(open)"> <li class="dropdown" dropdown on-toggle="toggled(open)">
<a href class="dropdown-toggle profile-nav" dropdown-toggle> <a href class="dropdown-toggle profile-nav" dropdown-toggle>
{{ currentUser.username }}<img ng-if="currentUser.profileImage" src="{{ currentUser.profileImage }}" class="profile img-circle"> <span ng-show="currentUser.profileImage">
</a> {{ currentUser.username }}<img src="{{ currentUser.profileImage }}" class="profile img-circle">
<ul class="dropdown-menu"> </span>
<li><a ng-click="logout()">Logout</a></li> <span ng-show="!currentUser.profileImage">
</ul> {{ currentUser.username }}<ng-letter-avatar height="35" width="35" data="currentUser.username" shape="round"></ng-letter-avatar>
</li> </span>
</a>
<ul class="dropdown-menu">
<li><a ng-click="logout()">Logout</a></li>
</ul>
</li>
</ul> </ul>
</div> </div>
</div> </div>
</div> </div>
<!-- Add your site or application content here --> <!-- Add your site or application content here -->
<div class="container-fluid"> <div class="container-fluid">
<div ng-view></div> <div ui-view></div>
</div> </div>
<footer class="footer"> <footer class="footer">
<div class="container"> <div class="container">
<p class="text-muted">Lemur is broken regularly by <a href="https://github.com/Netflix/lemur.git">Netflix</a>.</p> <p class="text-muted">
<span>Lemur is broken regularly by <a href="https://github.com/Netflix/lemur.git">Netflix</a>.</span>
<span class="pull-right">Confused? Check out our <a href="https://lemur.readthedocs.org/en/latest">docs</a>!</span>
</p>
</div> </div>
</footer> </footer>
</body> </body>

View File

@ -169,3 +169,13 @@ a {
background-color: #FFFFFF !important; background-color: #FFFFFF !important;
} }
.clipboard-btn {
border-width: 0;
background-color: transparent;
color: #777;
display: inline-block;
top: 0;
line-height: 1;
}

View File

@ -21,7 +21,7 @@ SECRET_KEY = 'I/dVhOZNSMZMqrFJa5tWli6VQccOGudKerq3eWPMSzQNmHHVhMAQfQ=='
# You should consider storing these separately from your config # You should consider storing these separately from your config
LEMUR_TOKEN_SECRET = 'test' LEMUR_TOKEN_SECRET = 'test'
LEMUR_ENCRYPTION_KEY = 'jPd2xwxgVGXONqghHNq7/S761sffYSrT3UAgKwgtMxbqa0gmKYCfag==' LEMUR_ENCRYPTION_KEYS = 'o61sBLNBSGtAckngtNrfVNd8xy8Hp9LBGDstTbMbqCY='
# this is a list of domains as regexes that only admins can issue # this is a list of domains as regexes that only admins can issue
LEMUR_RESTRICTED_DOMAINS = [] LEMUR_RESTRICTED_DOMAINS = []
@ -43,7 +43,7 @@ LOG_FILE = "lemur.log"
# modify this if you are not using a local database # modify this if you are not using a local database
SQLALCHEMY_DATABASE_URI = 'postgresql://lemur:lemur@localhost:5432/lemur' SQLALCHEMY_DATABASE_URI = 'postgresql://lemur:lemur@localhost:5432/lemur'
SQLALCHEMY_TRACK_MODIFICATIONS = False
# AWS # AWS

View File

@ -41,44 +41,44 @@ def test_create_basic_csr():
def test_cert_get_cn(): def test_cert_get_cn():
from lemur.tests.certs import INTERNAL_VALID_LONG_CERT from lemur.tests.certs import INTERNAL_VALID_LONG_CERT
from lemur.certificates.models import cert_get_cn from lemur.certificates.models import get_cn
assert cert_get_cn(INTERNAL_VALID_LONG_CERT) == 'long.lived.com' assert get_cn(INTERNAL_VALID_LONG_CERT) == 'long.lived.com'
def test_cert_get_subAltDomains(): def test_cert_get_subAltDomains():
from lemur.tests.certs import INTERNAL_VALID_SAN_CERT, INTERNAL_VALID_LONG_CERT from lemur.tests.certs import INTERNAL_VALID_SAN_CERT, INTERNAL_VALID_LONG_CERT
from lemur.certificates.models import cert_get_domains from lemur.certificates.models import get_domains
assert cert_get_domains(INTERNAL_VALID_LONG_CERT) == [] assert get_domains(INTERNAL_VALID_LONG_CERT) == []
assert cert_get_domains(INTERNAL_VALID_SAN_CERT) == ['example2.long.com', 'example3.long.com'] assert get_domains(INTERNAL_VALID_SAN_CERT) == ['example2.long.com', 'example3.long.com']
def test_cert_is_san(): def test_cert_is_san():
from lemur.tests.certs import INTERNAL_VALID_SAN_CERT, INTERNAL_VALID_LONG_CERT from lemur.tests.certs import INTERNAL_VALID_SAN_CERT, INTERNAL_VALID_LONG_CERT
from lemur.certificates.models import cert_is_san from lemur.certificates.models import is_san
assert cert_is_san(INTERNAL_VALID_LONG_CERT) == None # noqa assert is_san(INTERNAL_VALID_LONG_CERT) == None # noqa
assert cert_is_san(INTERNAL_VALID_SAN_CERT) == True # noqa assert is_san(INTERNAL_VALID_SAN_CERT) == True # noqa
def test_cert_is_wildcard(): def test_cert_is_wildcard():
from lemur.tests.certs import INTERNAL_VALID_WILDCARD_CERT, INTERNAL_VALID_LONG_CERT from lemur.tests.certs import INTERNAL_VALID_WILDCARD_CERT, INTERNAL_VALID_LONG_CERT
from lemur.certificates.models import cert_is_wildcard from lemur.certificates.models import is_wildcard
assert cert_is_wildcard(INTERNAL_VALID_WILDCARD_CERT) == True # noqa assert is_wildcard(INTERNAL_VALID_WILDCARD_CERT) == True # noqa
assert cert_is_wildcard(INTERNAL_VALID_LONG_CERT) == None # noqa assert is_wildcard(INTERNAL_VALID_LONG_CERT) == None # noqa
def test_cert_get_bitstrength(): def test_cert_get_bitstrength():
from lemur.tests.certs import INTERNAL_VALID_LONG_CERT from lemur.tests.certs import INTERNAL_VALID_LONG_CERT
from lemur.certificates.models import cert_get_bitstrength from lemur.certificates.models import get_bitstrength
assert cert_get_bitstrength(INTERNAL_VALID_LONG_CERT) == 2048 assert get_bitstrength(INTERNAL_VALID_LONG_CERT) == 2048
def test_cert_get_issuer(): def test_cert_get_issuer():
from lemur.tests.certs import INTERNAL_VALID_LONG_CERT from lemur.tests.certs import INTERNAL_VALID_LONG_CERT
from lemur.certificates.models import cert_get_issuer from lemur.certificates.models import get_issuer
assert cert_get_issuer(INTERNAL_VALID_LONG_CERT) == 'Example' assert get_issuer(INTERNAL_VALID_LONG_CERT) == 'Example'
def test_get_name_from_arn(): def test_get_name_from_arn():

View File

@ -10,7 +10,7 @@ def test_crud(session):
role = update(role.id, 'role_new', None, []) role = update(role.id, 'role_new', None, [])
assert role.name == 'role_new' assert role.name == 'role_new'
delete(role.id) delete(role.id)
assert get(role.id) == None assert not get(role.id)
def test_role_get(client): def test_role_get(client):

View File

@ -5,17 +5,123 @@
:license: Apache, see LICENSE for more details. :license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com> .. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
""" """
import os
import six
from flask import current_app from flask import current_app
from cryptography.fernet import Fernet, MultiFernet
import sqlalchemy.types as types
from contextlib import contextmanager
import tempfile
def get_key(): @contextmanager
def mktempfile():
with tempfile.NamedTemporaryFile(delete=False) as f:
name = f.name
try:
yield name
finally:
try:
os.unlink(name)
except OSError as e:
current_app.logger.debug("No file {0}".format(name))
@contextmanager
def mktemppath():
try:
path = os.path.join(tempfile._get_default_tempdir(), next(tempfile._get_candidate_names()))
yield path
finally:
try:
os.unlink(path)
except OSError as e:
current_app.logger.debug("No file {0}".format(path))
def get_keys():
""" """
Gets the current encryption key Gets the encryption keys.
This supports multiple keys to facilitate key rotation. The first
key in the list is used to encrypt. Decryption is attempted with
each key in succession.
:return: :return:
""" """
# when running lemur create_config, this code needs to work despite
# the fact that there is not a current_app with a config at that point
try: try:
return current_app.config.get('LEMUR_ENCRYPTION_KEY').strip() keys = current_app.config.get('LEMUR_ENCRYPTION_KEYS')
except RuntimeError: except Exception:
print("No Encryption Key Found") print("no encryption keys")
return '' return []
# this function is expected to return a list of keys, but we want
# to let people just specify a single key
if not isinstance(keys, list):
keys = [keys]
# make sure there is no accidental whitespace
keys = [key.strip() for key in keys]
return keys
class Vault(types.TypeDecorator):
"""
A custom SQLAlchemy column type that transparently handles encryption.
This uses the MultiFernet from the cryptography package to faciliate
key rotation. That class handles encryption and signing.
Fernet uses AES in CBC mode with 128-bit keys and PKCS7 padding. It
uses HMAC-SHA256 for ciphertext authentication. Initialization
vectors are generated using os.urandom().
"""
# required by SQLAlchemy. defines the underlying column type
impl = types.Binary
def process_bind_param(self, value, dialect):
"""
Encrypt values on the way into the database.
MultiFernet.encrypt uses the first key in the list.
"""
# we assume that the user's keys are already Fernet keys (32 byte
# keys that have been base64 encoded).
self.keys = [Fernet(key) for key in get_keys()]
# we only support strings and they should be of type bytes for Fernet
if not isinstance(value, six.string_types):
return None
value = bytes(value)
return MultiFernet(self.keys).encrypt(value)
def process_result_value(self, value, dialect):
"""
Decrypt values on the way out of the database.
MultiFernet tries each key until one works.
"""
# we assume that the user's keys are already Fernet keys (32 byte
# keys that have been base64 encoded).
self.keys = [Fernet(key) for key in get_keys()]
# if the value is not a string we aren't going to try to decrypt
# it. this is for the case where the column is null
if not isinstance(value, six.string_types):
return None
# TODO this may raise an InvalidToken exception in certain
# cases. Should we handle that?
# https://cryptography.io/en/latest/fernet/#cryptography.fernet.Fernet.decrypt
return MultiFernet(self.keys).decrypt(value)

View File

@ -1,6 +1,5 @@
{ {
"name": "Lemur", "name": "Lemur",
"version": "0.0.0",
"private": true, "private": true,
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -2,13 +2,14 @@
Lemur Lemur
===== =====
Is an SSL management and orchestration tool. Is a TLS management and orchestration tool.
:copyright: (c) 2015 by Netflix, see AUTHORS for more :copyright: (c) 2015 by Netflix, see AUTHORS for more
:license: Apache, see LICENSE for more details. :license: Apache, see LICENSE for more details.
""" """
from __future__ import absolute_import from __future__ import absolute_import
import sys
import json import json
import os.path import os.path
import datetime import datetime
@ -23,38 +24,47 @@ from subprocess import check_output
ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__))) ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__)))
# When executing the setup.py, we need to be able to import ourselves, this
# means that we need to add the src/ directory to the sys.path.
sys.path.insert(0, ROOT)
about = {}
with open(os.path.join(ROOT, "lemur", "__about__.py")) as f:
exec(f.read(), about)
install_requires = [ install_requires = [
'Flask==0.10.1', 'Flask==0.10.1',
'Flask-RESTful==0.3.3', 'Flask-RESTful==0.3.3',
'Flask-SQLAlchemy==2.0', 'Flask-SQLAlchemy==2.1',
'Flask-Script==2.0.5', 'Flask-Script==2.0.5',
'Flask-Migrate==1.4.0', 'Flask-Migrate==1.6.0',
'Flask-Bcrypt==0.6.2', 'Flask-Bcrypt==0.7.1',
'Flask-Principal==0.4.0', 'Flask-Principal==0.4.0',
'Flask-Mail==0.9.1', 'Flask-Mail==0.9.1',
'SQLAlchemy-Utils==0.30.11', 'SQLAlchemy-Utils==0.31.3',
'BeautifulSoup4', 'BeautifulSoup4==4.4.1',
'requests==2.7.0', 'requests==2.8.1',
'psycopg2==2.6.1', 'psycopg2==2.6.1',
'arrow==0.5.4', 'arrow==0.7.0',
'boto==2.38.0', # we might make this optional 'boto==2.38.0', # we might make this optional
'six==1.9.0', 'six==1.10.0',
'gunicorn==19.3.0', 'gunicorn==19.4.1',
'pycrypto==2.6.1', 'pycrypto==2.6.1',
'cryptography==1.0.1', 'cryptography==1.1.1',
'pyopenssl==0.15.1', 'pyopenssl==0.15.1',
'pyjwt==1.0.1', 'pyjwt==1.4.0',
'xmltodict==0.9.2', 'xmltodict==0.9.2',
'lockfile==0.10.2', 'lockfile==0.12.2',
'future==0.15.0', 'future==0.15.2',
] ]
tests_require = [ tests_require = [
'pyflakes', 'pyflakes',
'moto==0.4.6', 'moto==0.4.18',
'nose==1.3.7', 'nose==1.3.7',
'pytest==2.7.2', 'pytest==2.8.3',
'pytest-flask==0.8.1' 'pytest-flask==0.10.0'
] ]
docs_require = [ docs_require = [
@ -63,7 +73,7 @@ docs_require = [
] ]
dev_requires = [ dev_requires = [
'flake8>=2.0,<2.1', 'flake8>=2.0,<3.0',
] ]
@ -112,22 +122,23 @@ class BuildStatic(Command):
def run(self): def run(self):
log.info("running [npm install --quiet] in {0}".format(ROOT)) log.info("running [npm install --quiet] in {0}".format(ROOT))
try:
check_output(['npm', 'install', '--quiet'], cwd=ROOT)
check_output(['npm', 'install', '--quiet'], cwd=ROOT) log.info("running [gulp build]")
check_output([os.path.join(ROOT, 'node_modules', '.bin', 'gulp'), 'build'], cwd=ROOT)
log.info("running [gulp build]") log.info("running [gulp package]")
check_output([os.path.join(ROOT, 'node_modules', '.bin', 'gulp'), 'build'], cwd=ROOT) check_output([os.path.join(ROOT, 'node_modules', '.bin', 'gulp'), 'package'], cwd=ROOT)
log.info("running [gulp package]") except Exception as e:
check_output([os.path.join(ROOT, 'node_modules', '.bin', 'gulp'), 'package'], cwd=ROOT) log.warn("Unable to build static content")
setup( setup(
name='lemur', name=about["__title__"],
version='0.1.3', version=about["__version__"],
author='Kevin Glisson', author=about["__author__"],
author_email='kglisson@netflix.com', author_email=about["__email__"],
url='https://github.com/netflix/lemur', url=about["__uri__"],
download_url='https://github.com/Netflix/lemur/archive/0.1.3.tar.gz', description=about["__summary__"],
description='Certificate management and orchestration service',
long_description=open(os.path.join(ROOT, 'README.rst')).read(), long_description=open(os.path.join(ROOT, 'README.rst')).read(),
packages=find_packages(), packages=find_packages(),
include_package_data=True, include_package_data=True,
@ -149,11 +160,10 @@ setup(
], ],
'lemur.plugins': [ 'lemur.plugins': [
'verisign_issuer = lemur.plugins.lemur_verisign.plugin:VerisignIssuerPlugin', 'verisign_issuer = lemur.plugins.lemur_verisign.plugin:VerisignIssuerPlugin',
'cloudca_issuer = lemur.plugins.lemur_cloudca.plugin:CloudCAIssuerPlugin',
'cloudca_source = lemur.plugins.lemur_cloudca.plugin:CloudCASourcePlugin',
'aws_destination = lemur.plugins.lemur_aws.plugin:AWSDestinationPlugin', 'aws_destination = lemur.plugins.lemur_aws.plugin:AWSDestinationPlugin',
'aws_source = lemur.plugins.lemur_aws.plugin:AWSSourcePlugin', 'aws_source = lemur.plugins.lemur_aws.plugin:AWSSourcePlugin',
'email_notification = lemur.plugins.lemur_email.plugin:EmailNotificationPlugin', 'email_notification = lemur.plugins.lemur_email.plugin:EmailNotificationPlugin',
'java_export = lemur.plugins.lemur_java.plugin:JavaExportPlugin'
], ],
}, },
classifiers=[ classifiers=[
@ -161,6 +171,12 @@ setup(
'Intended Audience :: Developers', 'Intended Audience :: Developers',
'Intended Audience :: System Administrators', 'Intended Audience :: System Administrators',
'Operating System :: OS Independent', 'Operating System :: OS Independent',
'Topic :: Software Development' 'Topic :: Software Development',
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.3",
"Programming Language :: Python :: 3.4",
"Natural Language :: English",
"License :: OSI Approved :: Apache Software License"
] ]
) )