Compare commits

...

104 Commits
0.1.5 ... 0.2.1

Author SHA1 Message Date
b8ae8cd452 Merge pull request #190 from kevgliss/0.2.1
0.2.1 release info
2015-12-30 09:11:46 -08:00
ca82b227b9 0.2.1 release info 2015-12-30 09:11:19 -08:00
862496495f Merge pull request #189 from m4c3/patch-1
Define ACTIVE_PROVIDERS in default config
2015-12-30 08:50:05 -08:00
8bb9a8c5d1 Define ACTIVE_PROVIDERS in default config
The configuration item ACTIVE_PROVIDERS must be initialized

Workaround for this error:
2015-12-30 13:58:48,073 ERROR: Internal Error [in /www/lemur/local/lib/python2.7/site-packages/flask_restful/__init__.py:299]
Traceback (most recent call last):
  File "/www/lemur/local/lib/python2.7/site-packages/flask/app.py", line 1475, in full_dispatch_request
    rv = self.dispatch_request()
  File "/www/lemur/local/lib/python2.7/site-packages/flask/app.py", line 1461, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "/www/lemur/local/lib/python2.7/site-packages/flask_restful/__init__.py", line 462, in wrapper
    resp = resource(*args, **kwargs)
  File "/www/lemur/local/lib/python2.7/site-packages/flask/views.py", line 84, in view
    return self.dispatch_request(*args, **kwargs)
  File "/www/lemur/local/lib/python2.7/site-packages/flask_restful/__init__.py", line 572, in dispatch_request
    resp = meth(*args, **kwargs)
  File "/www/lemur/lemur/auth/views.py", line 276, in get
    for provider in current_app.config.get("ACTIVE_PROVIDERS"):
TypeError: 'NoneType' object is not iterable
2015-12-30 14:56:59 +01:00
00cb66484b Merge pull request #188 from kevgliss/csr
Adding the ability to submit a third party CSR
2015-12-29 12:11:11 -08:00
cabe2ae18d Adding the ability to issue third party created CSRs 2015-12-29 10:49:33 -08:00
665a3f3180 Merge pull request #187 from kevgliss/sso
Fixing some issues with dynamically supporting multiple SSO providers
2015-12-27 22:06:31 -05:00
3b5d7eaab6 More Linting 2015-12-27 18:08:17 -05:00
aa2358aa03 Fixing linting 2015-12-27 18:02:38 -05:00
a7decc1948 Fixing some issues with dynamically supporting multiple SSO providers 2015-12-27 17:54:11 -05:00
38b48604f3 Merge pull request #186 from rpicard/master
Add Google SSO
2015-12-27 15:30:52 -05:00
60856cb7b9 Add an endpoint to return active authentication providers
This endpoint can be used by Angular to figure out what authentication
options to display to the user. It returns a dictionary of configuration
details that the front-end needs for each provider.
2015-12-22 18:03:56 -05:00
350d013043 Add Google SSO
This pull request adds Google SSO support. There are two main changes:

