Compare commits
103 Commits
Author | SHA1 | Date | |
---|---|---|---|
837bfc3aa5 | |||
5ba1176f14 | |||
f08649b02d | |||
edbe5a254b | |||
cfedb30628 | |||
aa18b88a61 | |||
05962e71e3 | |||
bafc3d0082 | |||
308f1b44c3 | |||
cd17789529 | |||
bf988d89c4 | |||
b1e842ae47 | |||
fcc3c35ae2 | |||
e2524e43cf | |||
6aac2d62be | |||
95e2636f23 | |||
7565492bb9 | |||
89f7f12f92 | |||
cdd15ca818 | |||
11f2d88b16 | |||
8066d540e0 | |||
c3091a7346 | |||
9cadebcd50 | |||
3e54eb7520 | |||
068f19c895 | |||
3651cce542 | |||
9e0b9d9dda | |||
f194e2a1be | |||
f56c6f2836 | |||
ec896461a7 | |||
8eeed821d3 | |||
80c1689b24 | |||
7c29b566be | |||
920d595c12 | |||
5d7174b2a7 | |||
3c60f47e3f | |||
c4abc59673 | |||
ff4cdd82ee | |||
1c6e9caa40 | |||
07ec04ddc6 | |||
d6b3f5af81 | |||
ce1fe9321c | |||
2c88e4e3ba | |||
fed37c9dc0 | |||
e14eefdc31 | |||
2525d369d4 | |||
0600481a67 | |||
f0324e4755 | |||
00f0f957c0 | |||
9c652d784d | |||
eb2fa74661 | |||
146c599deb | |||
574c4033ab | |||
9f122eec18 | |||
eb0f6a04d8 | |||
c7230befe4 | |||
9a316ae1a9 | |||
df4364714e | |||
a1cd2b39eb | |||
8a7a15a361 | |||
2cdeecb6e0 | |||
638e4a5ac1 | |||
93b4ef5f17 | |||
26d490f74a | |||
01a1190524 | |||
2073090628 | |||
6d00cb208d | |||
13b9bf687d | |||
56f7da34d7 | |||
0f34440b64 | |||
bbcc7cca4e | |||
0453afcb0e | |||
cafecd1e19 | |||
4b968a9474 | |||
9244945e69 | |||
78819c1733 | |||
394e18f76e | |||
40eb950e94 | |||
90636a5329 | |||
2fc6d4cd21 | |||
b20bdf3c4e | |||
a20726a301 | |||
39727a1c9f | |||
168f46a436 | |||
4ec07a6dc7 | |||
798a6295ee | |||
73cb8da8c1 | |||
3167ce9785 | |||
63b7b71b49 | |||
9965af9ccd | |||
ba5d2c925a | |||
867be09e29 | |||
8362a92898 | |||
162482dbc4 | |||
c0f14db5bb | |||
3c561914c6 | |||
34c6f1bf4d | |||
2187898494 | |||
d4bc6ae7a1 | |||
81cdb15353 | |||
5cfa9d4bc5 | |||
92da453233 | |||
2aedfedbd3 |
3
.gitattributes
vendored
3
.gitattributes
vendored
@ -1 +1,2 @@
|
||||
* text=auto
|
||||
* text=auto
|
||||
version.py export-subst
|
@ -23,9 +23,6 @@ env:
|
||||
global:
|
||||
- PIP_DOWNLOAD_CACHE=".pip_download_cache"
|
||||
|
||||
install:
|
||||
- make dev-postgres
|
||||
|
||||
before_script:
|
||||
- psql -c "create database lemur;" -U postgres
|
||||
- psql -c "create user lemur with password 'lemur;'" -U postgres
|
||||
@ -36,4 +33,4 @@ script:
|
||||
|
||||
notifications:
|
||||
email:
|
||||
kglisson@netflix.com
|
||||
kglisson@netflix.com
|
||||
|
1
AUTHORS
1
AUTHORS
@ -1,2 +1,3 @@
|
||||
- Kevin Glisson <kglisson@netflix.com>
|
||||
- Jeremy Heffner <jheffner@netflix.com>
|
||||
|
||||
|
32
CHANGELOG.rst
Normal file
32
CHANGELOG.rst
Normal 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>`_
|
@ -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/static *
|
||||
global-exclude *~
|
||||
|
10
README.rst
10
README.rst
@ -16,12 +16,20 @@ Lemur
|
||||
.. image:: https://travis-ci.org/Netflix/lemur.svg
|
||||
: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
|
||||
|
||||
.. 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.
|
||||
|
||||
|
||||
Project resources
|
||||
=================
|
||||
|
||||
|
@ -29,11 +29,16 @@
|
||||
"angular-ui-switch": "~0.1.0",
|
||||
"angular-chart.js": "~0.7.1",
|
||||
"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": {
|
||||
"angular-mocks": "~1.3",
|
||||
"angular-scenario": "~1.3"
|
||||
"angular-scenario": "~1.3",
|
||||
"ngletteravatar": "~3.0.1"
|
||||
},
|
||||
"resolutions": {
|
||||
"bootstrap": "~3.3.1",
|
||||
|
@ -72,7 +72,7 @@ Basic Configuration
|
||||
.. data:: LEMUR_TOKEN_SECRET
|
||||
: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))
|
||||
|
||||
|
||||
.. data:: LEMUR_ENCRYPTION_KEY
|
||||
.. data:: LEMUR_ENCRYPTION_KEYS
|
||||
:noindex:
|
||||
|
||||
The LEMUR_ENCRYPTION_KEY is used to encrypt data at rest within Lemur's database. Without this key Lemur will refuse
|
||||
to start.
|
||||
The LEMUR_ENCRYPTION_KEYS is used to encrypt data at rest within Lemur's database. Without a key Lemur will refuse
|
||||
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
|
||||
---------------------------
|
||||
|
||||
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.
|
||||
|
||||
|
||||
@ -157,7 +163,7 @@ to be sent to subscribers.
|
||||
Templates for expiration emails are located under `lemur/plugins/lemur_email/templates` and can be modified for your needs.
|
||||
Notifications are sent to the certificate creator, owner and security team as specified by the `LEMUR_SECURITY_TEAM_EMAIL` configuration parameter.
|
||||
|
||||
Certificates marked as in-active will **not** be notified of upcoming expiration. This enables a user to essentially
|
||||
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
|
||||
30, 15, 2 days before expiration if no intervals are set.
|
||||
|
||||
@ -210,13 +216,13 @@ Authority Options
|
||||
-----------------
|
||||
|
||||
Authorities will each have their own configuration options. There is currently just one plugin bundled with Lemur,
|
||||
Verisign/Symantec. Additional plugins may define additional options. Refer to the plugins own documentation
|
||||
Verisign/Symantec. Additional plugins may define additional options. Refer to the plugin's own documentation
|
||||
for those plugins.
|
||||
|
||||
.. data:: VERISIGN_URL
|
||||
:noindex:
|
||||
|
||||
This is the url for the verisign API
|
||||
This is the url for the Verisign API
|
||||
|
||||
|
||||
.. data:: VERISIGN_PEM_PATH
|
||||
@ -256,7 +262,7 @@ for those plugins.
|
||||
|
||||
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.
|
||||
|
||||
For more information about how to use social logins, see: `Satellizer <https://github.com/sahat/satellizer>`_
|
||||
@ -295,7 +301,7 @@ For more information about how to use social logins, see: `Satellizer <https://g
|
||||
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 TLS certificates in AWS. But is not required to do so.
|
||||
|
||||
@ -348,9 +354,9 @@ 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.
|
||||
|
||||
@ -470,7 +476,7 @@ The configuration::
|
||||
|
||||
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
|
||||
settings.
|
||||
@ -554,9 +560,9 @@ All commands default to `~/.lemur/lemur.conf.py` if a configuration is not speci
|
||||
|
||||
.. 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
|
||||
validity it's status is marked 'unknown'
|
||||
validity its status is marked 'unknown'
|
||||
|
||||
|
||||
.. data:: sync
|
||||
@ -607,10 +613,10 @@ meaning.
|
||||
Within Lemur there are three main permissions: AdminPermission, CreatorPermission, OwnerPermission. Sub-permissions such
|
||||
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
|
||||
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
|
||||
structure is setup such that if the user is a `Creator` or `Owner` of a given certificate they are allow to view that
|
||||
@ -621,3 +627,34 @@ These permissions are applied to the user upon login and refreshed on every requ
|
||||
|
||||
.. seealso::
|
||||
`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.
|
||||
|
@ -1,2 +1 @@
|
||||
Change Log
|
||||
==========
|
||||
.. include:: ../CHANGELOG.rst
|
11
docs/conf.py
11
docs/conf.py
@ -11,7 +11,6 @@
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
@ -54,10 +53,12 @@ copyright = u'2015, Netflix Inc.'
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '0.1'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = '0.1.3'
|
||||
base_dir = os.path.join(os.path.dirname(__file__), os.pardir)
|
||||
about = {}
|
||||
with open(os.path.join(base_dir, "lemur", "__about__.py")) as f:
|
||||
exec(f.read(), about)
|
||||
|
||||
version = release = about["__version__"]
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
|
53
docs/doing-a-release.rst
Normal file
53
docs/doing-a-release.rst
Normal 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.
|
25
docs/faq.rst
25
docs/faq.rst
@ -4,8 +4,8 @@ Frequently Asked Questions
|
||||
Common Problems
|
||||
---------------
|
||||
|
||||
In my startup logs I see *'Aborting... Lemur cannot locate db encryption key, is LEMUR_ENCRYPTION_KEY set?'*
|
||||
You likely have not correctly configured **LEMUR_ENCRYPTION_KEY**. See
|
||||
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_KEYS**. See
|
||||
: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.
|
||||
|
||||
|
||||
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
|
||||
--------
|
||||
|
||||
|
@ -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()
|
@ -37,7 +37,7 @@ Create a New Certificate
|
||||
|
||||
.. figure:: create_certificate.png
|
||||
|
||||
Enter a owner, short description and the authority you wish to issue this certificate.
|
||||
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.
|
||||
|
||||
|
@ -21,7 +21,7 @@ Credential Management
|
||||
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.
|
||||
|
||||
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:
|
||||
|
||||
@ -72,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
|
||||
the Python server.
|
||||
|
||||
Therefor there are two steps:
|
||||
Therefore there are two steps:
|
||||
|
||||
- Run the Python process.
|
||||
- Run Nginx.
|
||||
@ -90,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.
|
||||
|
||||
`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::
|
||||
|
||||
@ -271,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
|
||||
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::
|
||||
|
||||
@ -293,6 +293,6 @@ Then you can manage the process by running::
|
||||
|
||||
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.
|
||||
|
@ -1,88 +1,93 @@
|
||||
Quickstart
|
||||
**********
|
||||
|
||||
This guide will step you through setting up a Python-based virtualenv, installing the required packages, and configuring the basic web service.
|
||||
This guide assumes a clean Ubuntu 14.04 instance, commands may differ based on the OS and configuration being used.
|
||||
This guide will step you through setting up a Python-based virtualenv, installing the required packages, and configuring the basic web service. This guide assumes a clean Ubuntu 14.04 instance, commands may differ based on the OS and configuration being used.
|
||||
|
||||
Pressed for time? See the Lemur docker file on `Github <https://github.com/Netflix/lemur-docker>`_.
|
||||
|
||||
|
||||
Dependencies
|
||||
------------
|
||||
|
||||
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
|
||||
* PostgreSQL
|
||||
* Nginx
|
||||
|
||||
.. 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.
|
||||
.. 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.
|
||||
|
||||
|
||||
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
|
||||
-------------------------
|
||||
|
||||
The first thing you'll need is the Python ``virtualenv`` package. You probably already
|
||||
have this, but if not, you can install it with::
|
||||
In this guide, Lemur will be installed in ``/www``, so you need to create that structure first:
|
||||
|
||||
pip install -U virtualenv
|
||||
.. code-block:: bash
|
||||
|
||||
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/``::
|
||||
$ sudo mkdir /www
|
||||
$ cd /www
|
||||
|
||||
virtualenv /www/lemur/
|
||||
Clone Lemur inside the just created directory and give yourself write permission (we assume ``lemur`` is the user):
|
||||
|
||||
Finally, activate your virtualenv::
|
||||
.. code-block:: bash
|
||||
|
||||
source /www/lemur/bin/activate
|
||||
$ sudo git clone https://github.com/Netflix/lemur
|
||||
$ sudo chown -R lemur lemur/
|
||||
|
||||
.. note:: Activating the environment adjusts your PATH, so that things like pip now
|
||||
install into the virtualenv by default.
|
||||
Create the virtual environment, activate it and enter the Lemur's directory:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
Installing build dependencies
|
||||
-----------------------------
|
||||
$ virtualenv lemur
|
||||
$ source /www/lemur/bin/activate
|
||||
$ cd lemur
|
||||
|
||||
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 postgresql
|
||||
.. note:: Activating the environment adjusts your PATH, so that things like pip now install into the virtualenv by default.
|
||||
|
||||
|
||||
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:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ which python
|
||||
|
||||
|
||||
And then run:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ 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
|
||||
------------------------
|
||||
|
||||
Before we run Lemur we must create a valid configuration file for it.
|
||||
|
||||
The Lemur cli comes with a simple command to get you up and running quickly.
|
||||
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.
|
||||
|
||||
Simply run:
|
||||
|
||||
@ -90,84 +95,85 @@ Simply run:
|
||||
|
||||
$ lemur create_config
|
||||
|
||||
.. 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.
|
||||
.. 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.
|
||||
|
||||
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
|
||||
-------------------------
|
||||
|
||||
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 stores etc..
|
||||
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.
|
||||
|
||||
.. 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
|
||||
--------------
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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
|
||||
# \password postgres
|
||||
Enter new password: lemur
|
||||
Enter it again: lemur
|
||||
.. code-block:: bash
|
||||
|
||||
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:
|
||||
|
||||
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
|
||||
------------------
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
In addition to create 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 garentee that every cerificate within
|
||||
Lemur will send one expiration notification to the security team.
|
||||
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.
|
||||
|
||||
Additional notifications can be created through the UI or API.
|
||||
See :ref:`Creating Notifications <CreatingNotifications>` and :ref:`Command Line Interface <CommandLineInterface>` for details.
|
||||
Additional notifications can be created through the UI or API. 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
|
||||
|
||||
$ lemur db init
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ lemur init
|
||||
|
||||
.. 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
|
||||
.. 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.
|
||||
|
||||
|
||||
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
|
||||
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.
|
||||
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.
|
||||
|
||||
|
||||
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
|
||||
|
||||
::
|
||||
|
||||
@ -180,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-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
|
||||
|
||||
location / {
|
||||
root /www/lemur/lemur/static/dist;
|
||||
include mime.types;
|
||||
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
|
||||
------------------------
|
||||
|
||||
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
|
||||
you can pass that via the --config option.
|
||||
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.
|
||||
|
||||
.. note::
|
||||
You can login with the default user created during :ref:`Initializing Lemur <InitializingLemur>` or any other
|
||||
@ -204,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!
|
||||
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
|
||||
---------------------------
|
||||
--------------------------
|
||||
|
||||
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``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
::
|
||||
|
||||
@ -235,11 +247,11 @@ folder and you're good to go.
|
||||
|
||||
See :ref:`Using Supervisor <UsingSupervisor>` for more details on using Supervisor.
|
||||
|
||||
|
||||
Syncing
|
||||
-------
|
||||
|
||||
Lemur uses periodic sync tasks to make sure it is up-to-date with it's environment. As always things can change outside
|
||||
of Lemur, but we do our best to reconcile those changes.
|
||||
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:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
@ -247,32 +259,30 @@ of Lemur, but we do our best to reconcile those changes.
|
||||
* 3 * * * lemur sync --all
|
||||
* 3 * * * lemur check_revoked
|
||||
|
||||
|
||||
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
|
||||
``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.
|
||||
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.
|
||||
|
||||
Some of the features which you'll likely find useful are listed below.
|
||||
|
||||
Some of those which you'll likely find useful are:
|
||||
|
||||
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
|
||||
~~~~~~
|
||||
|
||||
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?
|
||||
------------
|
||||
|
||||
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 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.
|
||||
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.
|
||||
|
||||
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.
|
||||
|
@ -6,24 +6,24 @@ markupsafe
|
||||
sphinxcontrib-httpdomain
|
||||
Flask==0.10.1
|
||||
Flask-RESTful==0.3.3
|
||||
Flask-SQLAlchemy==2.0
|
||||
Flask-SQLAlchemy==2.1
|
||||
Flask-Script==2.0.5
|
||||
Flask-Migrate==1.4.0
|
||||
Flask-Bcrypt==0.6.2
|
||||
Flask-Migrate==1.6.0
|
||||
Flask-Bcrypt==0.7.1
|
||||
Flask-Principal==0.4.0
|
||||
Flask-Mail==0.9.1
|
||||
SQLAlchemy-Utils==0.30.11
|
||||
SQLAlchemy-Utils==0.31.3
|
||||
BeautifulSoup4
|
||||
requests==2.7.0
|
||||
requests==2.8.1
|
||||
psycopg2==2.6.1
|
||||
arrow==0.5.4
|
||||
arrow==0.7.0
|
||||
boto==2.38.0 # we might make this optional
|
||||
six==1.9.0
|
||||
gunicorn==19.3.0
|
||||
six==1.10.0
|
||||
gunicorn==19.4.1
|
||||
pycrypto==2.6.1
|
||||
cryptography==1.0.1
|
||||
cryptography==1.1.1
|
||||
pyopenssl==0.15.1
|
||||
pyjwt==1.0.1
|
||||
pyjwt==1.4.0
|
||||
xmltodict==0.9.2
|
||||
lockfile==0.10.2
|
||||
future==0.15.0
|
||||
lockfile==0.12.2
|
||||
future==0.15.2
|
||||
|
66
docs/security.rst
Normal file
66
docs/security.rst
Normal 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
|
@ -72,7 +72,6 @@ gulp.task('dev:styles', function () {
|
||||
};
|
||||
|
||||
var fileList = [
|
||||
'lemur/static/app/styles/lemur.css',
|
||||
'bower_components/bootswatch/sandstone/bootswatch.less',
|
||||
'bower_components/fontawesome/css/font-awesome.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-wizard/dist/angular-wizard.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)
|
||||
@ -238,8 +238,8 @@ gulp.task('build:images', function () {
|
||||
|
||||
gulp.task('package:strip', function () {
|
||||
return gulp.src(['lemur/static/dist/scripts/main*'])
|
||||
.pipe(replace('http:\/\/localhost:5000', ''))
|
||||
.pipe(replace('http:\/\/localhost:3000', ''))
|
||||
.pipe(replace('http:\/\/localhost:8000', ''))
|
||||
.pipe(useref())
|
||||
.pipe(revReplace())
|
||||
.pipe(gulp.dest('lemur/static/dist/scripts'))
|
||||
|
18
lemur/__about__.py
Normal file
18
lemur/__about__.py
Normal 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__)
|
@ -8,6 +8,8 @@
|
||||
|
||||
|
||||
"""
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
from lemur import factory
|
||||
|
||||
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.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 = (
|
||||
users_bp,
|
||||
|
@ -35,7 +35,7 @@ class Login(Resource):
|
||||
|
||||
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.
|
||||
|
||||
.. note:: It is recommended that the token expiration is fairly short lived (hours not days). This will largely depend \
|
||||
|
@ -14,7 +14,7 @@ from sqlalchemy import Column, Integer, String, Text, func, ForeignKey, DateTime
|
||||
from sqlalchemy.dialects.postgresql import JSON
|
||||
|
||||
from lemur.database import db
|
||||
from lemur.certificates.models import cert_get_cn, cert_get_not_after, cert_get_not_before
|
||||
from lemur.certificates.models import get_cn, get_not_after, get_not_before
|
||||
|
||||
|
||||
class Authority(db.Model):
|
||||
@ -44,9 +44,9 @@ class Authority(db.Model):
|
||||
self.owner = owner
|
||||
self.plugin_name = plugin_name
|
||||
cert = x509.load_pem_x509_certificate(str(body), default_backend())
|
||||
self.cn = cert_get_cn(cert)
|
||||
self.not_before = cert_get_not_before(cert)
|
||||
self.not_after = cert_get_not_after(cert)
|
||||
self.cn = get_cn(cert)
|
||||
self.not_before = get_not_before(cert)
|
||||
self.not_after = get_not_after(cert)
|
||||
self.roles = roles
|
||||
self.description = description
|
||||
|
||||
|
@ -58,7 +58,15 @@ def create(kwargs):
|
||||
|
||||
cert = Certificate(cert_body, chain=intermediate)
|
||||
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.notifications = notification_service.create_default_expiration_notifications(
|
||||
|
@ -13,9 +13,7 @@ from cryptography.hazmat.backends import default_backend
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy import event, Integer, ForeignKey, String, DateTime, PassiveDefault, func, Column, Text, Boolean
|
||||
|
||||
from sqlalchemy_utils import EncryptedType
|
||||
|
||||
from lemur.utils import get_key
|
||||
from lemur.utils import Vault
|
||||
from lemur.database import db
|
||||
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.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):
|
||||
@ -34,6 +33,11 @@ def create_name(issuer, not_before, not_after, subject, san):
|
||||
useful information such as Common Name, Validation dates,
|
||||
and Issuer.
|
||||
|
||||
:param san:
|
||||
:param subject:
|
||||
:param not_after:
|
||||
:param issuer:
|
||||
:param not_before:
|
||||
:rtype : str
|
||||
:return:
|
||||
"""
|
||||
@ -63,7 +67,11 @@ def create_name(issuer, not_before, not_after, subject, san):
|
||||
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.
|
||||
|
||||
@ -75,7 +83,7 @@ def cert_get_cn(cert):
|
||||
)[0].value.strip()
|
||||
|
||||
|
||||
def cert_get_domains(cert):
|
||||
def get_domains(cert):
|
||||
"""
|
||||
Attempts to get an domains listed in a certificate.
|
||||
If 'subjectAltName' extension is not available we simply
|
||||
@ -96,7 +104,7 @@ def cert_get_domains(cert):
|
||||
return domains
|
||||
|
||||
|
||||
def cert_get_serial(cert):
|
||||
def get_serial(cert):
|
||||
"""
|
||||
Fetch the serial number from the certificate.
|
||||
|
||||
@ -106,7 +114,7 @@ def cert_get_serial(cert):
|
||||
return cert.serial
|
||||
|
||||
|
||||
def cert_is_san(cert):
|
||||
def is_san(cert):
|
||||
"""
|
||||
Determines if a given certificate is a SAN certificate.
|
||||
SAN certificates are simply certificates that cover multiple domains.
|
||||
@ -114,18 +122,18 @@ def cert_is_san(cert):
|
||||
:param cert:
|
||||
:return: Bool
|
||||
"""
|
||||
if len(cert_get_domains(cert)) > 1:
|
||||
if len(get_domains(cert)) > 1:
|
||||
return True
|
||||
|
||||
|
||||
def cert_is_wildcard(cert):
|
||||
def is_wildcard(cert):
|
||||
"""
|
||||
Determines if certificate is a wildcard certificate.
|
||||
|
||||
:param cert:
|
||||
:return: Bool
|
||||
"""
|
||||
domains = cert_get_domains(cert)
|
||||
domains = get_domains(cert)
|
||||
if len(domains) == 1 and domains[0][0:1] == "*":
|
||||
return True
|
||||
|
||||
@ -133,7 +141,7 @@ def cert_is_wildcard(cert):
|
||||
return True
|
||||
|
||||
|
||||
def cert_get_bitstrength(cert):
|
||||
def get_bitstrength(cert):
|
||||
"""
|
||||
Calculates a certificates public key bit length.
|
||||
|
||||
@ -143,7 +151,7 @@ def cert_get_bitstrength(cert):
|
||||
return cert.public_key().key_size
|
||||
|
||||
|
||||
def cert_get_issuer(cert):
|
||||
def get_issuer(cert):
|
||||
"""
|
||||
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))
|
||||
|
||||
|
||||
def cert_get_not_before(cert):
|
||||
def get_not_before(cert):
|
||||
"""
|
||||
Gets the naive datetime of the certificates 'not_before' field.
|
||||
This field denotes the first date in time which the given certificate
|
||||
@ -172,7 +180,7 @@ def cert_get_not_before(cert):
|
||||
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.
|
||||
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)
|
||||
owner = Column(String(128))
|
||||
body = Column(Text())
|
||||
private_key = Column(EncryptedType(String, get_key))
|
||||
private_key = Column(Vault)
|
||||
status = Column(String(128))
|
||||
deleted = Column(Boolean, index=True)
|
||||
name = Column(String(128))
|
||||
@ -224,10 +232,16 @@ class Certificate(db.Model):
|
||||
not_before = Column(DateTime)
|
||||
not_after = Column(DateTime)
|
||||
date_created = Column(DateTime, PassiveDefault(func.now()), nullable=False)
|
||||
signing_algorithm = Column(String(128))
|
||||
user_id = Column(Integer, ForeignKey('users.id'))
|
||||
authority_id = Column(Integer, ForeignKey('authorities.id'))
|
||||
notifications = relationship("Notification", secondary=certificate_notification_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')
|
||||
domains = relationship("Domain", secondary=certificate_associations, backref="certificate")
|
||||
|
||||
@ -237,16 +251,17 @@ class Certificate(db.Model):
|
||||
self.private_key = private_key
|
||||
self.chain = chain
|
||||
cert = x509.load_pem_x509_certificate(str(self.body), default_backend())
|
||||
self.bits = cert_get_bitstrength(cert)
|
||||
self.issuer = cert_get_issuer(cert)
|
||||
self.serial = cert_get_serial(cert)
|
||||
self.cn = cert_get_cn(cert)
|
||||
self.san = cert_is_san(cert)
|
||||
self.not_before = cert_get_not_before(cert)
|
||||
self.not_after = cert_get_not_after(cert)
|
||||
self.signing_algorithm = get_signing_algorithm(cert)
|
||||
self.bits = get_bitstrength(cert)
|
||||
self.issuer = get_issuer(cert)
|
||||
self.serial = get_serial(cert)
|
||||
self.cn = get_cn(cert)
|
||||
self.san = is_san(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)
|
||||
|
||||
for domain in cert_get_domains(cert):
|
||||
for domain in get_domains(cert):
|
||||
self.domains.append(Domain(name=domain))
|
||||
|
||||
@property
|
||||
@ -276,11 +291,44 @@ class Certificate(db.Model):
|
||||
"""
|
||||
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')
|
||||
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.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.")
|
||||
|
@ -17,6 +17,7 @@ from lemur.certificates.models import Certificate
|
||||
from lemur.destinations.models import Destination
|
||||
from lemur.notifications.models import Notification
|
||||
from lemur.authorities.models import Authority
|
||||
from lemur.domains.models import Domain
|
||||
|
||||
from lemur.roles.models import Role
|
||||
|
||||
@ -76,13 +77,30 @@ def find_duplicates(cert_body):
|
||||
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 owner:
|
||||
:param description:
|
||||
:param active:
|
||||
:param destinations:
|
||||
:param notifications:
|
||||
:param replaces:
|
||||
:return:
|
||||
"""
|
||||
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
|
||||
|
||||
database.update_list(cert, 'destinations', Destination, destinations)
|
||||
database.update_list(cert, 'replaces', Certificate, replaces)
|
||||
|
||||
cert.owner = owner
|
||||
|
||||
@ -165,6 +184,7 @@ def import_certificate(**kwargs):
|
||||
|
||||
notification_name = 'DEFAULT_SECURITY'
|
||||
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 = database.create(cert)
|
||||
@ -194,8 +214,8 @@ def upload(**kwargs):
|
||||
g.user.certificates.append(cert)
|
||||
|
||||
database.update_list(cert, 'destinations', Destination, kwargs.get('destinations'))
|
||||
|
||||
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
|
||||
notifications = []
|
||||
@ -228,7 +248,7 @@ def create(**kwargs):
|
||||
# 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.
|
||||
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'))
|
||||
|
||||
# create default notifications for this certificate if none are provided
|
||||
@ -266,6 +286,7 @@ def render(args):
|
||||
|
||||
if filt:
|
||||
terms = filt.split(';')
|
||||
|
||||
if 'issuer' in terms:
|
||||
# we can't rely on issuer being correct in the cert directly so we combine queries
|
||||
sub_query = database.session_query(Authority.id)\
|
||||
@ -280,10 +301,17 @@ def render(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]))
|
||||
elif 'active' in filt: # this is really weird but strcmp seems to not work here??
|
||||
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:
|
||||
query = database.filter(query, Certificate, terms)
|
||||
|
||||
|
@ -5,7 +5,6 @@
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
import os
|
||||
import requests
|
||||
import subprocess
|
||||
from OpenSSL import crypto
|
||||
@ -13,20 +12,7 @@ from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
|
||||
from flask import current_app
|
||||
|
||||
from contextlib import contextmanager
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
|
||||
@contextmanager
|
||||
def mktempfile():
|
||||
with NamedTemporaryFile(delete=False) as f:
|
||||
name = f.name
|
||||
|
||||
try:
|
||||
yield name
|
||||
finally:
|
||||
os.unlink(name)
|
||||
from lemur.utils import mktempfile
|
||||
|
||||
|
||||
def ocsp_verify(cert_path, issuer_chain_path):
|
||||
|
@ -5,32 +5,24 @@
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
import base64
|
||||
from builtins import str
|
||||
|
||||
from flask import Blueprint, make_response, jsonify
|
||||
from flask.ext.restful import reqparse, Api, fields
|
||||
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
|
||||
from lemur.certificates import service
|
||||
from lemur.authorities.models import Authority
|
||||
|
||||
from lemur.auth.service import AuthenticatedResource
|
||||
from lemur.auth.permissions import ViewKeyPermission, AuthorityPermission, UpdateCertificatePermission
|
||||
|
||||
from lemur.roles import service as role_service
|
||||
|
||||
from lemur.common.utils import marshal_items, paginated_parser
|
||||
|
||||
from lemur.notifications.views import notification_list
|
||||
|
||||
|
||||
mod = Blueprint('certificates', __name__)
|
||||
api = Api(mod)
|
||||
|
||||
|
||||
FIELDS = {
|
||||
'name': fields.String,
|
||||
'id': fields.Integer,
|
||||
@ -46,6 +38,7 @@ FIELDS = {
|
||||
'notBefore': fields.DateTime(dt_format='iso8601', attribute='not_before'),
|
||||
'notAfter': fields.DateTime(dt_format='iso8601', attribute='not_after'),
|
||||
'cn': fields.String,
|
||||
'signingAlgorithm': fields.String(attribute='signing_algorithm'),
|
||||
'status': fields.String,
|
||||
'body': fields.String
|
||||
}
|
||||
@ -102,6 +95,7 @@ def private_key_str(value, name):
|
||||
|
||||
class CertificatesList(AuthenticatedResource):
|
||||
""" Defines the 'certificates' endpoint """
|
||||
|
||||
def __init__(self):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(CertificatesList, self).__init__()
|
||||
@ -268,7 +262,10 @@ class CertificatesList(AuthenticatedResource):
|
||||
},
|
||||
"commonName": "test",
|
||||
"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**:
|
||||
@ -316,10 +313,11 @@ class CertificatesList(AuthenticatedResource):
|
||||
self.reqparse.add_argument('extensions', type=dict, 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('replacements', type=list, default=[], location='json')
|
||||
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('authority', type=valid_authority, location='json', required=True)
|
||||
self.reqparse.add_argument('description', type=str, location='json', required=True)
|
||||
self.reqparse.add_argument('description', type=str, location='json')
|
||||
self.reqparse.add_argument('country', type=str, location='json', required=True)
|
||||
self.reqparse.add_argument('state', type=str, location='json', required=True)
|
||||
self.reqparse.add_argument('location', type=str, location='json', required=True)
|
||||
@ -348,6 +346,7 @@ class CertificatesList(AuthenticatedResource):
|
||||
|
||||
class CertificatesUpload(AuthenticatedResource):
|
||||
""" Defines the 'certificates' upload endpoint """
|
||||
|
||||
def __init__(self):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(CertificatesUpload, self).__init__()
|
||||
@ -374,6 +373,7 @@ class CertificatesUpload(AuthenticatedResource):
|
||||
"privateKey": "---Begin Private..."
|
||||
"destinations": [],
|
||||
"notifications": [],
|
||||
"replacements": [],
|
||||
"name": "cert1"
|
||||
}
|
||||
|
||||
@ -400,6 +400,7 @@ class CertificatesUpload(AuthenticatedResource):
|
||||
"active": true,
|
||||
"notBefore": "2015-06-05T17:09:39",
|
||||
"notAfter": "2015-06-10T17:09:39",
|
||||
"signingAlgorithm": "sha2"
|
||||
"cn": "example.com",
|
||||
"status": "unknown"
|
||||
}
|
||||
@ -417,8 +418,9 @@ class CertificatesUpload(AuthenticatedResource):
|
||||
self.reqparse.add_argument('owner', type=str, required=True, location='json')
|
||||
self.reqparse.add_argument('name', type=str, location='json')
|
||||
self.reqparse.add_argument('publicCert', type=pem_str, required=True, dest='public_cert', location='json')
|
||||
self.reqparse.add_argument('destinations', type=list, default=[], dest='destinations', location='json')
|
||||
self.reqparse.add_argument('notifications', type=list, default=[], dest='notifications', location='json')
|
||||
self.reqparse.add_argument('destinations', type=list, default=[], 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('privateKey', type=private_key_str, dest='private_key', location='json')
|
||||
|
||||
@ -433,6 +435,7 @@ class CertificatesUpload(AuthenticatedResource):
|
||||
|
||||
class CertificatesStats(AuthenticatedResource):
|
||||
""" Defines the 'certificates' stats endpoint """
|
||||
|
||||
def __init__(self):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(CertificatesStats, self).__init__()
|
||||
@ -543,6 +546,7 @@ class Certificates(AuthenticatedResource):
|
||||
"active": true,
|
||||
"notBefore": "2015-06-05T17:09:39",
|
||||
"notAfter": "2015-06-10T17:09:39",
|
||||
"signingAlgorithm": "sha2",
|
||||
"cn": "example.com",
|
||||
"status": "unknown"
|
||||
}
|
||||
@ -572,7 +576,8 @@ class Certificates(AuthenticatedResource):
|
||||
"owner": "jimbob@example.com",
|
||||
"active": false
|
||||
"notifications": [],
|
||||
"destinations": []
|
||||
"destinations": [],
|
||||
"replacements": []
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
@ -611,6 +616,7 @@ class Certificates(AuthenticatedResource):
|
||||
self.reqparse.add_argument('description', type=str, location='json')
|
||||
self.reqparse.add_argument('destinations', type=list, default=[], location='json')
|
||||
self.reqparse.add_argument('notifications', type=notification_list, default=[], location='json')
|
||||
self.reqparse.add_argument('replacements', type=list, default=[], location='json')
|
||||
args = self.reqparse.parse_args()
|
||||
|
||||
cert = service.get(certificate_id)
|
||||
@ -625,7 +631,8 @@ class Certificates(AuthenticatedResource):
|
||||
args['description'],
|
||||
args['active'],
|
||||
args['destinations'],
|
||||
args['notifications']
|
||||
args['notifications'],
|
||||
args['replacements']
|
||||
)
|
||||
|
||||
return dict(message='You are not authorized to update this certificate'), 403
|
||||
@ -633,6 +640,7 @@ class Certificates(AuthenticatedResource):
|
||||
|
||||
class NotificationCertificatesList(AuthenticatedResource):
|
||||
""" Defines the 'certificates' endpoint """
|
||||
|
||||
def __init__(self):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(NotificationCertificatesList, self).__init__()
|
||||
@ -677,6 +685,7 @@ class NotificationCertificatesList(AuthenticatedResource):
|
||||
"active": true,
|
||||
"notBefore": "2015-06-05T17:09:39",
|
||||
"notAfter": "2015-06-10T17:09:39",
|
||||
"signingAlgorithm": "sha2",
|
||||
"cn": "example.com",
|
||||
"status": "unknown"
|
||||
}
|
||||
@ -707,9 +716,154 @@ class NotificationCertificatesList(AuthenticatedResource):
|
||||
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(Certificates, '/certificates/<int:certificate_id>', endpoint='certificate')
|
||||
api.add_resource(CertificatesStats, '/certificates/stats', endpoint='certificateStats')
|
||||
api.add_resource(CertificatesUpload, '/certificates/upload', endpoint='certificateUpload')
|
||||
api.add_resource(CertificatePrivateKey, '/certificates/<int:certificate_id>/key', endpoint='privateKeyCertificates')
|
||||
api.add_resource(NotificationCertificatesList, '/notifications/<int:notification_id>/certificates', endpoint='notificationCertificates')
|
||||
api.add_resource(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')
|
||||
|
@ -22,7 +22,8 @@ class InstanceManager(object):
|
||||
|
||||
def add(self, class_path):
|
||||
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):
|
||||
self.cache = None
|
||||
|
@ -63,9 +63,9 @@ class marshal_items(object):
|
||||
if hasattr(e, 'data'):
|
||||
return {'message': e.data['message']}, 400
|
||||
else:
|
||||
return {'message': 'unknown'}, 400
|
||||
return {'message': {'exception': 'unknown'}}, 400
|
||||
else:
|
||||
return {'message': str(e)}, 400
|
||||
return {'message': {'exception': str(e)}}, 400
|
||||
return wrapper
|
||||
|
||||
|
||||
|
@ -8,6 +8,7 @@
|
||||
from sqlalchemy import func
|
||||
|
||||
from lemur import database
|
||||
from lemur.models import certificate_destination_associations
|
||||
from lemur.destinations.models import Destination
|
||||
from lemur.certificates.models import Certificate
|
||||
|
||||
@ -117,10 +118,9 @@ def stats(**kwargs):
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
attr = getattr(Destination, kwargs.get('metric'))
|
||||
query = database.db.session.query(attr, func.count(attr))
|
||||
|
||||
items = query.group_by(attr).all()
|
||||
items = database.db.session.query(Destination.label, func.count(certificate_destination_associations.c.certificate_id))\
|
||||
.join(certificate_destination_associations)\
|
||||
.group_by(Destination.label).all()
|
||||
|
||||
keys = []
|
||||
values = []
|
||||
|
@ -72,7 +72,7 @@ SECRET_KEY = '{flask_secret_key}'
|
||||
|
||||
# You should consider storing these separately from your config
|
||||
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
|
||||
LEMUR_RESTRICTED_DOMAINS = []
|
||||
@ -171,7 +171,9 @@ def generate_settings():
|
||||
settings file.
|
||||
"""
|
||||
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)),
|
||||
flask_secret_key=base64.b64encode(os.urandom(KEY_LENGTH)),
|
||||
)
|
||||
@ -314,7 +316,7 @@ class CreateUser(Command):
|
||||
Option('-u', '--username', dest='username', required=True),
|
||||
Option('-e', '--email', dest='email', required=True),
|
||||
Option('-a', '--active', dest='active', default=True),
|
||||
Option('-r', '--roles', dest='roles', default=[])
|
||||
Option('-r', '--roles', dest='roles', action='append', default=[])
|
||||
)
|
||||
|
||||
def run(self, username, email, active, roles):
|
||||
@ -716,6 +718,24 @@ def publish_verisign_units():
|
||||
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():
|
||||
manager.add_command("start", LemurServer())
|
||||
manager.add_command("runserver", Server(host='127.0.0.1'))
|
||||
|
31
lemur/migrations/versions/33de094da890_.py
Normal file
31
lemur/migrations/versions/33de094da890_.py
Normal 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 ###
|
26
lemur/migrations/versions/4bcfa2c36623_.py
Normal file
26
lemur/migrations/versions/4bcfa2c36623_.py
Normal 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 ###
|
255
lemur/migrations/versions/ed422fc58ba_.py
Normal file
255
lemur/migrations/versions/ed422fc58ba_.py
Normal 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')
|
@ -36,6 +36,14 @@ certificate_notification_associations = db.Table('certificate_notification_assoc
|
||||
Column('certificate_id', Integer,
|
||||
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',
|
||||
Column('user_id', Integer, ForeignKey('users.id')),
|
||||
Column('role_id', Integer, ForeignKey('roles.id'))
|
||||
|
@ -108,10 +108,11 @@ class IPlugin(local):
|
||||
"""
|
||||
return self.resource_links
|
||||
|
||||
def get_option(self, name, options):
|
||||
@staticmethod
|
||||
def get_option(name, options):
|
||||
for o in options:
|
||||
if o.get('name') == name:
|
||||
return o['value']
|
||||
return o.get('value')
|
||||
|
||||
|
||||
class Plugin(IPlugin):
|
||||
|
@ -2,3 +2,4 @@ from .destination import DestinationPlugin # noqa
|
||||
from .issuer import IssuerPlugin # noqa
|
||||
from .source import SourcePlugin # noqa
|
||||
from .notification import NotificationPlugin, ExpirationNotificationPlugin # noqa
|
||||
from .export import ExportPlugin # noqa
|
||||
|
20
lemur/plugins/bases/export.py
Normal file
20
lemur/plugins/bases/export.py
Normal 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
|
@ -1,5 +1,12 @@
|
||||
import os
|
||||
import arrow
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
|
||||
loader = FileSystemLoader(searchpath=os.path.dirname(os.path.realpath(__file__)))
|
||||
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
16
lemur/plugins/lemur_email/tests/test_email.py
Normal file
16
lemur/plugins/lemur_email/tests/test_email.py
Normal 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'))
|
5
lemur/plugins/lemur_java/__init__.py
Normal file
5
lemur/plugins/lemur_java/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
try:
|
||||
VERSION = __import__('pkg_resources') \
|
||||
.get_distribution(__name__).version
|
||||
except Exception as e:
|
||||
VERSION = 'unknown'
|
175
lemur/plugins/lemur_java/plugin.py
Normal file
175
lemur/plugins/lemur_java/plugin.py
Normal 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
|
1
lemur/plugins/lemur_java/tests/conftest.py
Normal file
1
lemur/plugins/lemur_java/tests/conftest.py
Normal file
@ -0,0 +1 @@
|
||||
from lemur.tests.conftest import * # noqa
|
63
lemur/plugins/lemur_java/tests/test_java.py
Normal file
63
lemur/plugins/lemur_java/tests/test_java.py
Normal 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""
|
@ -86,7 +86,7 @@ class PluginsList(AuthenticatedResource):
|
||||
if args['type']:
|
||||
return list(plugins.all(plugin_type=args['type']))
|
||||
|
||||
return plugins.all()
|
||||
return list(plugins.all())
|
||||
|
||||
|
||||
class Plugins(AuthenticatedResource):
|
||||
|
@ -12,9 +12,8 @@
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy import Column, Integer, String, Text, ForeignKey
|
||||
|
||||
from sqlalchemy_utils import EncryptedType
|
||||
from lemur.database import db
|
||||
from lemur.utils import get_key
|
||||
from lemur.utils import Vault
|
||||
from lemur.models import roles_users
|
||||
|
||||
|
||||
@ -23,7 +22,7 @@ class Role(db.Model):
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String(128), unique=True)
|
||||
username = Column(String(128))
|
||||
password = Column(EncryptedType(String, get_key))
|
||||
password = Column(Vault)
|
||||
description = Column(Text)
|
||||
authority_id = Column(Integer, ForeignKey('authorities.id'))
|
||||
user_id = Column(Integer, ForeignKey('users.id'))
|
||||
|
34
lemur/static/app/angular/app.js
vendored
34
lemur/static/app/angular/app.js
vendored
@ -2,7 +2,7 @@
|
||||
|
||||
var lemur = angular
|
||||
.module('lemur', [
|
||||
'ngRoute',
|
||||
'ui.router',
|
||||
'ngTable',
|
||||
'ngAnimate',
|
||||
'chart.js',
|
||||
@ -13,15 +13,18 @@ var lemur = angular
|
||||
'toaster',
|
||||
'uiSwitch',
|
||||
'mgo-angular-wizard',
|
||||
'satellizer'
|
||||
'satellizer',
|
||||
'ngLetterAvatar',
|
||||
'angular-clipboard',
|
||||
'ngFileSaver'
|
||||
])
|
||||
.config(function ($routeProvider, $authProvider) {
|
||||
$routeProvider
|
||||
.when('/', {
|
||||
.config(function ($stateProvider, $urlRouterProvider, $authProvider) {
|
||||
$urlRouterProvider.otherwise('/welcome');
|
||||
|
||||
$stateProvider
|
||||
.state('welcome', {
|
||||
url: '/welcome',
|
||||
templateUrl: 'angular/welcome/welcome.html'
|
||||
})
|
||||
.otherwise({
|
||||
redirectTo: '/'
|
||||
});
|
||||
|
||||
$authProvider.oauth2({
|
||||
@ -71,7 +74,7 @@ lemur.service('DefaultService', function (LemurRestangular) {
|
||||
|
||||
lemur.factory('LemurRestangular', function (Restangular, $location, $auth) {
|
||||
return Restangular.withConfig(function (RestangularConfigurer) {
|
||||
RestangularConfigurer.setBaseUrl('http://localhost:5000/api/1');
|
||||
RestangularConfigurer.setBaseUrl('http://localhost:8000/api/1');
|
||||
RestangularConfigurer.setDefaultHttpFields({withCredentials: true});
|
||||
|
||||
RestangularConfigurer.addResponseInterceptor(function (data, operation) {
|
||||
@ -85,9 +88,22 @@ lemur.factory('LemurRestangular', function (Restangular, $location, $auth) {
|
||||
} else {
|
||||
extractedData = data;
|
||||
}
|
||||
|
||||
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) {
|
||||
// We want to make sure the user is auth'd before any requests
|
||||
if (!$auth.isAuthenticated()) {
|
||||
|
@ -1,8 +1,9 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('lemur')
|
||||
.config(function config($routeProvider) {
|
||||
$routeProvider.when('/login', {
|
||||
.config(function config($stateProvider) {
|
||||
$stateProvider.state('login', {
|
||||
url: '/login',
|
||||
templateUrl: '/angular/authentication/login/login.tpl.html',
|
||||
controller: 'LoginController'
|
||||
});
|
||||
|
@ -1,9 +1,10 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('lemur')
|
||||
.config(function config($routeProvider) {
|
||||
$routeProvider.when('/logout', {
|
||||
controller: 'LogoutCtrl'
|
||||
.config(function config($stateProvider) {
|
||||
$stateProvider.state('logout', {
|
||||
controller: 'LogoutCtrl',
|
||||
url: '/logout'
|
||||
});
|
||||
})
|
||||
.controller('LogoutCtrl', function ($scope, $location, lemurRestangular, userService) {
|
||||
|
@ -2,24 +2,32 @@
|
||||
|
||||
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) {
|
||||
AuthorityService.getRoles(authority);
|
||||
$scope.authority = authority;
|
||||
});
|
||||
|
||||
$scope.authorityService = AuthorityService;
|
||||
$scope.roleService = RoleService;
|
||||
|
||||
$scope.save = function (authority) {
|
||||
AuthorityService.update(authority).then(
|
||||
function () {
|
||||
toaster.pop({
|
||||
type: 'success',
|
||||
title: authority.name,
|
||||
body: 'Successfully updated!'
|
||||
});
|
||||
$modalInstance.close();
|
||||
},
|
||||
function () {
|
||||
|
||||
}
|
||||
);
|
||||
function (response) {
|
||||
toaster.pop({
|
||||
type: 'error',
|
||||
title: authority.name,
|
||||
body: 'Update Failed! ' + response.data.message,
|
||||
timeout: 100000
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$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');
|
||||
|
||||
// set the defaults
|
||||
AuthorityService.getDefaults($scope.authority);
|
||||
|
||||
$scope.loading = false;
|
||||
$scope.create = function (authority) {
|
||||
WizardHandler.wizard().context.loading = true;
|
||||
AuthorityService.create(authority).then(function () {
|
||||
WizardHandler.wizard().context.loading = false;
|
||||
$modalInstance.close();
|
||||
AuthorityService.create(authority).then(
|
||||
function () {
|
||||
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;
|
||||
});
|
||||
};
|
||||
|
||||
|
53
lemur/static/app/angular/authorities/services.js
vendored
53
lemur/static/app/angular/authorities/services.js
vendored
@ -56,7 +56,7 @@ angular.module('lemur')
|
||||
});
|
||||
return LemurRestangular.all('authorities');
|
||||
})
|
||||
.service('AuthorityService', function ($location, AuthorityApi, DefaultService, toaster) {
|
||||
.service('AuthorityService', function ($location, AuthorityApi, DefaultService) {
|
||||
var AuthorityService = this;
|
||||
AuthorityService.findAuthorityByName = function (filterValue) {
|
||||
return AuthorityApi.getList({'filter[name]': filterValue})
|
||||
@ -80,41 +80,11 @@ angular.module('lemur')
|
||||
|
||||
AuthorityService.create = function (authority) {
|
||||
authority.attachSubAltName();
|
||||
return AuthorityApi.post(authority).then(
|
||||
function () {
|
||||
toaster.pop({
|
||||
type: 'success',
|
||||
title: authority.name,
|
||||
body: 'Successfully created!'
|
||||
});
|
||||
$location.path('/authorities');
|
||||
},
|
||||
function (response) {
|
||||
toaster.pop({
|
||||
type: 'error',
|
||||
title: authority.name,
|
||||
body: 'Was not created! ' + response.data.message
|
||||
});
|
||||
});
|
||||
return AuthorityApi.post(authority);
|
||||
};
|
||||
|
||||
AuthorityService.update = function (authority) {
|
||||
return authority.put().then(
|
||||
function () {
|
||||
toaster.pop({
|
||||
type: 'success',
|
||||
title: authority.name,
|
||||
body: 'Successfully updated!'
|
||||
});
|
||||
$location.path('/authorities');
|
||||
},
|
||||
function (response) {
|
||||
toaster.pop({
|
||||
type: 'error',
|
||||
title: authority.name,
|
||||
body: 'Update Failed! ' + response.data.message
|
||||
});
|
||||
});
|
||||
return authority.put();
|
||||
};
|
||||
|
||||
AuthorityService.getDefaults = function (authority) {
|
||||
@ -134,20 +104,7 @@ angular.module('lemur')
|
||||
};
|
||||
|
||||
AuthorityService.updateActive = function (authority) {
|
||||
return authority.put().then(
|
||||
function () {
|
||||
toaster.pop({
|
||||
type: 'success',
|
||||
title: authority.name,
|
||||
body: 'Successfully updated!'
|
||||
});
|
||||
},
|
||||
function (response) {
|
||||
toaster.pop({
|
||||
type: 'error',
|
||||
title: authority.name,
|
||||
body: 'Update Failed! ' + response.data.message
|
||||
});
|
||||
});
|
||||
return authority.put();
|
||||
};
|
||||
|
||||
});
|
||||
|
@ -2,15 +2,22 @@
|
||||
|
||||
angular.module('lemur')
|
||||
|
||||
.config(function config($routeProvider) {
|
||||
$routeProvider.when('/authorities', {
|
||||
templateUrl: '/angular/authorities/view/view.tpl.html',
|
||||
controller: 'AuthoritiesViewController'
|
||||
});
|
||||
.config(function config($stateProvider) {
|
||||
$stateProvider
|
||||
.state('authorities', {
|
||||
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) {
|
||||
$scope.filter = {};
|
||||
.controller('AuthoritiesViewController', function ($scope, $q, $modal, $stateParams, AuthorityApi, AuthorityService, ngTableParams, toaster) {
|
||||
$scope.filter = $stateParams;
|
||||
$scope.authoritiesTable = new ngTableParams({
|
||||
page: 1, // show first 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 () {
|
||||
var def = $q.defer();
|
||||
|
@ -24,7 +24,7 @@
|
||||
</td>
|
||||
<td data-title="'Active'" filter="{ 'active': 'select' }" filter-data="getAuthorityStatus()">
|
||||
<form>
|
||||
<switch ng-change="authorityService.updateActive(authority)" id="status" name="status" ng-model="authority.active" class="green small"></switch>
|
||||
<switch ng-change="updateActive(authority)" id="status" name="status" ng-model="authority.active" class="green small"></switch>
|
||||
</form>
|
||||
</td>
|
||||
<td data-title="'Roles'"> <!--filter="{ 'select': 'role' }" filter-data="roleService.getRoleDropDown()">-->
|
||||
@ -35,7 +35,8 @@
|
||||
</div>
|
||||
</td>
|
||||
<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">
|
||||
Edit
|
||||
</button>
|
||||
|
@ -1,10 +1,57 @@
|
||||
'use strict';
|
||||
|
||||
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) {
|
||||
CertificateService.getNotifications(certificate);
|
||||
CertificateService.getDestinations(certificate);
|
||||
CertificateService.getReplacements(certificate);
|
||||
$scope.certificate = certificate;
|
||||
});
|
||||
|
||||
@ -13,16 +60,31 @@ angular.module('lemur')
|
||||
};
|
||||
|
||||
$scope.save = function (certificate) {
|
||||
CertificateService.update(certificate).then(function () {
|
||||
$modalInstance.close();
|
||||
});
|
||||
CertificateService.update(certificate).then(
|
||||
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.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');
|
||||
|
||||
// set the defaults
|
||||
@ -30,10 +92,24 @@ angular.module('lemur')
|
||||
|
||||
$scope.create = function (certificate) {
|
||||
WizardHandler.wizard().context.loading = true;
|
||||
CertificateService.create(certificate).then(function () {
|
||||
WizardHandler.wizard().context.loading = false;
|
||||
$modalInstance.close();
|
||||
});
|
||||
CertificateService.create(certificate).then(
|
||||
function () {
|
||||
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 = [
|
||||
@ -95,6 +171,7 @@ angular.module('lemur')
|
||||
$scope.plugins = plugins;
|
||||
});
|
||||
|
||||
$scope.certificateService = CertificateService;
|
||||
$scope.authorityService = AuthorityService;
|
||||
$scope.destinationService = DestinationService;
|
||||
$scope.notificationService = NotificationService;
|
||||
|
@ -23,10 +23,11 @@
|
||||
Description
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea name="description" ng-model="certificate.description" placeholder="Something elegant" class="form-control" ng-pattern="/^[\w\-\s]+$/" required></textarea>
|
||||
<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>
|
||||
</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>
|
||||
</form>
|
||||
|
@ -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>
|
@ -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>
|
@ -1,84 +1,118 @@
|
||||
<form name="trackingForm" novalidate>
|
||||
<div class="form-horizontal">
|
||||
<div class="form-group"
|
||||
ng-class="{'has-error': trackingForm.ownerEmail.$invalid, 'has-success': !trackingForm.$invalid&&trackingForm.ownerEmail.$dirty}">
|
||||
<label class="control-label col-sm-2">
|
||||
Owner
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="email" name="ownerEmail" ng-model="certificate.owner" placeholder="TeamDL@example.com" tooltip="This is the certificates team distribution list or main point of contact" class="form-control" required/>
|
||||
<p ng-show="trackingForm.ownerEmail.$invalid && !trackingForm.ownerEmail.$pristine" class="help-block">You must enter an Certificate owner</p>
|
||||
</div>
|
||||
</div>
|
||||
<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="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 class="form-horizontal">
|
||||
<div class="form-group"
|
||||
ng-class="{'has-error': trackingForm.ownerEmail.$invalid, 'has-success': !trackingForm.$invalid&&trackingForm.ownerEmail.$dirty}">
|
||||
<label class="control-label col-sm-2">
|
||||
Owner
|
||||
</label>
|
||||
|
||||
<div class="col-sm-10">
|
||||
<input type="email" name="ownerEmail" ng-model="certificate.owner" placeholder="TeamDL@example.com"
|
||||
tooltip="This is the certificates team distribution list or main point of contact" class="form-control"
|
||||
required/>
|
||||
|
||||
<p ng-show="trackingForm.ownerEmail.$invalid && !trackingForm.ownerEmail.$pristine" class="help-block">You must
|
||||
enter an Certificate owner</p>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
|
@ -2,21 +2,36 @@
|
||||
|
||||
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.upload = CertificateService.upload;
|
||||
|
||||
$scope.destinationService = DestinationService;
|
||||
$scope.notificationService = NotificationService;
|
||||
$scope.certificateService = CertificateService;
|
||||
|
||||
PluginService.getByType('destination').then(function (plugins) {
|
||||
$scope.plugins = plugins;
|
||||
});
|
||||
|
||||
$scope.save = function (certificate) {
|
||||
CertificateService.upload(certificate).then(function () {
|
||||
$modalInstance.close();
|
||||
});
|
||||
CertificateService.upload(certificate).then(
|
||||
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 () {
|
||||
|
@ -80,6 +80,7 @@
|
||||
class="help-block">Enter a valid certificate.</p>
|
||||
</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>
|
||||
</form>
|
||||
|
121
lemur/static/app/angular/certificates/services.js
vendored
121
lemur/static/app/angular/certificates/services.js
vendored
@ -67,6 +67,16 @@ angular.module('lemur')
|
||||
removeDestination: function (index) {
|
||||
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) {
|
||||
this.selectedNotification = null;
|
||||
if (this.notifications === undefined) {
|
||||
@ -89,7 +99,7 @@ angular.module('lemur')
|
||||
});
|
||||
return LemurRestangular.all('certificates');
|
||||
})
|
||||
.service('CertificateService', function ($location, CertificateApi, LemurRestangular, DefaultService, toaster) {
|
||||
.service('CertificateService', function ($location, CertificateApi, AuthorityService, LemurRestangular, DefaultService) {
|
||||
var CertificateService = this;
|
||||
CertificateService.findCertificatesByName = function (filterValue) {
|
||||
return CertificateApi.getList({'filter[name]': filterValue})
|
||||
@ -100,80 +110,23 @@ angular.module('lemur')
|
||||
|
||||
CertificateService.create = function (certificate) {
|
||||
certificate.attachSubAltName();
|
||||
return CertificateApi.post(certificate).then(
|
||||
function () {
|
||||
toaster.pop({
|
||||
type: 'success',
|
||||
title: certificate.name,
|
||||
body: 'Successfully created!'
|
||||
});
|
||||
},
|
||||
function (response) {
|
||||
toaster.pop({
|
||||
type: 'error',
|
||||
title: certificate.name,
|
||||
body: 'Was not created! ' + response.data.message
|
||||
});
|
||||
}
|
||||
);
|
||||
// Help users who may have just typed in their authority
|
||||
if (!certificate.authority) {
|
||||
AuthorityService.findActiveAuthorityByName(certificate.selectedAuthority).then(function (authorities) {
|
||||
if (authorities.length > 0) {
|
||||
certificate.authority = authorities[0];
|
||||
}
|
||||
});
|
||||
}
|
||||
return CertificateApi.post(certificate);
|
||||
};
|
||||
|
||||
CertificateService.update = function (certificate) {
|
||||
return LemurRestangular.copy(certificate).put().then(
|
||||
function () {
|
||||
toaster.pop({
|
||||
type: 'success',
|
||||
title: certificate.name,
|
||||
body: 'Successfully updated!'
|
||||
});
|
||||
},
|
||||
function (response) {
|
||||
toaster.pop({
|
||||
type: 'error',
|
||||
title: certificate.name,
|
||||
body: 'Failed to update ' + response.data.message
|
||||
});
|
||||
});
|
||||
return LemurRestangular.copy(certificate).put();
|
||||
};
|
||||
|
||||
CertificateService.upload = function (certificate) {
|
||||
return CertificateApi.customPOST(certificate, 'upload').then(
|
||||
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!'
|
||||
});
|
||||
});
|
||||
return CertificateApi.customPOST(certificate, 'upload');
|
||||
};
|
||||
|
||||
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) {
|
||||
return DefaultService.get().then(function (defaults) {
|
||||
certificate.country = defaults.country;
|
||||
@ -216,22 +175,16 @@ angular.module('lemur')
|
||||
});
|
||||
};
|
||||
|
||||
CertificateService.loadPrivateKey = function (certificate) {
|
||||
return certificate.customGET('key');
|
||||
};
|
||||
|
||||
CertificateService.updateActive = function (certificate) {
|
||||
return certificate.put().then(
|
||||
function () {
|
||||
toaster.pop({
|
||||
type: 'success',
|
||||
title: certificate.name,
|
||||
body: 'Successfully updated!'
|
||||
});
|
||||
},
|
||||
function (response) {
|
||||
toaster.pop({
|
||||
type: 'error',
|
||||
title: certificate.name,
|
||||
body: 'Was not updated! ' + response.data.message
|
||||
});
|
||||
});
|
||||
return certificate.put();
|
||||
};
|
||||
|
||||
CertificateService.export = function (certificate) {
|
||||
return certificate.customPOST(certificate.exportOptions, 'export');
|
||||
};
|
||||
|
||||
return CertificateService;
|
||||
|
@ -2,15 +2,23 @@
|
||||
|
||||
angular.module('lemur')
|
||||
|
||||
.config(function config($routeProvider) {
|
||||
$routeProvider.when('/certificates', {
|
||||
templateUrl: '/angular/certificates/view/view.tpl.html',
|
||||
controller: 'CertificatesViewController'
|
||||
});
|
||||
.config(function config($stateProvider) {
|
||||
|
||||
$stateProvider
|
||||
.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) {
|
||||
$scope.filter = {};
|
||||
.controller('CertificatesViewController', function ($q, $scope, $modal, $stateParams, CertificateApi, CertificateService, MomentService, ngTableParams, toaster) {
|
||||
$scope.filter = $stateParams;
|
||||
$scope.certificateTable = new ngTableParams({
|
||||
page: 1, // show first page
|
||||
count: 10, // count per page
|
||||
@ -28,6 +36,7 @@ angular.module('lemur')
|
||||
CertificateService.getDomains(certificate);
|
||||
CertificateService.getDestinations(certificate);
|
||||
CertificateService.getNotifications(certificate);
|
||||
CertificateService.getReplacements(certificate);
|
||||
CertificateService.getAuthority(certificate);
|
||||
CertificateService.getCreator(certificate);
|
||||
});
|
||||
@ -37,15 +46,65 @@ angular.module('lemur')
|
||||
}
|
||||
});
|
||||
|
||||
$scope.certificateService = CertificateService;
|
||||
$scope.momentService = MomentService;
|
||||
|
||||
$scope.remove = function (certificate) {
|
||||
certificate.remove().then(function () {
|
||||
$scope.certificateTable.reload();
|
||||
});
|
||||
certificate.remove().then(
|
||||
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 () {
|
||||
var def = $q.defer();
|
||||
def.resolve([{'title': 'Active', 'id': true}, {'title': 'Inactive', 'id': false}]);
|
||||
@ -104,4 +163,18 @@ angular.module('lemur')
|
||||
$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;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
@ -5,12 +5,10 @@
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<div class="btn-group pull-right">
|
||||
<button data-placement="left" data-title="Create Certificate" bs-tooltip ng-click="create()"
|
||||
class="btn btn-primary">
|
||||
<button ng-click="create()" class="btn btn-primary">
|
||||
Create
|
||||
</button>
|
||||
<button data-placement="left" data-title="Import Certificate" bs-tooltip ng-click="import()"
|
||||
class="btn btn-info">
|
||||
<button ng-click="import()" class="btn btn-info">
|
||||
Import
|
||||
</button>
|
||||
</div>
|
||||
@ -26,120 +24,156 @@
|
||||
<tr ng-class="{'even-row': $even }" ng-repeat-start="certificate in $data track by $index">
|
||||
<td data-title="'Name'" sortable="'name'" filter="{ 'name': 'text' }">
|
||||
<ul class="list-unstyled">
|
||||
<li>{{ certificate.name }}</li>
|
||||
<li><span class="text-muted">{{ certificate.owner }}</span></li>
|
||||
<li>{{ ::certificate.name }}</li>
|
||||
<li><span class="text-muted">{{ ::certificate.owner }}</span></li>
|
||||
</ul>
|
||||
</td>
|
||||
<td data-title="'Active'" filter="{ 'active': 'select' }" filter-data="getCertificateStatus()">
|
||||
<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>
|
||||
</td>
|
||||
<td data-title="'Issuer'" sortable="'issuer'" filter="{ 'issuer': 'text' }">
|
||||
{{ certificate.authority.name || certificate.issuer }}
|
||||
{{ ::certificate.authority.name || certificate.issuer }}
|
||||
</td>
|
||||
<td data-title="'Common Name'" filter="{ 'cn': 'text'}">
|
||||
{{ certificate.cn }}
|
||||
<td data-title="'Domains'" filter="{ 'cn': 'text'}">
|
||||
{{ ::certificate.cn }}
|
||||
</td>
|
||||
<td data-title="''">
|
||||
<td class="col-md-2" data-title="''">
|
||||
<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>
|
||||
<button class="btn btn-sm btn-warning" ng-click="edit(certificate.id)">Edit</button>
|
||||
<a class="btn btn-sm btn-default" ui-sref="certificate({name: certificate.name})">Permalink</a>
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="warning" ng-show="certificate.toggle" ng-repeat-end>
|
||||
<td colspan="6">
|
||||
<tabset justified="true" class="col-md-6">
|
||||
<tab heading="Basic Info">
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item">
|
||||
<strong>Creator</strong>
|
||||
<span class="pull-right">
|
||||
{{ certificate.creator.email }}
|
||||
</span>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<strong>Not Before</strong>
|
||||
<span class="pull-right" tooltip="{{ certificate.notBefore }}">
|
||||
{{ momentService.createMoment(certificate.notBefore) }}
|
||||
</span>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<strong>Not After</strong>
|
||||
<span class="pull-right" tooltip="{{ certificate.notAfter }}">
|
||||
{{ momentService.createMoment(certificate.notAfter) }}
|
||||
</span>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<strong>San</strong>
|
||||
<span class="pull-right">
|
||||
<i class="glyphicon glyphicon-ok" ng-show="certificate.san"></i>
|
||||
<i class="glyphicon glyphicon-remove" ng-show="!certificate.san"></i>
|
||||
</span>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<strong>Bits</strong>
|
||||
<span class="pull-right">{{ certificate.bits }}</span>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<strong>Serial</strong>
|
||||
<span class="pull-right">{{ certificate.serial }}</span>
|
||||
</li>
|
||||
<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">
|
||||
<strong>Validity</strong>
|
||||
<span class="pull-right">
|
||||
<span ng-show="!certificate.status" class="label label-warning">Unknown</span>
|
||||
<span ng-show="certificate.status == 'revoked'" class="label label-danger">Revoked</span>
|
||||
<span ng-show="certificate.status == 'valid'" class="label label-success">Valid</span>
|
||||
</span>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<strong>Description</strong>
|
||||
<span class="pull-right">{{ certificate.description }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</tab>
|
||||
<tab heading="Notifications">
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item" ng-repeat="notification in certificate.notifications">
|
||||
<strong>{{ notification.label }}</strong>
|
||||
<span class="pull-right">{{ notification.description}}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</tab>
|
||||
<tab heading="Destinations">
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item" ng-repeat="destination in certificate.destinations">
|
||||
<strong>{{ destination.label }}</strong>
|
||||
<span class="pull-right">{{ destination.description }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</tab>
|
||||
<tab heading="Domains">
|
||||
<div class="list-group">
|
||||
<a href="#/domains/{{ domain.id }}" class="list-group-item" ng-repeat="domain in certificate.domains">{{ domain.name }}</a>
|
||||
</div>
|
||||
</tab>
|
||||
<tabset justified="true" class="col-md-6">
|
||||
<tab>
|
||||
<tab-heading>Basic Info</tab-heading>
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item">
|
||||
<strong>Creator</strong>
|
||||
<span class="pull-right">
|
||||
{{ ::certificate.creator.email }}
|
||||
</span>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<strong>Not Before</strong>
|
||||
<span class="pull-right" tooltip="{{ ::certificate.notBefore }}">
|
||||
{{ ::momentService.createMoment(certificate.notBefore) }}
|
||||
</span>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<strong>Not After</strong>
|
||||
<span class="pull-right" tooltip="{{ ::certificate.notAfter }}">
|
||||
{{ ::momentService.createMoment(certificate.notAfter) }}
|
||||
</span>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<strong>San</strong>
|
||||
<span class="pull-right">
|
||||
<i class="glyphicon glyphicon-ok" ng-show="certificate.san"></i>
|
||||
<i class="glyphicon glyphicon-remove" ng-show="!certificate.san"></i>
|
||||
</span>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<strong>Bits</strong>
|
||||
<span class="pull-right">{{ ::certificate.bits }}</span>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<strong>Signing Algorithm</strong>
|
||||
<span class="pull-right">{{ ::certificate.signingAlgorithm }}</span>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<strong>Serial</strong>
|
||||
<span class="pull-right">{{ ::certificate.serial }}</span>
|
||||
</li>
|
||||
<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">
|
||||
<strong>Validity</strong>
|
||||
<span class="pull-right">
|
||||
<span ng-show="!certificate.status" class="label label-warning">Unknown</span>
|
||||
<span ng-show="certificate.status == 'revoked'" class="label label-danger">Revoked</span>
|
||||
<span ng-show="certificate.status == 'valid'" class="label label-success">Valid</span>
|
||||
</span>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<strong>Description</strong>
|
||||
<p>{{ ::certificate.description }}</p>
|
||||
</li>
|
||||
</ul>
|
||||
</tab>
|
||||
<tab>
|
||||
<tab-heading>Notifications</tab-heading>
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item" ng-repeat="notification in certificate.notifications">
|
||||
<strong>{{ ::notification.label }}</strong>
|
||||
<span class="pull-right">{{ ::notification.description}}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</tab>
|
||||
<tab>
|
||||
<tab-heading>Destinations</tab-heading>
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item" ng-repeat="destination in certificate.destinations">
|
||||
<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 justified="true" class="col-md-6">
|
||||
<tab heading="Chain">
|
||||
<p>
|
||||
<pre style="width: 550px">{{ certificate.chain }}</pre>
|
||||
</p>
|
||||
<tab>
|
||||
<tab-heading>
|
||||
Chain
|
||||
<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 heading="Public Certificate">
|
||||
<p>
|
||||
<pre style="width: 550px">{{ certificate.body }}</pre>
|
||||
</p>
|
||||
<tab>
|
||||
<tab-heading>
|
||||
Public Certificate
|
||||
<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 ng-click="certificateService.loadPrivateKey(certificate)">
|
||||
<tab ng-click="loadPrivateKey(certificate)">
|
||||
<tab-heading>
|
||||
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>
|
||||
<p>
|
||||
<pre style="width: 550px">{{ certificate.privateKey }}</pre>
|
||||
</p>
|
||||
<pre style="width: 100%">{{ ::certificate.privateKey }}</pre>
|
||||
</tab>
|
||||
</tabset>
|
||||
</td>
|
||||
|
16
lemur/static/app/angular/dashboard/dashboard.js
vendored
16
lemur/static/app/angular/dashboard/dashboard.js
vendored
@ -1,15 +1,16 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('lemur')
|
||||
.config(function config($routeProvider) {
|
||||
$routeProvider.when('/dashboard', {
|
||||
.config(function config($stateProvider) {
|
||||
$stateProvider.state('dashboard', {
|
||||
url: '/dashboard',
|
||||
templateUrl: '/angular/dashboard/dashboard.tpl.html',
|
||||
controller: 'DashboardController'
|
||||
});
|
||||
})
|
||||
.controller('DashboardController', function ($scope, $rootScope, $filter, $location, LemurRestangular) {
|
||||
|
||||
$scope.colours = [
|
||||
$scope.colors = [
|
||||
{
|
||||
fillColor: 'rgba(41, 171, 224, 0.2)',
|
||||
strokeColor: 'rgba(41, 171, 224, 1)',
|
||||
@ -78,13 +79,18 @@ angular.module('lemur')
|
||||
$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'})
|
||||
.then(function (data) {
|
||||
$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) {
|
||||
$scope.destinations = {labels: data.items.labels, values: [data.items.values]};
|
||||
$scope.destinations = data.items;
|
||||
});
|
||||
});
|
||||
|
@ -10,43 +10,54 @@
|
||||
<h3 class="panel-title">Expiring Certificates</h3>
|
||||
</div>
|
||||
<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 class="row"></div>
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<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 class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">Issuers</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<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" colours="colours" legend="true"></canvas>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<canvas id="issuersPie" class="chart chart-pie" data="issuers.values" labels="issuers.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">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 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>
|
||||
<!-- /.row -->
|
||||
</div>
|
||||
|
@ -2,8 +2,9 @@
|
||||
|
||||
angular.module('lemur')
|
||||
|
||||
.config(function config($routeProvider) {
|
||||
$routeProvider.when('/destinations', {
|
||||
.config(function config($stateProvider) {
|
||||
$stateProvider.state('destinations', {
|
||||
url: '/destinations',
|
||||
templateUrl: '/angular/destinations/view/view.tpl.html',
|
||||
controller: 'DestinationsViewController'
|
||||
});
|
||||
|
@ -2,8 +2,9 @@
|
||||
|
||||
angular.module('lemur')
|
||||
|
||||
.config(function config($routeProvider) {
|
||||
$routeProvider.when('/domains', {
|
||||
.config(function config($stateProvider) {
|
||||
$stateProvider.state('domains', {
|
||||
url: '/domains',
|
||||
templateUrl: '/angular/domains/view/view.tpl.html',
|
||||
controller: 'DomainsViewController'
|
||||
});
|
||||
|
@ -2,8 +2,9 @@
|
||||
|
||||
angular.module('lemur')
|
||||
|
||||
.config(function config($routeProvider) {
|
||||
$routeProvider.when('/notifications', {
|
||||
.config(function config($stateProvider) {
|
||||
$stateProvider.state('notifications', {
|
||||
url: '/notifications',
|
||||
templateUrl: '/angular/notifications/view/view.tpl.html',
|
||||
controller: 'NotificationsViewController'
|
||||
});
|
||||
|
5
lemur/static/app/angular/roles/view/view.js
vendored
5
lemur/static/app/angular/roles/view/view.js
vendored
@ -2,8 +2,9 @@
|
||||
|
||||
angular.module('lemur')
|
||||
|
||||
.config(function config($routeProvider) {
|
||||
$routeProvider.when('/roles', {
|
||||
.config(function config($stateProvider) {
|
||||
$stateProvider.state('roles', {
|
||||
url: '/roles',
|
||||
templateUrl: '/angular/roles/view/view.tpl.html',
|
||||
controller: 'RolesViewController'
|
||||
});
|
||||
|
@ -2,8 +2,9 @@
|
||||
|
||||
angular.module('lemur')
|
||||
|
||||
.config(function config($routeProvider) {
|
||||
$routeProvider.when('/sources', {
|
||||
.config(function config($stateProvider) {
|
||||
$stateProvider.state('sources', {
|
||||
url: '/sources',
|
||||
templateUrl: '/angular/sources/view/view.tpl.html',
|
||||
controller: 'SourcesViewController'
|
||||
});
|
||||
|
5
lemur/static/app/angular/users/view/view.js
vendored
5
lemur/static/app/angular/users/view/view.js
vendored
@ -2,8 +2,9 @@
|
||||
|
||||
angular.module('lemur')
|
||||
|
||||
.config(function config($routeProvider) {
|
||||
$routeProvider.when('/users', {
|
||||
.config(function config($stateProvider) {
|
||||
$stateProvider.state('users', {
|
||||
url: '/users',
|
||||
templateUrl: '/angular/users/view/view.tpl.html',
|
||||
controller: 'UsersViewController'
|
||||
});
|
||||
|
@ -1,12 +1,26 @@
|
||||
<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>
|
||||
|
||||
<p>Welcome to Lemur! A central portal for all (most) of your TLS needs.</p>
|
||||
|
||||
<p><a href="/#/certificates/create" class="btn btn-primary btn-lg" role="button">Create a Certificate</a></p>
|
||||
<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>
|
||||
</div>
|
||||
<div class="row featurette">
|
||||
<div class="col-md-10">
|
||||
<h2 class="featurette-heading">TLS In The Cloud <span class="text-muted">Encrypt it all </span></h2>
|
||||
</div>
|
||||
|
||||
<div class="container marketing">
|
||||
<!-- Three columns of text below the carousel -->
|
||||
<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 »</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 »</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 »</a></p>
|
||||
</div><!-- /.col-lg-4 -->
|
||||
</div><!-- /.row -->
|
||||
</div>
|
||||
|
@ -5,7 +5,7 @@
|
||||
<div class="modal-footer">
|
||||
<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="!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>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
@ -49,45 +49,53 @@
|
||||
</div>
|
||||
<div class="navbar-collapse collapse" ng-controller="LoginController">
|
||||
<ul class="nav navbar-nav navbar-left">
|
||||
<li><a href="/#/dashboard">Dashboard</a></li>
|
||||
<li><a href="/#/certificates">Certificates</a></li>
|
||||
<li><a href="/#/authorities">Authorities</a></li>
|
||||
<li><a href="/#/notifications">Notifications</a></li>
|
||||
<li><a href="/#/destinations">Destinations</a></li>
|
||||
<li><a href="/#/sources">Sources</a></li>
|
||||
<li><a ui-sref="dashboard">Dashboard</a></li>
|
||||
<li><a ui-sref="certificates">Certificates</a></li>
|
||||
<li><a ui-sref="authorities">Authorities</a></li>
|
||||
<li><a ui-sref="notifications">Notifications</a></li>
|
||||
<li><a ui-sref="destinations">Destinations</a></li>
|
||||
<li><a ui-sref="sources">Sources</a></li>
|
||||
<li></li>
|
||||
<li class="dropdown" dropdown on-toggle="toggled(open)">
|
||||
<a href class="dropdown-toggle" dropdown-toggle>Settings <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="/#/roles">Roles</a></li>
|
||||
<li><a href="/#/users">Users</a></li>
|
||||
<li><a href="/#/domains">Domains</a></li>
|
||||
<li><a ui-sref="roles">Roles</a></li>
|
||||
<li><a ui-sref="users">Users</a></li>
|
||||
<li><a ui-sref="domains">Domains</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<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 ng-show="currentUser.username" class="nav navbar-nav navbar-right">
|
||||
<li class="dropdown" dropdown on-toggle="toggled(open)">
|
||||
<a href class="dropdown-toggle profile-nav" dropdown-toggle>
|
||||
{{ currentUser.username }}<img ng-if="currentUser.profileImage" src="{{ currentUser.profileImage }}" class="profile img-circle">
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a ng-click="logout()">Logout</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="dropdown" dropdown on-toggle="toggled(open)">
|
||||
<a href class="dropdown-toggle profile-nav" dropdown-toggle>
|
||||
<span ng-show="currentUser.profileImage">
|
||||
{{ currentUser.username }}<img src="{{ currentUser.profileImage }}" class="profile img-circle">
|
||||
</span>
|
||||
<span ng-show="!currentUser.profileImage">
|
||||
{{ currentUser.username }}<ng-letter-avatar height="35" width="35" data="currentUser.username" shape="round"></ng-letter-avatar>
|
||||
</span>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a ng-click="logout()">Logout</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Add your site or application content here -->
|
||||
<div class="container-fluid">
|
||||
<div ng-view></div>
|
||||
<div ui-view></div>
|
||||
</div>
|
||||
<footer class="footer">
|
||||
<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>
|
||||
</footer>
|
||||
</body>
|
||||
|
@ -169,3 +169,13 @@ a {
|
||||
background-color: #FFFFFF !important;
|
||||
}
|
||||
|
||||
.clipboard-btn {
|
||||
border-width: 0;
|
||||
background-color: transparent;
|
||||
color: #777;
|
||||
display: inline-block;
|
||||
top: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
|
||||
|
@ -21,7 +21,7 @@ SECRET_KEY = 'I/dVhOZNSMZMqrFJa5tWli6VQccOGudKerq3eWPMSzQNmHHVhMAQfQ=='
|
||||
|
||||
# You should consider storing these separately from your config
|
||||
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
|
||||
LEMUR_RESTRICTED_DOMAINS = []
|
||||
@ -43,7 +43,7 @@ LOG_FILE = "lemur.log"
|
||||
|
||||
# modify this if you are not using a local database
|
||||
SQLALCHEMY_DATABASE_URI = 'postgresql://lemur:lemur@localhost:5432/lemur'
|
||||
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
|
||||
# AWS
|
||||
|
||||
|
@ -41,44 +41,44 @@ def test_create_basic_csr():
|
||||
|
||||
def test_cert_get_cn():
|
||||
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():
|
||||
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 cert_get_domains(INTERNAL_VALID_SAN_CERT) == ['example2.long.com', 'example3.long.com']
|
||||
assert get_domains(INTERNAL_VALID_LONG_CERT) == []
|
||||
assert get_domains(INTERNAL_VALID_SAN_CERT) == ['example2.long.com', 'example3.long.com']
|
||||
|
||||
|
||||
def test_cert_is_san():
|
||||
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 cert_is_san(INTERNAL_VALID_SAN_CERT) == True # noqa
|
||||
assert is_san(INTERNAL_VALID_LONG_CERT) == None # noqa
|
||||
assert is_san(INTERNAL_VALID_SAN_CERT) == True # noqa
|
||||
|
||||
|
||||
def test_cert_is_wildcard():
|
||||
from lemur.tests.certs import INTERNAL_VALID_WILDCARD_CERT, INTERNAL_VALID_LONG_CERT
|
||||
from lemur.certificates.models import cert_is_wildcard
|
||||
assert cert_is_wildcard(INTERNAL_VALID_WILDCARD_CERT) == True # noqa
|
||||
assert cert_is_wildcard(INTERNAL_VALID_LONG_CERT) == None # noqa
|
||||
from lemur.certificates.models import is_wildcard
|
||||
assert is_wildcard(INTERNAL_VALID_WILDCARD_CERT) == True # noqa
|
||||
assert is_wildcard(INTERNAL_VALID_LONG_CERT) == None # noqa
|
||||
|
||||
|
||||
def test_cert_get_bitstrength():
|
||||
from lemur.tests.certs import INTERNAL_VALID_LONG_CERT
|
||||
from lemur.certificates.models import cert_get_bitstrength
|
||||
assert cert_get_bitstrength(INTERNAL_VALID_LONG_CERT) == 2048
|
||||
from lemur.certificates.models import get_bitstrength
|
||||
assert get_bitstrength(INTERNAL_VALID_LONG_CERT) == 2048
|
||||
|
||||
|
||||
def test_cert_get_issuer():
|
||||
from lemur.tests.certs import INTERNAL_VALID_LONG_CERT
|
||||
from lemur.certificates.models import cert_get_issuer
|
||||
assert cert_get_issuer(INTERNAL_VALID_LONG_CERT) == 'Example'
|
||||
from lemur.certificates.models import get_issuer
|
||||
assert get_issuer(INTERNAL_VALID_LONG_CERT) == 'Example'
|
||||
|
||||
|
||||
def test_get_name_from_arn():
|
||||
|
@ -10,7 +10,7 @@ def test_crud(session):
|
||||
role = update(role.id, 'role_new', None, [])
|
||||
assert role.name == 'role_new'
|
||||
delete(role.id)
|
||||
assert get(role.id) == None
|
||||
assert not get(role.id)
|
||||
|
||||
|
||||
def test_role_get(client):
|
||||
|
118
lemur/utils.py
118
lemur/utils.py
@ -5,17 +5,123 @@
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
import os
|
||||
import six
|
||||
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:
|
||||
"""
|
||||
|
||||
# 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:
|
||||
return current_app.config.get('LEMUR_ENCRYPTION_KEY').strip()
|
||||
except RuntimeError:
|
||||
print("No Encryption Key Found")
|
||||
return ''
|
||||
keys = current_app.config.get('LEMUR_ENCRYPTION_KEYS')
|
||||
except Exception:
|
||||
print("no encryption keys")
|
||||
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)
|
||||
|
@ -1,6 +1,5 @@
|
||||
{
|
||||
"name": "Lemur",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
66
setup.py
66
setup.py
@ -9,6 +9,7 @@ Is a TLS management and orchestration tool.
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
|
||||
import sys
|
||||
import json
|
||||
import os.path
|
||||
import datetime
|
||||
@ -23,38 +24,47 @@ from subprocess import check_output
|
||||
|
||||
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 = [
|
||||
'Flask==0.10.1',
|
||||
'Flask-RESTful==0.3.3',
|
||||
'Flask-SQLAlchemy==2.0',
|
||||
'Flask-SQLAlchemy==2.1',
|
||||
'Flask-Script==2.0.5',
|
||||
'Flask-Migrate==1.4.0',
|
||||
'Flask-Bcrypt==0.6.2',
|
||||
'Flask-Migrate==1.6.0',
|
||||
'Flask-Bcrypt==0.7.1',
|
||||
'Flask-Principal==0.4.0',
|
||||
'Flask-Mail==0.9.1',
|
||||
'SQLAlchemy-Utils==0.30.11',
|
||||
'BeautifulSoup4',
|
||||
'requests==2.7.0',
|
||||
'SQLAlchemy-Utils==0.31.3',
|
||||
'BeautifulSoup4==4.4.1',
|
||||
'requests==2.8.1',
|
||||
'psycopg2==2.6.1',
|
||||
'arrow==0.5.4',
|
||||
'arrow==0.7.0',
|
||||
'boto==2.38.0', # we might make this optional
|
||||
'six==1.9.0',
|
||||
'gunicorn==19.3.0',
|
||||
'six==1.10.0',
|
||||
'gunicorn==19.4.1',
|
||||
'pycrypto==2.6.1',
|
||||
'cryptography==1.0.1',
|
||||
'cryptography==1.1.1',
|
||||
'pyopenssl==0.15.1',
|
||||
'pyjwt==1.0.1',
|
||||
'pyjwt==1.4.0',
|
||||
'xmltodict==0.9.2',
|
||||
'lockfile==0.10.2',
|
||||
'future==0.15.0',
|
||||
'lockfile==0.12.2',
|
||||
'future==0.15.2',
|
||||
]
|
||||
|
||||
tests_require = [
|
||||
'pyflakes',
|
||||
'moto==0.4.6',
|
||||
'moto==0.4.18',
|
||||
'nose==1.3.7',
|
||||
'pytest==2.7.2',
|
||||
'pytest-flask==0.8.1'
|
||||
'pytest==2.8.3',
|
||||
'pytest-flask==0.10.0'
|
||||
]
|
||||
|
||||
docs_require = [
|
||||
@ -63,7 +73,7 @@ docs_require = [
|
||||
]
|
||||
|
||||
dev_requires = [
|
||||
'flake8>=2.0,<2.1',
|
||||
'flake8>=2.0,<3.0',
|
||||
]
|
||||
|
||||
|
||||
@ -123,13 +133,12 @@ class BuildStatic(Command):
|
||||
log.warn("Unable to build static content")
|
||||
|
||||
setup(
|
||||
name='lemur',
|
||||
version='0.1.4',
|
||||
author='Kevin Glisson',
|
||||
author_email='kglisson@netflix.com',
|
||||
url='https://github.com/netflix/lemur',
|
||||
download_url='https://github.com/Netflix/lemur/archive/0.1.3.tar.gz',
|
||||
description='Certificate management and orchestration service',
|
||||
name=about["__title__"],
|
||||
version=about["__version__"],
|
||||
author=about["__author__"],
|
||||
author_email=about["__email__"],
|
||||
url=about["__uri__"],
|
||||
description=about["__summary__"],
|
||||
long_description=open(os.path.join(ROOT, 'README.rst')).read(),
|
||||
packages=find_packages(),
|
||||
include_package_data=True,
|
||||
@ -154,6 +163,7 @@ setup(
|
||||
'aws_destination = lemur.plugins.lemur_aws.plugin:AWSDestinationPlugin',
|
||||
'aws_source = lemur.plugins.lemur_aws.plugin:AWSSourcePlugin',
|
||||
'email_notification = lemur.plugins.lemur_email.plugin:EmailNotificationPlugin',
|
||||
'java_export = lemur.plugins.lemur_java.plugin:JavaExportPlugin'
|
||||
],
|
||||
},
|
||||
classifiers=[
|
||||
@ -161,6 +171,12 @@ setup(
|
||||
'Intended Audience :: Developers',
|
||||
'Intended Audience :: System Administrators',
|
||||
'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"
|
||||
]
|
||||
)
|
||||
|
Reference in New Issue
Block a user