1. Add the Google auth view resource
2. Make passwords optional when creating a new user. This allows an admin
to create a user without a password so that they can only login via Google.
2015-12-22 13:44:30 -05:00
70c92fea15 Merge pull request #183 from kevgliss/rotate
Adding rotate command
2015-12-18 12:05:52 -05:00
6211b126a9 Fixing py3 syntax error 2015-12-18 11:01:08 -05:00
54c3fcc72a Adding rotate command 2015-12-17 23:17:27 -05:00
27c9088ddb Merge pull request #182 from kevgliss/176-p12
Closes #176
2015-12-17 15:01:54 -08:00
b8c2d42cad Closes #176 2015-12-17 14:52:20 -08:00
1f5ddd9530 Merge pull request #181 from kevgliss/172-export
Closes #172
2015-12-17 14:35:55 -08:00
2896ce0dad Closes #172 2015-12-16 08:18:01 -08:00
29bcde145c 0.2.1 release 2015-12-14 10:42:51 -08:00
11db429bcc adding OSSMETADATA for NetflixOSS tracking 2015-12-11 15:57:28 -08:00
75aea9f885 Merge pull request #179 from rpicard/master
Update example supervisor configuration file
2015-12-10 18:31:02 -08:00
c80559005f Update example supervisor configuration file
supervisord should run as root and spawn the lemur process as the lemur
user. I also added the LEMUR_CONF environment variable because it was
not reading the configuration file in by default.
2015-12-10 17:39:49 -08:00
9b927cfcc2 Merge pull request #177 from kevgliss/docs
clarifying upgrade process
2015-12-09 17:29:13 -08:00
4db7931aa0 clarifying upgrade process 2015-12-09 17:18:01 -08:00
1e67329c64 Merge pull request #175 from kevgliss/notifications
Fixing templates
2015-12-04 09:57:42 -08:00
6d17e4d538 Fixing templates 2015-12-04 09:51:38 -08:00
350f58ec9d Merge pull request #174 from kevgliss/binding
Disabling one-time binding
2015-12-03 17:16:19 -08:00
de9478a992 Disabling one-time binding 2015-12-03 16:57:37 -08:00
70a2c985cf Merge pull request #171 from kevgliss/packaging
Fixing the startup port
2015-12-02 17:14:24 -08:00
78037dc9ec Fixing the startup port 2015-12-02 17:13:52 -08:00
9b11efd1e5 Merge pull request #170 from kevgliss/export
Adding export plugin docs
2015-12-02 16:05:36 -08:00
3c2ee8fbb3 Adding export plugin docs 2015-12-02 16:04:40 -08:00
163cc3f795 Merge pull request #169 from kevgliss/bump
Version bump
2015-12-02 14:54:17 -08:00
041382b02f Version bump 2015-12-02 14:53:46 -08:00
837bfc3aa5 Merge pull request #167 from forkd/master
minor changes in quickstart guide
2015-12-02 14:51:07 -08:00
5ba1176f14 Merge pull request #168 from kevgliss/changelog
Changelog
2015-12-02 14:50:42 -08:00
f08649b02d Updating change log 2015-12-02 14:50:14 -08:00
edbe5a254b minor changes in quickstart guide 2015-12-02 14:34:22 +00:00
cfedb30628 Merge pull request #166 from kevgliss/template
Making the notification email template cleaner
2015-12-01 18:16:54 -08:00
aa18b88a61 Making the notification email template cleaner 2015-12-01 17:13:43 -08:00
05962e71e3 Merge branch 'forkd-master' 2015-12-01 13:03:23 -08:00
bafc3d0082 minor adjustments 2015-12-01 13:03:08 -08:00
308f1b44c3 Merge branch 'master' of git://github.com/forkd/lemur into forkd-master 2015-12-01 13:01:54 -08:00
cd17789529 Removing unneeded import 2015-12-01 11:51:39 -08:00
bf988d89c4 updated quickstart guide 2015-12-01 19:03:17 +00:00
b1e842ae47 Merge pull request #162 from kevgliss/160-startup
Closes #160
2015-12-01 10:08:03 -08:00
fcc3c35ae2 Merge pull request #163 from kevgliss/docs
Updating docs
2015-12-01 10:07:37 -08:00
e2524e43cf adding exports 2015-12-01 09:44:41 -08:00
6aac2d62be Closes #160 2015-12-01 09:40:27 -08:00
95e2636f23 Updating docs 2015-12-01 09:15:53 -08:00
7565492bb9 Merge pull request #161 from kevgliss/docs
adding version.py
2015-12-01 08:34:25 -08:00
89f7f12f92 adding version.py 2015-12-01 08:33:37 -08:00
cdd15ca818 Merge pull request #159 from kevgliss/migrations
Adding current migration files.
2015-11-30 15:44:14 -08:00
11f2d88b16 Adding current migration files. 2015-11-30 15:43:38 -08:00
8066d540e0 Merge pull request #158 from kevgliss/fix
Fix
2015-11-30 14:27:23 -08:00
c3091a7346 Adding missing files. 2015-11-30 14:08:17 -08:00
9cadebcd50 adding example requests 2015-11-30 13:51:27 -08:00
3e54eb7520 adding closed 2015-11-30 12:51:28 -08:00
068f19c895 Merge pull request #156 from kevgliss/bump
adding automatic versioning
2015-11-30 12:09:33 -08:00
3651cce542 adding automatic versioning 2015-11-30 10:43:41 -08:00
9e0b9d9dda Merge pull request #154 from kevgliss/125-output-plugins
Initial work on #125
2015-11-30 10:31:25 -08:00
f194e2a1be Linting 2015-11-30 10:24:53 -08:00
f56c6f2836 Downgrading req to pass tests. 2015-11-30 10:10:50 -08:00
ec896461a7 Adding final touches to #125 2015-11-30 09:47:36 -08:00
8eeed821d3 Adding UI elements 2015-11-27 13:27:14 -08:00
80c1689b24 Merge pull request #155 from Joe8Bit/master
Fix requires.io badge
2015-11-27 10:21:33 -08:00
7c29b566be Update README.rst
Update README.rst to point to correct requires.io badge
2015-11-27 14:37:23 +00:00
920d595c12 Initial work on #125 2015-11-25 14:54:08 -08:00
5d7174b2a7 Merge pull request #153 from Netflix/requires-io-master
[requires.io] dependency update on master branch
2015-11-25 14:44:31 -08:00
3c60f47e3f [requires.io] dependency update 2015-11-25 14:18:01 -08:00
c4abc59673 [requires.io] dependency update 2015-11-25 14:18:00 -08:00
ff4cdd82ee Merge pull request #152 from kevgliss/144-search
Closes #144
2015-11-24 16:13:05 -08:00
1c6e9caa40 Closes #144 2015-11-24 16:07:44 -08:00
07ec04ddc6 Merge pull request #151 from kevgliss/122-replacement
Closes #122
2015-11-24 15:01:39 -08:00
d6b3f5af81 Closes #122 2015-11-24 14:53:22 -08:00
ce1fe9321c Merge pull request #150 from kevgliss/121-validation
121 validation
2015-11-23 16:48:40 -08:00
2c88e4e3ba fixing conflict 2015-11-23 16:42:14 -08:00
fed37c9dc0 Fixes badge 2015-11-23 16:41:31 -08:00
e14eefdc31 Added the ability to find an authority even if a user only types the name in and does not select it. 2015-11-23 16:41:31 -08:00
2525d369d4 Merge pull request #149 from kevgliss/requirements2
Updating requirements
2015-11-23 15:59:58 -08:00
0600481a67 Updating requirements 2015-11-23 15:41:11 -08:00
f0324e4755 Merge pull request #148 from kevgliss/120-error-length
Closes #120
2015-11-23 15:25:30 -08:00
00f0f957c0 Lint again 2015-11-23 15:13:18 -08:00
9c652d784d Merge pull request #143 from kevgliss/requirements
Updating requirements
2015-11-23 14:59:31 -08:00
eb2fa74661 Fixing test 2015-11-23 14:49:05 -08:00
146c599deb Lint cleanup 2015-11-23 14:47:34 -08:00
574c4033ab Closes #120 2015-11-23 14:30:23 -08:00
9f122eec18 Merge pull request #145 from kevgliss/140-permalink
Closes #140
2015-11-23 12:02:49 -08:00
eb0f6a04d8 Closes #140 2015-11-23 10:43:07 -08:00
c7230befe4 Merge pull request #142 from kevgliss/139-description
Closes #139
2015-11-23 10:27:04 -08:00
9a316ae1a9 Updating requirements 2015-11-23 10:23:23 -08:00
df4364714e Closes #139 2015-11-23 09:53:55 -08:00
a1cd2b39eb Merge pull request #135 from mikegrima/travis-tweak
Removed un-needed build step.
2015-10-29 13:34:50 -07:00
8a7a15a361 Merge pull request #132 from cloughrm/master
Use american english for consistency
2015-10-29 13:34:31 -07:00
2cdeecb6e0 Merge pull request #134 from Netflix/monkeysecurity-patch-1
Removing hyphen from in-active.
2015-10-29 13:33:49 -07:00
638e4a5ac1 Removed un-needed build step.
I hope they have size 'M'.
2015-10-29 13:02:58 -07:00
93b4ef5f17 Removing hyphen from in-active.
`inactive` is a word.  in-active is ... something else.
2015-10-29 11:54:00 -07:00
26d490f74a Merge pull request #133 from belladzaster/grammer
Fixing grammar
2015-10-28 21:53:19 -07:00
01a1190524 Fixing grammer 2015-10-28 19:55:08 -07:00
2073090628 Use american english for consistency 2015-10-28 19:39:10 -07:00
6d00cb208d Merge pull request #131 from belladzaster/master
Fixing Typos
2015-10-28 19:32:08 -07:00
13b9bf687d Fixing Typos 2015-10-28 18:24:31 -07:00
73 changed files with 2138 additions and 741 deletions

3
.gitattributes vendored
View File

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

View File

@ -23,9 +23,6 @@ env:
global:
- 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

View File

@ -1,12 +1,39 @@
Changelog
=========
0.2.0 - `master` _
~~~~~~~~~~~~~~~~~~~
0.2.2 - `master` _
~~~~~~~~~~~~~~~~~~
.. note:: This version not yet released and is under active development
0.2.1 - 2015-12-14
~~~~~~~~~~~~~~~~~~
* Fixed bug with search not refreshing values
* Cleaned up documentation, including working supervisor example (thanks rpicard!)
* Closed #165 - Fixed an issue with email templates
* Closed #188 - Added ability to submit third party CSR
* Closed #176 - Java-export should allow user to specify truststore/keystore
* Closed #176 - Extended support for exporting certificate in P12 format
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
~~~~~~~~~~~~~~~~~~~

View File

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

1
OSSMETADATA Normal file
View File

@ -0,0 +1 @@
osslifecycle=active

View File

@ -16,6 +16,10 @@ 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'

View File

@ -32,7 +32,8 @@
"angularjs-toaster": "~0.4.14",
"ngletteravatar": "~3.0.1",
"angular-ui-router": "~0.2.15",
"angular-clipboard": "~1.1.1"
"angular-clipboard": "~1.1.1",
"angular-file-saver": "~1.0.1"
},
"devDependencies": {
"angular-mocks": "~1.3",

View File

@ -109,7 +109,7 @@ Basic Configuration
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.
@ -163,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.
@ -262,11 +262,18 @@ for those plugins.
Authentication
--------------
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.
Lemur currently supports Basic Authentication, Ping OAuth2, and Google out of the box. Additional flows can be added relatively easily.
If you are not using an authentication provider you do not need to configure any of these options.
For more information about how to use social logins, see: `Satellizer <https://github.com/sahat/satellizer>`_
.. data:: ACTIVE_PROVIDERS
:noindex:
::
ACTIVE_PROVIDERS = ["ping", "google"]
.. data:: PING_SECRET
:noindex:
@ -296,6 +303,33 @@ For more information about how to use social logins, see: `Satellizer <https://g
PING_JWKS_URL = "https://<yourpingserver>/pf/JWKS"
.. data:: PING_NAME
:noindex:
::
PING_NAME = "Example Oauth2 Provider"
.. data:: PING_CLIENT_ID
:noindex:
::
PING_CLIENT_ID = "client-id"
.. data:: GOOGLE_CLIENT_ID
:noindex:
::
GOOGLE_CLIENT_ID = "client-id"
.. data:: GOOGLE_SECRET
:noindex:
::
GOOGLE_SECRET = "somethingsecret"
AWS Plugin Configuration
@ -493,6 +527,11 @@ version of Lemur from pypi and then apply any schema changes with the following
.. note:: Internally, this uses `Alembic <https://alembic.readthedocs.org/en/latest/>`_ to manage database migrations.
.. note:: By default Alembic looks for the `migrations` folder in the current working directory.
The migrations folder is located under `<LEMUR_HOME>/lemur/migrations` if you are running the lemur command from any
location besides `<LEMUR_HOME>/lemur` you will need to pass the `-d` flag to specify the absolute file path to the
`migrations` folder.
.. _CommandLineInterface:
Command Line Interface
@ -657,4 +696,4 @@ After you have the latest version of the Lemur code base you must run any needed
lemur db upgrade
This will ensure that any needed tables or columns are created or destroyed.
This will ensure that any needed tables or columns are created or destroyed.

View File

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

View File

@ -215,7 +215,7 @@ certificate Lemur does not know about and adding the certificate to it's invento
The `SourcePlugin` object has one default option of `pollRate`. This controls the number of seconds which to get new certificates.
.. warning::
Lemur currently has a very basic polling system of running a cron job every 15min to see which source plugins need to be run. A lock file is generated to guarantee that
Lemur currently has a very basic polling system of running a cron job every 15min to see which source plugins need to be run. A lock file is generated to guarantee that
only one sync is running at a time. It also means that the minimum resolution of a source plugin poll rate is effectively 15min. You can always specify a faster cron
job if you need a higher resolution sync job.
@ -227,7 +227,29 @@ The `SourcePlugin` object requires implementation of one function::
.. Note::
Often times to facilitate code re-use it makes sense put source and destination plugins into one package.
Often times to facilitate code re-use it makes sense put source and destination plugins into one package.
Export
------
Formats, formats and more formats. That's the current PKI landscape. See the always relevant `xkcd <https://xkcd.com/927/>`_.
Thankfully Lemur supports the ability to output your certificates into whatever format you want. This integration comes by the way
of Export plugins. Support is still new and evolving, the goal of these plugins is to return raw data in a new format that
can then be used by any number of applications. Included in Lemur is the `JavaExportPlugin` which currently supports generating
a Java Key Store (JKS) file for use in Java based applications.
The `ExportPlugin` object requires the implementation of one function::
def export(self, body, chain, key, options, **kwargs):
# sys.call('openssl hokuspocus')
# return "extension", passphrase, raw
.. Note::
Support of various formats sometimes relies on external tools system calls. Always be mindful of sanitizing any input to
these calls.
Testing

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

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

View File

@ -14,6 +14,11 @@ 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.

View File

@ -257,13 +257,12 @@ Create a configuration file named supervisor.ini::
nodaemon=false
minfds=1024
minprocs=200
user=lemur
[program:lemur]
command=python /path/to/lemur/manage.py manage.py start
directory=/path/to/lemur/
environment=PYTHONPATH='/path/to/lemur/'
environment=PYTHONPATH='/path/to/lemur/',LEMUR_CONF='/home/lemur/.lemur/lemur.conf.py'
user=lemur
autostart=true
autorestart=true

View File

@ -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,84 @@ 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 stored 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 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 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 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**
.. code-block:: bash
$ lemur db init
**Make note of the password used as this will be used during first login to the Lemur UI.**
.. 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 +185,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 +215,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 +246,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 its 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 +258,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 the features 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.
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.

View File

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

View File

@ -44,7 +44,7 @@ containing:
new releases, and publicly disclose the issue.
Simultaneously, the reporter of the issue will receive notification of the date
on which we plan to take the issue public.
on which we plan to make the issue public.
On the day of disclosure, we will take the following steps:
@ -59,7 +59,7 @@ 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
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.

View File

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

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

View File

@ -8,6 +8,8 @@
"""
from __future__ import absolute_import, division, print_function
from lemur import factory
from lemur.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,

View File

@ -230,5 +230,77 @@ class Ping(Resource):
return dict(token=create_token(user))
class Google(Resource):
def __init__(self):
self.reqparse = reqparse.RequestParser()
super(Google, self).__init__()
def post(self):
access_token_url = 'https://accounts.google.com/o/oauth2/token'
people_api_url = 'https://www.googleapis.com/plus/v1/people/me/openIdConnect'
self.reqparse.add_argument('clientId', type=str, required=True, location='json')
self.reqparse.add_argument('redirectUri', type=str, required=True, location='json')
self.reqparse.add_argument('code', type=str, required=True, location='json')
args = self.reqparse.parse_args()
# Step 1. Exchange authorization code for access token
payload = {
'client_id': args['clientId'],
'grant_type': 'authorization_code',
'redirect_uri': args['redirectUri'],
'code': args['code'],
'client_secret': current_app.config.get('GOOGLE_SECRET')
}
r = requests.post(access_token_url, data=payload)
token = r.json()
# Step 2. Retrieve information about the current user
headers = {'Authorization': 'Bearer {0}'.format(token['access_token'])}
r = requests.get(people_api_url, headers=headers)
profile = r.json()
user = user_service.get_by_email(profile['email'])
if user:
return dict(token=create_token(user))
class Providers(Resource):
def get(self):
active_providers = []
for provider in current_app.config.get("ACTIVE_PROVIDERS"):
provider = provider.lower()
if provider == "google":
active_providers.append({
'name': 'google',
'clientId': current_app.config.get("GOOGLE_CLIENT_ID"),
'url': api.url_for(Google)
})
elif provider == "ping":
active_providers.append({
'name': current_app.config.get("PING_NAME"),
'url': current_app.config.get('PING_REDIRECT_URI'),
'redirectUri': current_app.config.get("PING_REDIRECT_URI"),
'clientId': current_app.config.get("PING_CLIENT_ID"),
'responseType': 'code',
'scope': ['openid', 'email', 'profile', 'address'],
'scopeDelimeter': ' ',
'authorizationEndpoint': current_app.config.get("PING_AUTH_ENDPOINT"),
'requiredUrlParams': ['scope'],
'type': '2.0'
})
return active_providers
api.add_resource(Login, '/auth/login', endpoint='login')
api.add_resource(Ping, '/auth/ping', endpoint='ping')
api.add_resource(Google, '/auth/google', endpoint='google')
api.add_resource(Providers, '/auth/providers', endpoint='providers')

View File

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

View File

@ -22,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):
@ -32,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:
"""
@ -231,6 +237,11 @@ class Certificate(db.Model):
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")
@ -280,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.")

View File

@ -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
@ -121,7 +140,12 @@ def mint(issuer_options):
issuer = plugins.get(authority.plugin_name)
csr, private_key = create_csr(issuer_options)
# allow the CSR to be specified by the user
if not issuer_options.get('csr'):
csr, private_key = create_csr(issuer_options)
else:
csr = issuer_options.get('csr')
private_key = None
issuer_options['creator'] = g.user.email
cert_body, cert_chain = issuer.create_certificate(csr, issuer_options)
@ -165,6 +189,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 +219,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 +253,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 +291,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 +306,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)

View File

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

View File

@ -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,
@ -103,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__()
@ -199,6 +192,7 @@ class CertificatesList(AuthenticatedResource):
"owner": "bob@example.com",
"description": "test",
"selectedAuthority": "timetest2",
"csr",
"authority": {
"body": "-----BEGIN...",
"name": "timetest2",
@ -269,7 +263,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**:
@ -317,6 +314,7 @@ 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)
@ -328,6 +326,7 @@ class CertificatesList(AuthenticatedResource):
self.reqparse.add_argument('organizationalUnit', type=str, location='json', required=True)
self.reqparse.add_argument('owner', type=str, location='json', required=True)
self.reqparse.add_argument('commonName', type=str, location='json', required=True)
self.reqparse.add_argument('csr', type=str, location='json')
args = self.reqparse.parse_args()
@ -349,6 +348,7 @@ class CertificatesList(AuthenticatedResource):
class CertificatesUpload(AuthenticatedResource):
""" Defines the 'certificates' upload endpoint """
def __init__(self):
self.reqparse = reqparse.RequestParser()
super(CertificatesUpload, self).__init__()
@ -375,6 +375,7 @@ class CertificatesUpload(AuthenticatedResource):
"privateKey": "---Begin Private..."
"destinations": [],
"notifications": [],
"replacements": [],
"name": "cert1"
}
@ -419,8 +420,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')
@ -435,6 +437,7 @@ class CertificatesUpload(AuthenticatedResource):
class CertificatesStats(AuthenticatedResource):
""" Defines the 'certificates' stats endpoint """
def __init__(self):
self.reqparse = reqparse.RequestParser()
super(CertificatesStats, self).__init__()
@ -575,7 +578,8 @@ class Certificates(AuthenticatedResource):
"owner": "jimbob@example.com",
"active": false
"notifications": [],
"destinations": []
"destinations": [],
"replacements": []
}
**Example response**:
@ -614,6 +618,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)
@ -628,7 +633,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
@ -636,6 +642,7 @@ class Certificates(AuthenticatedResource):
class NotificationCertificatesList(AuthenticatedResource):
""" Defines the 'certificates' endpoint """
def __init__(self):
self.reqparse = reqparse.RequestParser()
super(NotificationCertificatesList, self).__init__()
@ -711,9 +718,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')

View File

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

View File

@ -90,6 +90,8 @@ LEMUR_DEFAULT_LOCATION = ''
LEMUR_DEFAULT_ORGANIZATION = ''
LEMUR_DEFAULT_ORGANIZATIONAL_UNIT = ''
# Authentication Providers
ACTIVE_PROVIDERS = []
# Logging
@ -503,11 +505,34 @@ def unicode_(data):
return data
class RotateELBs(Command):
"""
Rotates existing certificates to a new one on an ELB
"""
option_list = (
Option('-c', '--cert-name', dest='cert_name', required=True),
Option('-a', '--account-id', dest='account_id', required=True),
Option('-e', '--elb-list', dest='elb_list', required=True)
)
def run(self, cert_name, account_id, elb_list):
from lemur.plugins.lemur_aws import elb
arn = "arn:aws:iam::{0}:server-certificate/{1}".format(account_id, cert_name)
for e in open(elb_list, 'r').readlines():
for region in elb.get_all_regions():
if str(region) in e:
name = "-".join(e.split('.')[0].split('-')[:-1])
if name.startswith("internal"):
name = "-".join(name.split("-")[1:])
elb.update_listeners(account_id, str(region), name, [(443, 7001, 'https', arn)], [443])
sys.out.write("[+] Updated {0} to use {1} on 443\n".format(name, cert_name))
class ProvisionELB(Command):
"""
Creates and provisions a certificate on an ELB based on command line arguments
"""
option_list = (
Option('-d', '--dns', dest='dns', action='append', required=True, type=unicode_),
Option('-e', '--elb', dest='elb_name', required=True, type=unicode_),
@ -746,6 +771,7 @@ def main():
manager.add_command("create_user", CreateUser())
manager.add_command("create_role", CreateRole())
manager.add_command("provision_elb", ProvisionELB())
manager.add_command("rotate_elbs", RotateELBs())
manager.run()
if __name__ == "__main__":

View File

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

View File

@ -28,8 +28,8 @@ from lemur.common.utils import get_psuedo_random_string
conn = op.get_bind()
#op.drop_table('encrypted_keys')
#op.drop_table('encrypted_passwords')
op.drop_table('encrypted_keys')
op.drop_table('encrypted_passwords')
# helper tables to migrate data
temp_key_table = op.create_table('encrypted_keys',

View File

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

View File

@ -36,13 +36,16 @@ def _get_message_data(cert):
:param cert:
:return:
"""
cert_dict = cert.as_dict()
cert_dict = {}
if cert.user:
cert_dict['creator'] = cert.user.email
cert_dict['domains'] = [x .name for x in cert.domains]
cert_dict['superseded'] = list(set([x.name for x in _find_superseded(cert) if cert.name != x]))
cert_dict['not_after'] = cert.not_after
cert_dict['owner'] = cert.owner
cert_dict['name'] = cert.name
cert_dict['body'] = cert.body
return cert_dict

View File

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

View File

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

View File

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

View File

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

View File

@ -7,141 +7,161 @@
<meta name="format-detection" content="telephone=no"> <!-- disable auto telephone linking in iOS -->
<title>Lemur</title>
<style type="text/css">
/* Resets: see reset.css for details */
.ReadMsgBody { width: 100%; background-color: #ebebeb;}
.ExternalClass {width: 100%; background-color: #ebebeb;}
.ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div {line-height:100%;}
body {-webkit-text-size-adjust:none; -ms-text-size-adjust:none;}
body {margin:0; padding:0;}
table {border-spacing:0;}
table td {border-collapse:collapse;}
.yshortcuts a {border-bottom: none !important;}
/* Constrain email width for small screens */
@media screen and (max-width: 600px) {
table[class="container"] {
width: 95% !important;
}
}
/* Give content more room on mobile */
@media screen and (max-width: 480px) {
td[class="container-padding"] {
padding-left: 12px !important;
padding-right: 12px !important;
}
}
</style>
</head>
<body style="margin:0; padding:10px 0;" bgcolor="#ebebeb" leftmargin="0" topmargin="0" marginwidth="0" marginheight="0">
<br>
<!-- 100% wrapper (grey background) -->
<table border="0" width="100%" height="100%" cellpadding="50" cellspacing="0" bgcolor="#ebebeb">
<tr>
<td align="center" valign="top" bgcolor="#ebebeb" style="background-color: #ebebeb;">
<!-- 600px container (white background) -->
<table border="0" width="600" cellpadding="0" cellspacing="0" class="container" bgcolor="#ffffff">
<tr>
<td class="container-padding" bgcolor="#ffffff" style="background-color: #ffffff; padding-left: 30px; padding-right: 30px; font-size: 14px; line-height: 20px; font-family: Helvetica, sans-serif; color: #333;">
<br />
<div style="font-weight: bold; font-size: 18px; line-height: 24px; color: #202d3b">
<span style="color: #29abe0">Notice: Your TLS certificates are expiring!</span>
<hr />
</div>
<p>
Lemur, has noticed that the following certificates are expiring soon, if you rely on these certificates
you should create new certificates to replace the certificates that are expiring.
</p>
<p>
Visit https://{{ hostname }}/#/certificates/create to reissue them.
</p>
</td>
</tr>
{% for message in messages %}
<tr>
<td class="container-padding" bgcolor="#ffffff" style="background-color: #ffffff; padding-left: 30px; padding-right: 30px; font-size: 14px; line-height: 20px; font-family: Helvetica, sans-serif; color: #333;">
<hr />
<table width="540">
<tr>
<td><strong>Name</strong></td>
</tr>
<tr>
<td>{{ message.name }}</td>
<tr>
<td><strong>Owner</strong></td>
</tr>
<tr>
<td>{{ message.owner }}</td>
</tr>
<tr>
<td><strong>Creator</strong></td>
</tr>
<tr>
<td>{{ message.creator }}</td>
</tr>
<tr>
<td><strong>Description</strong></td>
</tr>
<tr>
<td>{{ message.description }}</td>
</tr>
<tr>
<td><strong>Not Before</strong></td>
</tr>
<tr>
<td>{{ message.not_before }}</td>
</tr>
<tr>
<td><strong>Not After</strong></td>
</tr>
<tr>
<td>{{ message.not_after }}</td>
</tr>
<tr>
<td><strong>Associated Domains</strong></td>
</tr>
{% if message.domains %}
{% for name in message.domains %}
<tr>
<td>{{ name }}</td>
</tr>
{% endfor %}
{% else %}
<div style="margin:0;padding:0" bgcolor="#FFFFFF">
<table width="100%" height="100%" style="min-width:348px" border="0" cellspacing="0" cellpadding="0">
<tbody>
<tr height="32px"></tr>
<tr align="center">
<td width="32px"></td>
<td>
<table border="0" cellspacing="0" cellpadding="0" style="max-width:600px">
<tbody>
<tr>
<td>
<table width="100%" border="0" cellspacing="0" cellpadding="0">
<tbody>
<tr>
<td>Unknown</td>
<td align="left" style="font-family:Roboto-Regular,Helvetica,Arial,sans-serif;font-size:35px;color:#727272;" line-height:1.5">
Lemur
</td>
</tr>
{% endif %}
<tr>
<td><strong>Potentially Superseded by</strong> (Lemur's best guess)</td>
</tr>
{% if message.superseded %}
{% for name in message.superseded %}
<tr><td>{{ name }}</td></tr>
{% endfor %}
{% else %}
<tr><td>Unknown</td></tr>
{% endif %}
<tr><td></td></tr>
</table>
</td>
</tr>
{% endfor %}
</table>
<!--/600px container -->
</td>
</tr>
<tr>
<td style="padding-top: 0px" align="center" valign="top">
<em style="font-style:italic; font-size: 12px; color: #aaa;">Lemur is broken regularly by Netflix</em>
</td>
</tr>
</table>
<!--/100% wrapper-->
<br>
<br>
</body>
</html>
</tbody>
</table>
</td>
</tr>
<tr height="16"></tr>
<tr>
<td>
<table bgcolor="#F44336" width="100%" border="0" cellspacing="0" cellpadding="0"
style="min-width:332px;max-width:600px;border:1px solid #e0e0e0;border-bottom:0;border-top-left-radius:3px;border-top-right-radius:3px">
<tbody>
<tr>
<td height="72px" colspan="3"></td>
</tr>
<tr>
<td width="32px"></td>
<td style="font-family:Roboto-Regular,Helvetica,Arial,sans-serif;font-size:24px;color:#ffffff;line-height:1.25">
Your certificate(s) are expiring!
</td>
<td width="32px"></td>
</tr>
<tr>
<td height="18px" colspan="3"></td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr>
<td>
<table bgcolor="#FAFAFA" width="100%" border="0" cellspacing="0" cellpadding="0"
style="min-width:332px;max-width:600px;border:1px solid #f0f0f0;border-bottom:1px solid #c0c0c0;border-top:0;border-bottom-left-radius:3px;border-bottom-right-radius:3px">
<tbody>
<tr height="16px">
<td width="32px" rowspan="3"></td>
<td></td>
<td width="32px" rowspan="3"></td>
</tr>
<tr>
<td>
<table style="min-width:300px" border="0" cellspacing="0" cellpadding="0">
<tbody>
<tr>
<td style="font-family:Roboto-Regular,Helvetica,Arial,sans-serif;font-size:13px;color:#202020;line-height:1.5">
Hi,
</td>
</tr>
<tr>
<td style="font-family:Roboto-Regular,Helvetica,Arial,sans-serif;font-size:13px;color:#202020;line-height:1.5">
<br>This is a Lemur certificate expiration notice. Please verify that the following certificates are no longer used.
<table border="0" cellspacing="0" cellpadding="0"
style="margin-top:48px;margin-bottom:48px">
<tbody>
{% for message in messages %}
<tr valign="middle">
<td width="32px"></td>
<td width="16px"></td>
<td style="line-height:1.2"><span
style="font-family:Roboto-Regular,Helvetica,Arial,sans-serif;font-size:20px;color:#202020">{{ message.name }}</span><br><span
style="font-family:Roboto-Regular,Helvetica,Arial,sans-serif;font-size:13px;color:#727272">{{ message.owner }}
<br>{{ message.not_after | time }}
<a href="https://{{ hostname }}/#/certificates/{{ message.name }}" target="_blank">Details</a>
</span>
</td>
</tr>
{% if not loop.last %}
<tr valign="middle">
<td width="32px" height="24px"></td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
</td>
</tr>
<tr>
<td style="font-family:Roboto-Regular,Helvetica,Arial,sans-serif;font-size:13px;color:#202020;line-height:1.5">
If the above certificates are still in use. You should re-issue and deploy new certificates as soon as possible.</span>
</td>
</tr>
<tr>
</tr>
<tr>
<td style="font-family:Roboto-Regular,Helvetica,Arial,sans-serif;font-size:13px;color:#202020;line-height:1.5">
<br>Best,<br><span class="il">Lemur</span>
</td>
</tr>
<tr height="16px"></tr>
<tr>
<td>
<table style="font-family:Roboto-Regular,Helvetica,Arial,sans-serif;font-size:12px;color:#b9b9b9;line-height:1.5">
<tbody>
<tr>
<td>*All expiration times are in UTC<br></td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr height="32px"></tr>
</tbody>
</table>
</td>
</tr>
<tr height="16"></tr>
<tr>
<td style="max-width:600px;font-family:Roboto-Regular,Helvetica,Arial,sans-serif;font-size:10px;color:#bcbcbc;line-height:1.5"></td>
</tr>
<tr>
<td>
<table style="font-family:Roboto-Regular,Helvetica,Arial,sans-serif;font-size:10px;color:#666666;line-height:18px;padding-bottom:10px">
<tbody>
<tr>
<td>You received this mandatory email service announcement to update you about
important changes to your <span class="il">TLS certificate</span>.
</td>
</tr>
<tr>
<td>
<div style="direction:ltr;text-align:left">© 2015 <span class="il">Lemur</span></div>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
<td width="32px"></td>
</tr>
<tr height="32px"></tr>
</tbody>
</table>
</div>

View File

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

View File

@ -0,0 +1,5 @@
try:
VERSION = __import__('pkg_resources') \
.get_distribution(__name__).version
except Exception as e:
VERSION = 'unknown'

View File

@ -0,0 +1,195 @@
"""
.. 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
def create_truststore(cert, chain, jks_tmp, alias, passphrase):
with mktempfile() as cert_tmp:
with open(cert_tmp, 'w') as f:
f.write(cert)
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"
])
def create_keystore(cert, jks_tmp, key, alias, passphrase):
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 cert_tmp:
with open(cert_tmp, 'w') as f:
f.write(cert)
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
run_process([
"keytool",
"-importkeystore",
"-destkeystore", jks_tmp,
"-srckeystore", p12_tmp,
"-srcstoretype", "PKCS12",
"-alias", alias,
"-srcstorepass", passphrase,
"-deststorepass", passphrase
])
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': ['Truststore (JKS)', 'Keystore (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"
type = self.get_option('type', options)
with mktemppath() as jks_tmp:
if type == 'Truststore (JKS)':
create_truststore(body, chain, jks_tmp, alias, passphrase)
elif type == 'Keystore (JKS)':
if not key:
raise Exception("Unable to export, no private key found.")
create_truststore(body, chain, jks_tmp, alias, passphrase)
create_keystore(body, jks_tmp, key, alias, passphrase)
else:
raise Exception("Unable to export, unsupported type: {0}".format(type))
with open(jks_tmp, 'rb') as f:
raw = f.read()
return "jks", passphrase, raw

View File

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

View File

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

View File

@ -0,0 +1,5 @@
try:
VERSION = __import__('pkg_resources') \
.get_distribution(__name__).version
except Exception as e:
VERSION = 'unknown'

View File

@ -0,0 +1,130 @@
"""
.. module: lemur.plugins.lemur_openssl.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_openssl as openssl
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)
current_app.logger.debug(command)
stdout, stderr = p.communicate()
if p.returncode != 0:
current_app.logger.debug(" ".join(command))
current_app.logger.error(stderr)
raise Exception(stderr)
def create_pkcs12(cert, p12_tmp, key, alias, passphrase):
"""
Creates a pkcs12 formated file.
:param cert:
:param jks_tmp:
:param key:
:param alias:
:param passphrase:
"""
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 cert_tmp:
with open(cert_tmp, 'w') as f:
f.write(cert)
run_process([
"openssl",
"pkcs12",
"-export",
"-name", alias,
"-in", cert_tmp,
"-inkey", key_tmp,
"-out", p12_tmp,
"-password", "pass:{}".format(passphrase)
])
class OpenSSLExportPlugin(ExportPlugin):
title = 'OpenSSL'
slug = 'openssl-export'
description = 'Is a loose interface to openssl and support various formats'
version = openssl.VERSION
author = 'Kevin Glisson'
author_url = 'https://github.com/netflix/lemur'
options = [
{
'name': 'type',
'type': 'select',
'required': True,
'available': ['PKCS12 (.p12)'],
'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"
type = self.get_option('type', options)
with mktemppath() as output_tmp:
if type == 'PKCS12 (.p12)':
create_pkcs12(body, output_tmp, key, alias, passphrase)
extension = "p12"
else:
raise Exception("Unable to export, unsupported type: {0}".format(type))
with open(output_tmp, 'rb') as f:
raw = f.read()
return extension, passphrase, raw

View File

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

View File

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

View File

@ -1,124 +1,158 @@
'use strict';
var lemur = angular
.module('lemur', [
'ui.router',
'ngTable',
'ngAnimate',
'chart.js',
'restangular',
'angular-loading-bar',
'ui.bootstrap',
'angular-spinkit',
'toaster',
'uiSwitch',
'mgo-angular-wizard',
'satellizer',
'ngLetterAvatar',
'angular-clipboard'
])
.config(function ($stateProvider, $urlRouterProvider, $authProvider) {
$urlRouterProvider.otherwise('/welcome');
(function() {
var lemur = angular
.module('lemur', [
'ui.router',
'ngTable',
'ngAnimate',
'chart.js',
'restangular',
'angular-loading-bar',
'ui.bootstrap',
'angular-spinkit',
'toaster',
'uiSwitch',
'mgo-angular-wizard',
'satellizer',
'ngLetterAvatar',
'angular-clipboard',
'ngFileSaver'
]);
function fetchData() {
var initInjector = angular.injector(['ng']);
var $http = initInjector.get('$http');
return $http.get('http://localhost:8000/api/1/auth/providers').then(function(response) {
lemur.constant('providers', response.data);
}, function(errorResponse) {
console.log('Could not fetch SSO providers' + errorResponse);
});
}
function bootstrapApplication() {
angular.element(document).ready(function() {
angular.bootstrap(document, ['lemur']);
});
}
fetchData().then(bootstrapApplication);
lemur.config(function ($stateProvider, $urlRouterProvider, $authProvider, providers) {
$urlRouterProvider.otherwise('/welcome');
$stateProvider
.state('welcome', {
url: '/welcome',
templateUrl: 'angular/welcome/welcome.html'
});
$authProvider.oauth2({
name: 'example',
url: 'http://localhost:5000/api/1/auth/ping',
redirectUri: 'http://localhost:3000/',
clientId: 'client-id',
responseType: 'code',
scope: ['openid', 'email', 'profile', 'address'],
scopeDelimiter: ' ',
authorizationEndpoint: 'https://example.com/as/authorization.oauth2',
requiredUrlParams: ['scope']
});
});
lemur.service('MomentService', function () {
this.diffMoment = function (start, end) {
if (end !== 'None') {
return moment(end, 'YYYY-MM-DD HH:mm Z').diff(moment(start, 'YYYY-MM-DD HH:mm Z'), 'minutes') + ' minutes';
}
return 'Unknown';
};
this.createMoment = function (date) {
if (date !== 'None') {
return moment(date, 'YYYY-MM-DD HH:mm Z').fromNow();
}
return 'Unknown';
};
});
lemur.controller('datePickerController', function ($scope, $timeout){
$scope.open = function() {
$timeout(function() {
$scope.opened = true;
});
};
});
lemur.service('DefaultService', function (LemurRestangular) {
var DefaultService = this;
DefaultService.get = function () {
return LemurRestangular.all('defaults').customGET().then(function (defaults) {
return defaults;
});
};
});
lemur.factory('LemurRestangular', function (Restangular, $location, $auth) {
return Restangular.withConfig(function (RestangularConfigurer) {
RestangularConfigurer.setBaseUrl('http://localhost:5000/api/1');
RestangularConfigurer.setDefaultHttpFields({withCredentials: true});
RestangularConfigurer.addResponseInterceptor(function (data, operation) {
var extractedData;
// .. to look for getList operations
if (operation === 'getList') {
// .. and handle the data and meta data
extractedData = data.items;
extractedData.total = data.total;
_.each(providers, function(provider) {
if ($authProvider.hasOwnProperty(provider.name)) {
$authProvider[provider.name] = provider;
} else {
extractedData = data;
$authProvider.oauth2(provider);
}
return extractedData;
});
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()) {
$location.path('/login');
return false;
}
var regExp = /\[([^)]+)\]/;
var s = 'sorting';
var f = 'filter';
var newParams = {};
for (var item in params) {
if (item.indexOf(s) > -1) {
newParams.sortBy = regExp.exec(item)[1];
newParams.sortDir = params[item];
} else if (item.indexOf(f) > -1) {
var key = regExp.exec(item)[1];
newParams.filter = key + ';' + params[item];
} else {
newParams[item] = params[item];
}
}
return { params: newParams };
});
});
});
lemur.run(['$templateCache', function ($templateCache) {
$templateCache.put('ng-table/pager.html', '<div class="ng-cloak ng-table-pager"> <div ng-if="params.settings().counts.length" class="ng-table-counts btn-group pull-left"> <button ng-repeat="count in params.settings().counts" type="button" ng-class="{\'active\':params.count()==count}" ng-click="params.count(count)" class="btn btn-default"> <span ng-bind="count"></span> </button></div><div class="pull-right"><ul style="margin: 0; padding: 0;" class="pagination ng-table-pagination"> <li ng-class="{\'disabled\': !page.active}" ng-repeat="page in pages" ng-switch="page.type"> <a ng-switch-when="prev" ng-click="params.page(page.number)" href="">&laquo;</a> <a ng-switch-when="first" ng-click="params.page(page.number)" href=""><span ng-bind="page.number"></span></a> <a ng-switch-when="page" ng-click="params.page(page.number)" href=""><span ng-bind="page.number"></span></a> <a ng-switch-when="more" ng-click="params.page(page.number)" href="">&#8230;</a> <a ng-switch-when="last" ng-click="params.page(page.number)" href=""><span ng-bind="page.number"></span></a> <a ng-switch-when="next" ng-click="params.page(page.number)" href="">&raquo;</a> </li> </ul> </div></div>');
}]);
lemur.service('MomentService', function () {
this.diffMoment = function (start, end) {
if (end !== 'None') {
return moment(end, 'YYYY-MM-DD HH:mm Z').diff(moment(start, 'YYYY-MM-DD HH:mm Z'), 'minutes') + ' minutes';
}
return 'Unknown';
};
this.createMoment = function (date) {
if (date !== 'None') {
return moment(date, 'YYYY-MM-DD HH:mm Z').fromNow();
}
return 'Unknown';
};
});
lemur.controller('datePickerController', function ($scope, $timeout){
$scope.open = function() {
$timeout(function() {
$scope.opened = true;
});
};
});
lemur.service('DefaultService', function (LemurRestangular) {
var DefaultService = this;
DefaultService.get = function () {
return LemurRestangular.all('defaults').customGET().then(function (defaults) {
return defaults;
});
};
});
lemur.factory('LemurRestangular', function (Restangular, $location, $auth) {
return Restangular.withConfig(function (RestangularConfigurer) {
RestangularConfigurer.setBaseUrl('http://localhost:8000/api/1');
RestangularConfigurer.setDefaultHttpFields({withCredentials: true});
RestangularConfigurer.addResponseInterceptor(function (data, operation) {
var extractedData;
// .. to look for getList operations
if (operation === 'getList') {
// .. and handle the data and meta data
extractedData = data.items;
extractedData.total = data.total;
} 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()) {
$location.path('/login');
return false;
}
var regExp = /\[([^)]+)\]/;
var s = 'sorting';
var f = 'filter';
var newParams = {};
for (var item in params) {
if (item.indexOf(s) > -1) {
newParams.sortBy = regExp.exec(item)[1];
newParams.sortDir = params[item];
} else if (item.indexOf(f) > -1) {
var key = regExp.exec(item)[1];
newParams.filter = key + ';' + params[item];
} else {
newParams[item] = params[item];
}
}
return { params: newParams };
});
});
});
lemur.run(['$templateCache', function ($templateCache) {
$templateCache.put('ng-table/pager.html', '<div class="ng-cloak ng-table-pager"> <div ng-if="params.settings().counts.length" class="ng-table-counts btn-group pull-left"> <button ng-repeat="count in params.settings().counts" type="button" ng-class="{\'active\':params.count()==count}" ng-click="params.count(count)" class="btn btn-default"> <span ng-bind="count"></span> </button></div><div class="pull-right"><ul style="margin: 0; padding: 0;" class="pagination ng-table-pagination"> <li ng-class="{\'disabled\': !page.active}" ng-repeat="page in pages" ng-switch="page.type"> <a ng-switch-when="prev" ng-click="params.page(page.number)" href="">&laquo;</a> <a ng-switch-when="first" ng-click="params.page(page.number)" href=""><span ng-bind="page.number"></span></a> <a ng-switch-when="page" ng-click="params.page(page.number)" href=""><span ng-bind="page.number"></span></a> <a ng-switch-when="more" ng-click="params.page(page.number)" href="">&#8230;</a> <a ng-switch-when="last" ng-click="params.page(page.number)" href=""><span ng-bind="page.number"></span></a> <a ng-switch-when="next" ng-click="params.page(page.number)" href="">&raquo;</a> </li> </ul> </div></div>');
}]);
}());

View File

@ -8,11 +8,13 @@ angular.module('lemur')
controller: 'LoginController'
});
})
.controller('LoginController', function ($rootScope, $scope, AuthenticationService, UserService) {
.controller('LoginController', function ($rootScope, $scope, AuthenticationService, UserService, providers) {
$scope.login = AuthenticationService.login;
$scope.authenticate = AuthenticationService.authenticate;
$scope.logout = AuthenticationService.logout;
$scope.providers = providers;
UserService.getCurrentUser().then(function (user) {
$scope.currentUser = user;
});

View File

@ -3,8 +3,8 @@
<div class="login">
<div class="row">
<div class="col-xs-12 col-sm-12 col-md-12">
<button class="btn btn-block btn-default" ng-click="authenticate('Example')">
Login with Example
<button class="btn btn-block btn-default" ng-repeat="(key, value) in providers" ng-click="authenticate(value.name)">
Login with {{ value.name }}
</button>
</div>
</div>

View File

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

View File

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

View File

@ -16,7 +16,7 @@ angular.module('lemur')
});
})
.controller('AuthoritiesViewController', function ($scope, $q, $modal, $stateParams, AuthorityApi, AuthorityService, ngTableParams) {
.controller('AuthoritiesViewController', function ($scope, $q, $modal, $stateParams, AuthorityApi, AuthorityService, ngTableParams, toaster) {
$scope.filter = $stateParams;
$scope.authoritiesTable = new ngTableParams({
page: 1, // show first page
@ -38,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();

View File

@ -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,10 +35,8 @@
</div>
</td>
<td data-title="''">
<a ui-sref="authority({'name': '{{ authority.name }}'})">Permalink</a>
</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>

View File

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

View File

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

View File

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

View File

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

View File

@ -1,84 +1,132 @@
<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 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 class="form-group"
ng-class="{'has-error': trackingForm.csr.$invalid&&trackingForm.csr.$dirty, 'has-success': !trackingForm.csr.$invalid&&trackingForm.csr.$dirty}">
<label class="control-label col-sm-2">
Certificate Signing Request (CSR)
</label>
<div class="col-sm-10">
<textarea tooltip="Values defined in the CSR will take precedence" name="certificate signing request" ng-model="certificate.csr"
placeholder="PEM encoded string..." class="form-control"
ng-pattern="/^-----BEGIN CERTIFICATE REQUEST-----/"></textarea>
<p ng-show="trackingForm.csr.$invalid && !trackingForm.csr.$pristine"
class="help-block">Enter a valid certificate signing request.</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>
</div>
</form>

View File

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

View File

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

View File

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

View File

@ -17,7 +17,7 @@ angular.module('lemur')
});
})
.controller('CertificatesViewController', function ($q, $scope, $modal, $stateParams, CertificateApi, CertificateService, MomentService, ngTableParams) {
.controller('CertificatesViewController', function ($q, $scope, $modal, $stateParams, CertificateApi, CertificateService, MomentService, ngTableParams, toaster) {
$scope.filter = $stateParams;
$scope.certificateTable = new ngTableParams({
page: 1, // show first page
@ -36,6 +36,7 @@ angular.module('lemur')
CertificateService.getDomains(certificate);
CertificateService.getDestinations(certificate);
CertificateService.getNotifications(certificate);
CertificateService.getReplacements(certificate);
CertificateService.getAuthority(certificate);
CertificateService.getCreator(certificate);
});
@ -45,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}]);
@ -112,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;
}
}
});
};
});

View File

@ -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>
@ -32,24 +30,25 @@
</td>
<td data-title="'Active'" filter="{ 'active': 'select' }" filter-data="getCertificateStatus()">
<form>
<switch ng-change="certificateService.updateActive(certificate)" id="status" name="status"
<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 }}
</td>
<td data-title="'Common Name'" filter="{ 'cn': 'text'}">
<td data-title="'Domains'" filter="{ 'cn': 'text'}">
{{ certificate.cn }}
</td>
<td data-title="''">
<a ui-sref="certificate({'name': '{{ certificate.name }}'})">Permalink</a>
</td>
<td data-title="''">
<td class="col-md-2" data-title="''">
<div class="btn-group pull-right">
<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>
@ -109,7 +108,7 @@
</li>
<li class="list-group-item">
<strong>Description</strong>
<span class="pull-right">{{ certificate.description }}</span>
<p>{{ certificate.description }}</p>
</li>
</ul>
</tab>
@ -138,6 +137,15 @@
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>
@ -158,7 +166,7 @@
</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"

View File

@ -10,7 +10,7 @@ angular.module('lemur')
})
.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)',

View File

@ -11,7 +11,7 @@
</div>
<div class="panel-body">
<canvas id="expiringBar" class="chart chart-bar" data="expiring.values" labels="expiring.labels"
colours="colours"></canvas>
colors="colors"></canvas>
</div>
</div>
</div>
@ -23,7 +23,7 @@
<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"></canvas>
<canvas id="issuersPie" class="chart chart-pie" data="issuers.values" labels="issuers.labels" colors="colors"></canvas>
</div>
</div>
</div>
@ -33,7 +33,7 @@
<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"></canvas>
<canvas id="bitsPie" class="chart chart-pie" data="bits.values" labels="bits.labels" colors="colors"></canvas>
</div>
</div>
</div>
@ -45,7 +45,7 @@
</div>
<div class="panel-body">
<canvas id="destinationPie" class="chart chart-pie" data="destinations.values" labels="destinations.labels"
colours="colours"></canvas>
colors="colors"></canvas>
</div>
</div>
</div>
@ -56,7 +56,7 @@
</div>
<div class="panel-body">
<canvas id="signingPie" class="chart chart-pie" data="algos.values" labels="algos.labels"
colours="colours"></canvas>
colors="colors"></canvas>
</div>
</div>
</div>

View File

@ -30,8 +30,7 @@
Password
</label>
<div class="col-sm-10">
<input type="password" name="password" ng-model="user.password" placeholder="hunter2" class="form-control" required/>
<p ng-show="createForm.password.$invalid && !createForm.password.$pristine" class="help-block">You must enter an password</p>
<input type="password" name="password" ng-model="user.password" placeholder="hunter2" class="form-control" />
</div>
</div>
<div class="form-group">

View File

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

View File

@ -32,7 +32,7 @@
<!-- endbuild -->
</head>
<body ng-app="lemur" ng-csp>
<body ng-csp>
<toaster-container></toaster-container>
<!--[if lt IE 7]>
<p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>

View File

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

View File

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

View File

@ -52,7 +52,8 @@ class User(db.Model):
:param password:
:return:
"""
return bcrypt.check_password_hash(self.password, password)
if self.password:
return bcrypt.check_password_hash(self.password, password)
def hash_password(self):
"""
@ -60,8 +61,9 @@ class User(db.Model):
:return:
"""
self.password = bcrypt.generate_password_hash(self.password)
return self.password
if self.password:
self.password = bcrypt.generate_password_hash(self.password)
return self.password
@property
def is_admin(self):

View File

@ -157,7 +157,7 @@ class UsersList(AuthenticatedResource):
"""
self.reqparse.add_argument('username', type=str, location='json', required=True)
self.reqparse.add_argument('email', type=str, location='json', required=True)
self.reqparse.add_argument('password', type=str, location='json', required=True)
self.reqparse.add_argument('password', type=str, location='json', default=None)
self.reqparse.add_argument('active', type=bool, default=True, location='json')
self.reqparse.add_argument('roles', type=roles, default=[], location='json')

View File

@ -5,11 +5,41 @@
: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
@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():
"""
@ -26,7 +56,7 @@ def get_keys():
# the fact that there is not a current_app with a config at that point
try:
keys = current_app.config.get('LEMUR_ENCRYPTION_KEYS')
except:
except Exception:
print("no encryption keys")
return []

View File

@ -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.2',
'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,9 @@ docs_require = [
]
dev_requires = [
'flake8>=2.0,<2.1',
'flake8>=2.0,<3.0',
'invoke',
'twine'
]
@ -123,13 +135,12 @@ class BuildStatic(Command):
log.warn("Unable to build static content")
setup(
name='lemur',
version='0.1.5',
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 +165,8 @@ 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'
'openssl_export = lemur.plugins.lemur_openssl.plugin:OpenSSLExportPlugin'
],
},
classifiers=[
@ -161,6 +174,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"
]
)