Compare commits
83 Commits
Author | SHA1 | Date | |
---|---|---|---|
1c3c70d460 | |||
e8e7bdf9e0 | |||
d263e0e60c | |||
028d86c0bb | |||
f8b6830013 | |||
49a40c50e8 | |||
2ba48995fe | |||
3cc8ade6d8 | |||
39c9a0a299 | |||
3ad317fb6d | |||
bd46440d12 | |||
f3a28814ae | |||
9f8f64b9ec | |||
1e524a49c0 | |||
467c276fca | |||
f610e39418 | |||
27d977b2fa | |||
b36e72bfcc | |||
e49701228d | |||
48f8b33d7d | |||
d87ace8c89 | |||
b1326d4145 | |||
7c2862c958 | |||
0a4f5ad64d | |||
c617a11c55 | |||
053167965a | |||
a7ac45b937 | |||
5482bbf4bd | |||
0a58e106b5 | |||
a1395a5808 | |||
a0d50ef03a | |||
685e2c8b6d | |||
c6d9a20fe5 | |||
4a952d867b | |||
cb4cf43fcf | |||
1bce7a832b | |||
574234f70f | |||
42e5470dd0 | |||
8199365324 | |||
86c92eb31e | |||
d9fd952c03 | |||
967c7ded8d | |||
a4bf847b56 | |||
d6917155e8 | |||
3f024c1ef4 | |||
96d253f0f9 | |||
9b166fb9a9 | |||
b8ae8cd452 | |||
ca82b227b9 | |||
862496495f | |||
8bb9a8c5d1 | |||
00cb66484b | |||
cabe2ae18d | |||
665a3f3180 | |||
3b5d7eaab6 | |||
aa2358aa03 | |||
a7decc1948 | |||
38b48604f3 | |||
60856cb7b9 | |||
350d013043 | |||
70c92fea15 | |||
6211b126a9 | |||
54c3fcc72a | |||
27c9088ddb | |||
b8c2d42cad | |||
1f5ddd9530 | |||
2896ce0dad | |||
29bcde145c | |||
11db429bcc | |||
75aea9f885 | |||
c80559005f | |||
9b927cfcc2 | |||
4db7931aa0 | |||
1e67329c64 | |||
6d17e4d538 | |||
350f58ec9d | |||
de9478a992 | |||
70a2c985cf | |||
78037dc9ec | |||
9b11efd1e5 | |||
3c2ee8fbb3 | |||
163cc3f795 | |||
041382b02f |
@ -2,10 +2,31 @@ Changelog
|
||||
=========
|
||||
|
||||
|
||||
0.2.1 - `master` _
|
||||
0.2.2 - 2016-02-05
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. note:: This version not yet released and is under active development
|
||||
* Closed [#234](https://github.com/Netflix/lemur/issues/234) - Allows export plugins to define whether they need
|
||||
private key material (default is True)
|
||||
* Closed [#231](https://github.com/Netflix/lemur/issues/231) - Authorities were not respecting 'owning' roles and their
|
||||
users
|
||||
* Closed [#228](https://github.com/Netflix/lemur/issues/228) - Fixed documentation with correct filter values
|
||||
* Closed [#226](https://github.com/Netflix/lemur/issues/226) - Fixes issue were `import_certificate` was requiring
|
||||
replacement certificates to be specified
|
||||
* Closed [#224](https://github.com/Netflix/lemur/issues/224) - Fixed an issue where NPM might not be globally available (thanks AlexClineBB!)
|
||||
* Closed [#221](https://github.com/Netflix/lemur/issues/234) - Fixes several reported issues where older migration scripts were
|
||||
missing tables, this change removes pre 0.2 migration scripts
|
||||
* Closed [#218](https://github.com/Netflix/lemur/issues/234) - Fixed an issue where export passphrases would not validate
|
||||
|
||||
|
||||
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
|
||||
|
1
LICENSE
1
LICENSE
@ -1,4 +1,3 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
1
OSSMETADATA
Normal file
1
OSSMETADATA
Normal file
@ -0,0 +1 @@
|
||||
osslifecycle=active
|
@ -24,7 +24,6 @@ Basic Configuration
|
||||
|
||||
LOG_FILE = "/logs/lemur/lemur-test.log"
|
||||
|
||||
|
||||
.. data:: debug
|
||||
:noindex:
|
||||
|
||||
@ -34,7 +33,6 @@ Basic Configuration
|
||||
|
||||
debug = False
|
||||
|
||||
|
||||
.. warning::
|
||||
This should never be used in a production environment as it exposes Lemur to
|
||||
remote code execution through the debug console.
|
||||
@ -173,47 +171,123 @@ Lemur supports sending certification expiration notifications through SES and SM
|
||||
.. data:: LEMUR_EMAIL_SENDER
|
||||
:noindex:
|
||||
|
||||
Specifies which service will be delivering notification emails. Valid values are `SMTP` or `SES`
|
||||
Specifies which service will be delivering notification emails. Valid values are `SMTP` or `SES`
|
||||
|
||||
.. note::
|
||||
If using STMP as your provider you will need to define additional configuration options as specified by Flask-Mail.
|
||||
See: `Flask-Mail <https://pythonhosted.org/Flask-Mail>`_
|
||||
.. note::
|
||||
If using SMP as your provider you will need to define additional configuration options as specified by Flask-Mail.
|
||||
See: `Flask-Mail <https://pythonhosted.org/Flask-Mail>`_
|
||||
|
||||
If you are using SES the email specified by the `LEMUR_MAIL` configuration will need to be verified by AWS before
|
||||
you can send any mail. See: `Verifying Email Address in Amazon SES <http://docs.aws.amazon.com/ses/latest/DeveloperGuide/verify-email-addresses.html>`_
|
||||
|
||||
If you are using SES the email specified by the `LEMUR_MAIL` configuration will need to be verified by AWS before
|
||||
you can send any mail. See: `Verifying Email Address in Amazon SES <http://docs.aws.amazon.com/ses/latest/DeveloperGuide/verify-email-addresses.html>`_
|
||||
|
||||
.. data:: LEMUR_MAIL
|
||||
:noindex:
|
||||
|
||||
Lemur sender's email
|
||||
Lemur sender's email
|
||||
|
||||
::
|
||||
|
||||
LEMUR_MAIL = 'lemur.example.com'
|
||||
LEMUR_MAIL = 'lemur.example.com'
|
||||
|
||||
|
||||
.. data:: LEMUR_SECURITY_TEAM_EMAIL
|
||||
:noindex:
|
||||
|
||||
This is an email or list of emails that should be notified when a certificate is expiring. It is also the contact email address for any discovered certificate.
|
||||
This is an email or list of emails that should be notified when a certificate is expiring. It is also the contact email address for any discovered certificate.
|
||||
|
||||
::
|
||||
|
||||
LEMUR_SECURITY_TEAM_EMAIL = ['security@example.com']
|
||||
LEMUR_SECURITY_TEAM_EMAIL = ['security@example.com']
|
||||
|
||||
|
||||
.. data:: LEMUR_DEFAULT_EXPIRATION_NOTIFICATION_INTERVALS
|
||||
:noindex:
|
||||
|
||||
Lemur notification intervals
|
||||
Lemur notification intervals
|
||||
|
||||
::
|
||||
|
||||
LEMUR_DEFAULT_EXPIRATION_NOTIFICATION_INTERVALS = [30, 15, 2]
|
||||
LEMUR_DEFAULT_EXPIRATION_NOTIFICATION_INTERVALS = [30, 15, 2]
|
||||
|
||||
|
||||
Authority Options
|
||||
-----------------
|
||||
Authentication 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:
|
||||
|
||||
::
|
||||
|
||||
PING_SECRET = 'somethingsecret'
|
||||
|
||||
.. data:: PING_ACCESS_TOKEN_URL
|
||||
:noindex:
|
||||
|
||||
::
|
||||
|
||||
PING_ACCESS_TOKEN_URL = "https://<yourpingserver>/as/token.oauth2"
|
||||
|
||||
|
||||
.. data:: PING_USER_API_URL
|
||||
:noindex:
|
||||
|
||||
::
|
||||
|
||||
PING_USER_API_URL = "https://<yourpingserver>/idp/userinfo.openid"
|
||||
|
||||
.. data:: PING_JWKS_URL
|
||||
:noindex:
|
||||
|
||||
::
|
||||
|
||||
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"
|
||||
|
||||
|
||||
Plugin Specific Options
|
||||
-----------------------
|
||||
|
||||
Verisign Issuer Plugin
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Authorities will each have their own configuration options. There is currently just one plugin bundled with Lemur,
|
||||
Verisign/Symantec. Additional plugins may define additional options. Refer to the plugin's own documentation
|
||||
@ -260,53 +334,16 @@ for those plugins.
|
||||
This is the root to be used for your CA chain
|
||||
|
||||
|
||||
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.
|
||||
|
||||
For more information about how to use social logins, see: `Satellizer <https://github.com/sahat/satellizer>`_
|
||||
|
||||
.. data:: PING_SECRET
|
||||
:noindex:
|
||||
|
||||
::
|
||||
|
||||
PING_SECRET = 'somethingsecret'
|
||||
|
||||
.. data:: PING_ACCESS_TOKEN_URL
|
||||
:noindex:
|
||||
|
||||
::
|
||||
|
||||
PING_ACCESS_TOKEN_URL = "https://<yourpingserver>/as/token.oauth2"
|
||||
|
||||
|
||||
.. data:: PING_USER_API_URL
|
||||
:noindex:
|
||||
|
||||
::
|
||||
|
||||
PING_USER_API_URL = "https://<yourpingserver>/idp/userinfo.openid"
|
||||
|
||||
.. data:: PING_JWKS_URL
|
||||
:noindex:
|
||||
|
||||
::
|
||||
|
||||
PING_JWKS_URL = "https://<yourpingserver>/pf/JWKS"
|
||||
|
||||
|
||||
|
||||
AWS Plugin Configuration
|
||||
========================
|
||||
AWS Source/Destination Plugin
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
In order for Lemur to manage its own account and other accounts we must ensure it has the correct AWS permissions.
|
||||
|
||||
.. note:: AWS usage is completely optional. Lemur can upload, find and manage TLS certificates in AWS. But is not required to do so.
|
||||
|
||||
Setting up IAM roles
|
||||
--------------------
|
||||
""""""""""""""""""""
|
||||
|
||||
Lemur's AWS plugin uses boto heavily to talk to all the AWS resources it manages. By default it uses the on-instance credentials to make the necessary calls.
|
||||
|
||||
@ -411,7 +448,8 @@ IAM-ServerCertificate
|
||||
|
||||
|
||||
Setting up STS access
|
||||
---------------------
|
||||
"""""""""""""""""""""
|
||||
|
||||
Once we have setup our accounts we need to ensure that we create a trust relationship so that LemurInstanceProfile can assume the Lemur role.
|
||||
|
||||
In the AWS console select the Lemur IAM role and select the Trust Relationships tab and click Edit Trust Relationship
|
||||
@ -438,7 +476,7 @@ Below is an example policy:
|
||||
|
||||
|
||||
Adding N+1 accounts
|
||||
-------------------
|
||||
"""""""""""""""""""
|
||||
|
||||
To add another account we go to the new account and create a new Lemur IAM role with the same policy as above.
|
||||
|
||||
@ -466,7 +504,7 @@ An example policy:
|
||||
}
|
||||
|
||||
Setting up SES
|
||||
--------------
|
||||
""""""""""""""
|
||||
|
||||
Lemur has built in support for sending it's certificate notifications via Amazon's simple email service (SES). To force
|
||||
Lemur to use SES ensure you are the running as the IAM role defined above and that you have followed the steps outlined
|
||||
@ -481,18 +519,6 @@ Will be the sender of all notifications, so ensure that it is verified with AWS.
|
||||
SES if the default notification gateway and will be used unless SMTP settings are configured in the application configuration
|
||||
settings.
|
||||
|
||||
Upgrading Lemur
|
||||
===============
|
||||
|
||||
Lemur provides an easy way to upgrade between versions. Simply download the newest
|
||||
version of Lemur from pypi and then apply any schema changes with the following command.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ lemur db upgrade
|
||||
|
||||
.. note:: Internally, this uses `Alembic <https://alembic.readthedocs.org/en/latest/>`_ to manage database migrations.
|
||||
|
||||
.. _CommandLineInterface:
|
||||
|
||||
Command Line Interface
|
||||
@ -602,32 +628,6 @@ and to get help on sub-commands
|
||||
lemur certificates --help
|
||||
|
||||
|
||||
Identity and Access Management
|
||||
==============================
|
||||
|
||||
Lemur uses a Role Based Access Control (RBAC) mechanism to control which users have access to which resources. When a
|
||||
user is first created in Lemur they can be assigned one or more roles. These roles are typically dynamically created
|
||||
depending on a external identity provider (Google, LDAP, etc.,) or are hardcoded within Lemur and associated with special
|
||||
meaning.
|
||||
|
||||
Within Lemur there are three main permissions: AdminPermission, CreatorPermission, OwnerPermission. Sub-permissions such
|
||||
as ViewPrivateKeyPermission are compositions of these three main Permissions.
|
||||
|
||||
Lets take a look at how these permissions are used:
|
||||
|
||||
Each `Authority` has a set of roles associated with it. If a user is also associated with the same roles
|
||||
that the `Authority` is associated with, Lemur allows that user to user/view/update that `Authority`.
|
||||
|
||||
This RBAC is also used when determining which users can access which certificate private key. Lemur's current permission
|
||||
structure is setup such that if the user is a `Creator` or `Owner` of a given certificate they are allow to view that
|
||||
private key. Owners can also be a role name, such that any user with the same role as owner will be allowed to view the
|
||||
private key information.
|
||||
|
||||
These permissions are applied to the user upon login and refreshed on every request.
|
||||
|
||||
.. seealso::
|
||||
`Flask-Principal <https://pythonhosted.org/Flask-Principal>`_
|
||||
|
||||
|
||||
Upgrading Lemur
|
||||
===============
|
||||
@ -658,3 +658,61 @@ After you have the latest version of the Lemur code base you must run any needed
|
||||
|
||||
|
||||
This will ensure that any needed tables or columns are created or destroyed.
|
||||
|
||||
.. 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.
|
||||
|
||||
Plugins
|
||||
=======
|
||||
|
||||
There are several interfaces currently available to extend Lemur. These are a work in
|
||||
progress and the API is not frozen.
|
||||
|
||||
Bundled Plugins
|
||||
---------------
|
||||
|
||||
Lemur includes several plugins by default. Including extensive support for AWS, VeriSign/Symantec and CloudCA services.
|
||||
|
||||
3rd Party Extensions
|
||||
--------------------
|
||||
|
||||
The following extensions are available and maintained by members of the Lemur community:
|
||||
|
||||
Have an extension that should be listed here? Submit a `pull request <https://github.com/netflix/lemur>`_ and we'll
|
||||
get it added.
|
||||
|
||||
Want to create your own extension? See :doc:`../developer/plugins/index` to get started.
|
||||
|
||||
|
||||
Identity and Access Management
|
||||
==============================
|
||||
|
||||
Lemur uses a Role Based Access Control (RBAC) mechanism to control which users have access to which resources. When a
|
||||
user is first created in Lemur they can be assigned one or more roles. These roles are typically dynamically created
|
||||
depending on a external identity provider (Google, LDAP, etc.,) or are hardcoded within Lemur and associated with special
|
||||
meaning.
|
||||
|
||||
Within Lemur there are three main permissions: AdminPermission, CreatorPermission, OwnerPermission. Sub-permissions such
|
||||
as ViewPrivateKeyPermission are compositions of these three main Permissions.
|
||||
|
||||
Lets take a look at how these permissions are used:
|
||||
|
||||
Each `Authority` has a set of roles associated with it. If a user is also associated with the same roles
|
||||
that the `Authority` is associated with, Lemur allows that user to user/view/update that `Authority`.
|
||||
|
||||
This RBAC is also used when determining which users can access which certificate private key. Lemur's current permission
|
||||
structure is setup such that if the user is a `Creator` or `Owner` of a given certificate they are allow to view that
|
||||
private key. Owners can also be a role name, such that any user with the same role as owner will be allowed to view the
|
||||
private key information.
|
||||
|
||||
These permissions are applied to the user upon login and refreshed on every request.
|
||||
|
||||
.. seealso::
|
||||
|
||||
`Flask-Principal <https://pythonhosted.org/Flask-Principal>`_
|
||||
|
10
docs/conf.py
10
docs/conf.py
@ -101,9 +101,13 @@ pygments_style = 'sphinx'
|
||||
|
||||
# -- Options for HTML output ----------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
html_theme = 'default'
|
||||
# on_rtd is whether we are on readthedocs.org, this line of code grabbed from docs.readthedocs.org
|
||||
on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
|
||||
|
||||
if not on_rtd: # only import and set the theme if we're building docs locally
|
||||
import sphinx_rtd_theme
|
||||
html_theme = 'sphinx_rtd_theme'
|
||||
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
|
@ -180,19 +180,92 @@ You can see a list of open pull requests (pending changes) by visiting https://g
|
||||
|
||||
Pull requests should be against **master** and pass all TravisCI checks
|
||||
|
||||
Plugins
|
||||
=======
|
||||
|
||||
Writing a Plugin
|
||||
================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:maxdepth: 2
|
||||
|
||||
plugins/index
|
||||
|
||||
|
||||
REST API
|
||||
========
|
||||
|
||||
Lemur's front end is entirely API driven. Any action that you can accomplish via the UI can also be accomplished by the
|
||||
UI. The following is documents and provides examples on how to make requests to the Lemur API.
|
||||
|
||||
Authentication
|
||||
--------------
|
||||
|
||||
.. automodule:: lemur.auth.views
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Destinations
|
||||
------------
|
||||
|
||||
.. automodule:: lemur.destinations.views
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Notifications
|
||||
-------------
|
||||
|
||||
.. automodule:: lemur.notifications.views
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Users
|
||||
-----
|
||||
|
||||
.. automodule:: lemur.users.views
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Roles
|
||||
-----
|
||||
|
||||
.. automodule:: lemur.roles.views
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Certificates
|
||||
------------
|
||||
|
||||
.. automodule:: lemur.certificates.views
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Authorities
|
||||
-----------
|
||||
|
||||
.. automodule:: lemur.authorities.views
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Domains
|
||||
-------
|
||||
|
||||
.. automodule:: lemur.domains.views
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
Internals
|
||||
=========
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:maxdepth: 2
|
||||
|
||||
internals/lemur
|
||||
|
||||
|
@ -27,6 +27,5 @@ Subpackages
|
||||
lemur.plugins.base
|
||||
lemur.plugins.bases
|
||||
lemur.plugins.lemur_aws
|
||||
lemur.plugins.lemur_cloudca
|
||||
lemur.plugins.lemur_email
|
||||
lemur.plugins.lemur_verisign
|
||||
|
@ -96,5 +96,4 @@ Subpackages
|
||||
lemur.notifications
|
||||
lemur.plugins
|
||||
lemur.roles
|
||||
lemur.status
|
||||
lemur.users
|
||||
|
@ -1,11 +0,0 @@
|
||||
status Package
|
||||
==============
|
||||
|
||||
:mod:`views` Module
|
||||
-------------------
|
||||
|
||||
.. automodule:: lemur.status.views
|
||||
:noindex:
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
@ -1,6 +1,3 @@
|
||||
Writing a Plugin
|
||||
================
|
||||
|
||||
Several interfaces exist for extending Lemur:
|
||||
|
||||
* Issuer (lemur.plugins.base.issuer)
|
||||
@ -215,7 +212,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 +224,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
|
||||
|
@ -1,66 +0,0 @@
|
||||
Lemur's front end is entirely API driven. Any action that you can accomplish via the UI can also be accomplished by the
|
||||
UI. The following is documents and provides examples on how to make requests to the Lemur API.
|
||||
|
||||
Authentication
|
||||
--------------
|
||||
|
||||
.. automodule:: lemur.auth.views
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Destinations
|
||||
------------
|
||||
|
||||
.. automodule:: lemur.destinations.views
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Notifications
|
||||
-------------
|
||||
|
||||
.. automodule:: lemur.notifications.views
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Users
|
||||
-----
|
||||
|
||||
.. automodule:: lemur.users.views
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Roles
|
||||
-----
|
||||
|
||||
.. automodule:: lemur.roles.views
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Certificates
|
||||
------------
|
||||
|
||||
.. automodule:: lemur.certificates.views
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Authorities
|
||||
-----------
|
||||
|
||||
.. automodule:: lemur.authorities.views
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Domains
|
||||
-------
|
||||
|
||||
.. automodule:: lemur.domains.views
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
@ -27,8 +27,7 @@ Administration
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
administration/index
|
||||
plugins/index
|
||||
administration
|
||||
|
||||
Developers
|
||||
----------
|
||||
@ -38,14 +37,21 @@ Developers
|
||||
|
||||
developer/index
|
||||
|
||||
|
||||
REST API
|
||||
Security
|
||||
--------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
developer/rest
|
||||
security
|
||||
|
||||
Doing a Release
|
||||
---------------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
doing-a-release
|
||||
|
||||
FAQ
|
||||
----
|
||||
|
@ -1,20 +0,0 @@
|
||||
Plugins
|
||||
=======
|
||||
|
||||
There are several interfaces currently available to extend Lemur. These are a work in
|
||||
progress and the API is not frozen.
|
||||
|
||||
Bundled Plugins
|
||||
---------------
|
||||
|
||||
Lemur includes several plugins by default. Including extensive support for AWS, VeriSign/Symantec and CloudCA services.
|
||||
|
||||
3rd Party Extensions
|
||||
--------------------
|
||||
|
||||
The following extensions are available and maintained by members of the Lemur community:
|
||||
|
||||
Have an extension that should be listed here? Submit a `pull request <https://github.com/netflix/lemur>`_ and we'll
|
||||
get it added.
|
||||
|
||||
Want to create your own extension? See :doc:`../developer/plugins/index` to get started.
|
@ -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
|
||||
|
@ -158,7 +158,7 @@ Additional notifications can be created through the UI or API. See :ref:`Creati
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ lemur db init
|
||||
$ cd /www/lemur/lemur
|
||||
$ 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.
|
||||
@ -220,7 +220,7 @@ you can pass that via the ``--config`` option.
|
||||
# the correct host and port!
|
||||
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:8000/``.
|
||||
|
||||
|
||||
Running Lemur as a Service
|
||||
|
@ -8,20 +8,20 @@ Flask==0.10.1
|
||||
Flask-RESTful==0.3.3
|
||||
Flask-SQLAlchemy==2.1
|
||||
Flask-Script==2.0.5
|
||||
Flask-Migrate==1.6.0
|
||||
Flask-Migrate==1.7.0
|
||||
Flask-Bcrypt==0.7.1
|
||||
Flask-Principal==0.4.0
|
||||
Flask-Mail==0.9.1
|
||||
SQLAlchemy-Utils==0.31.3
|
||||
SQLAlchemy-Utils==0.31.4
|
||||
BeautifulSoup4
|
||||
requests==2.8.1
|
||||
requests==2.9.1
|
||||
psycopg2==2.6.1
|
||||
arrow==0.7.0
|
||||
boto==2.38.0 # we might make this optional
|
||||
six==1.10.0
|
||||
gunicorn==19.4.1
|
||||
gunicorn==19.4.4
|
||||
pycrypto==2.6.1
|
||||
cryptography==1.1.1
|
||||
cryptography==1.1.2
|
||||
pyopenssl==0.15.1
|
||||
pyjwt==1.4.0
|
||||
xmltodict==0.9.2
|
||||
|
@ -9,7 +9,7 @@ __title__ = "lemur"
|
||||
__summary__ = ("Certificate management and orchestration service")
|
||||
__uri__ = "https://github.com/Netflix/lemur"
|
||||
|
||||
__version__ = "0.2"
|
||||
__version__ = "0.2.2"
|
||||
|
||||
__author__ = "The Lemur developers"
|
||||
__email__ = "security@netflix.com"
|
||||
|
@ -19,6 +19,11 @@ CertificateCreator = namedtuple('certificate', ['method', 'value'])
|
||||
CertificateCreatorNeed = partial(CertificateCreator, 'key')
|
||||
|
||||
|
||||
class SensitiveDomainPermission(Permission):
|
||||
def __init__(self):
|
||||
super(SensitiveDomainPermission, self).__init__(RoleNeed('admin'))
|
||||
|
||||
|
||||
class ViewKeyPermission(Permission):
|
||||
def __init__(self, certificate_id, owner):
|
||||
c_need = CertificateCreatorNeed(certificate_id)
|
||||
|
@ -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'],
|
||||
'scopeDelimiter': ' ',
|
||||
'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')
|
||||
|
@ -28,7 +28,6 @@ def update(authority_id, description=None, owner=None, active=None, roles=None):
|
||||
|
||||
:param authority_id:
|
||||
:param roles: roles that are allowed to use this authority
|
||||
:rtype : Authority
|
||||
:return:
|
||||
"""
|
||||
authority = get(authority_id)
|
||||
@ -47,7 +46,6 @@ def create(kwargs):
|
||||
"""
|
||||
Create a new authority.
|
||||
|
||||
:rtype : Authority
|
||||
:return:
|
||||
"""
|
||||
|
||||
@ -103,6 +101,10 @@ def create(kwargs):
|
||||
database.update(cert)
|
||||
authority = database.create(authority)
|
||||
|
||||
# the owning dl or role should have this authority associated with it
|
||||
owner_role = role_service.get_by_name(kwargs['ownerEmail'])
|
||||
owner_role.authority = authority
|
||||
|
||||
g.current_user.authorities.append(authority)
|
||||
|
||||
return authority
|
||||
@ -123,7 +125,6 @@ def get(authority_id):
|
||||
"""
|
||||
Retrieves an authority given it's ID
|
||||
|
||||
:rtype : Authority
|
||||
:param authority_id:
|
||||
:return:
|
||||
"""
|
||||
@ -135,7 +136,6 @@ def get_by_name(authority_name):
|
||||
Retrieves an authority given it's name.
|
||||
|
||||
:param authority_name:
|
||||
:rtype : Authority
|
||||
:return:
|
||||
"""
|
||||
return database.get(Authority, authority_name, field='name')
|
||||
|
@ -85,9 +85,9 @@ class AuthoritiesList(AuthenticatedResource):
|
||||
|
||||
:query sortBy: field to sort on
|
||||
:query sortDir: acs or desc
|
||||
:query page: int. default is 1
|
||||
:query filter: key value pair. format is k=v;
|
||||
:query limit: limit number. default is 10
|
||||
:query page: int default is 1
|
||||
:query filter: key value pair. format is k;v
|
||||
:query limit: limit number default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
|
@ -140,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)
|
||||
@ -184,7 +189,10 @@ 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'])
|
||||
|
||||
if kwargs.get('replacements'):
|
||||
database.update_list(cert, 'replaces', Certificate, kwargs['replacements'])
|
||||
|
||||
cert.notifications = notifications
|
||||
|
||||
cert = database.create(cert)
|
||||
|
@ -7,16 +7,26 @@
|
||||
"""
|
||||
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.plugins import plugins
|
||||
|
||||
from lemur.auth.service import AuthenticatedResource
|
||||
from lemur.auth.permissions import ViewKeyPermission
|
||||
from lemur.auth.permissions import AuthorityPermission
|
||||
from lemur.auth.permissions import UpdateCertificatePermission
|
||||
from lemur.auth.permissions import SensitiveDomainPermission
|
||||
|
||||
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.domains import service as domain_service
|
||||
from lemur.common.utils import marshal_items, paginated_parser
|
||||
from lemur.notifications.views import notification_list
|
||||
|
||||
@ -63,6 +73,36 @@ def valid_authority(authority_options):
|
||||
return authority
|
||||
|
||||
|
||||
def get_domains_from_options(options):
|
||||
"""
|
||||
Retrive all domains from certificate options
|
||||
:param options:
|
||||
:return:
|
||||
"""
|
||||
domains = [options['commonName']]
|
||||
if options.get('extensions'):
|
||||
if options['extensions'].get('subAltNames'):
|
||||
for k, v in options['extensions']['subAltNames']['names']:
|
||||
if k == 'DNSName':
|
||||
domains.append(v)
|
||||
return domains
|
||||
|
||||
|
||||
def check_sensitive_domains(domains):
|
||||
"""
|
||||
Determines if any certificates in the given certificate
|
||||
are marked as sensitive
|
||||
:param domains:
|
||||
:return:
|
||||
"""
|
||||
for domain in domains:
|
||||
domain_objs = domain_service.get_by_name(domain)
|
||||
for d in domain_objs:
|
||||
if d.sensitive:
|
||||
raise ValueError("The domain {0} has been marked as sensitive. Contact an administrator to "
|
||||
"issue this certificate".format(d.name))
|
||||
|
||||
|
||||
def pem_str(value, name):
|
||||
"""
|
||||
Used to validate that the given string is a PEM formatted string
|
||||
@ -150,7 +190,7 @@ class CertificatesList(AuthenticatedResource):
|
||||
:query sortBy: field to sort on
|
||||
:query sortDir: acs or desc
|
||||
:query page: int. default is 1
|
||||
:query filter: key value pair. format is k=v;
|
||||
:query filter: key value pair format is k;v
|
||||
:query limit: limit number. default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
@ -192,6 +232,7 @@ class CertificatesList(AuthenticatedResource):
|
||||
"owner": "bob@example.com",
|
||||
"description": "test",
|
||||
"selectedAuthority": "timetest2",
|
||||
"csr",
|
||||
"authority": {
|
||||
"body": "-----BEGIN...",
|
||||
"name": "timetest2",
|
||||
@ -325,6 +366,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()
|
||||
|
||||
@ -336,9 +378,12 @@ class CertificatesList(AuthenticatedResource):
|
||||
|
||||
# allow "owner" roles by team DL
|
||||
roles.append(role)
|
||||
permission = AuthorityPermission(authority.id, roles)
|
||||
authority_permission = AuthorityPermission(authority.id, roles)
|
||||
|
||||
if permission.can():
|
||||
if authority_permission.can():
|
||||
# if we are not admins lets make sure we aren't issuing anything sensitive
|
||||
if not SensitiveDomainPermission().can():
|
||||
check_sensitive_domains(get_domains_from_options(args))
|
||||
return service.create(**args)
|
||||
|
||||
return dict(message="You are not authorized to use {0}".format(args['authority'].name)), 403
|
||||
@ -695,9 +740,9 @@ class NotificationCertificatesList(AuthenticatedResource):
|
||||
|
||||
:query sortBy: field to sort on
|
||||
:query sortDir: acs or desc
|
||||
:query page: int. default is 1
|
||||
:query filter: key value pair. format is k=v;
|
||||
:query limit: limit number. default is 10
|
||||
:query page: int default is 1
|
||||
:query filter: key value pair format is k;v
|
||||
:query limit: limit number default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
@ -849,12 +894,17 @@ class CertificateExport(AuthenticatedResource):
|
||||
|
||||
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))
|
||||
plugin = plugins.get(args['export']['plugin']['slug'])
|
||||
if plugin.requires_key:
|
||||
if permission.can():
|
||||
extension, passphrase, data = plugin.export(cert.body, cert.chain, cert.private_key, args['export']['plugin']['pluginOptions'])
|
||||
else:
|
||||
return dict(message='You are not authorized to export this certificate'), 403
|
||||
else:
|
||||
extension, passphrase, data = plugin.export(cert.body, cert.chain, cert.private_key, args['export']['plugin']['pluginOptions'])
|
||||
|
||||
return dict(message='You are not authorized to export this certificate'), 403
|
||||
# we take a hit in message size when b64 encoding
|
||||
return dict(extension=extension, passphrase=passphrase, data=base64.b64encode(data))
|
||||
|
||||
|
||||
api.add_resource(CertificatesList, '/certificates', endpoint='certificates')
|
||||
|
@ -11,8 +11,10 @@
|
||||
"""
|
||||
from sqlalchemy import exc
|
||||
from sqlalchemy.sql import and_, or_
|
||||
from sqlalchemy.orm import make_transient
|
||||
from sqlalchemy.orm.exc import NoResultFound
|
||||
|
||||
|
||||
from lemur.extensions import db
|
||||
from lemur.exceptions import AttrNotFound, DuplicateError
|
||||
|
||||
@ -254,6 +256,18 @@ def update_list(model, model_attr, item_model, items):
|
||||
return model
|
||||
|
||||
|
||||
def clone(model):
|
||||
"""
|
||||
Clones the given model and removes it's primary key
|
||||
:param model:
|
||||
:return:
|
||||
"""
|
||||
db.session.expunge(model)
|
||||
make_transient(model)
|
||||
model.id = None
|
||||
return model
|
||||
|
||||
|
||||
def sort_and_page(query, model, args):
|
||||
"""
|
||||
Helper that allows us to combine sorting and paging
|
||||
|
@ -82,8 +82,8 @@ class DestinationsList(AuthenticatedResource):
|
||||
:query sortBy: field to sort on
|
||||
:query sortDir: acs or desc
|
||||
:query page: int. default is 1
|
||||
:query filter: key value pair. format is k=v;
|
||||
:query limit: limit number. default is 10
|
||||
:query filter: key value pair format is k;v
|
||||
:query limit: limit number default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
"""
|
||||
@ -341,9 +341,9 @@ class CertificateDestinations(AuthenticatedResource):
|
||||
|
||||
:query sortBy: field to sort on
|
||||
:query sortDir: acs or desc
|
||||
:query page: int. default is 1
|
||||
:query filter: key value pair. format is k=v;
|
||||
:query limit: limit number. default is 10
|
||||
:query page: int default is 1
|
||||
:query filter: key value pair format is k;v
|
||||
:query limit: limit number default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
"""
|
||||
|
@ -7,7 +7,7 @@
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
|
||||
"""
|
||||
from sqlalchemy import Column, Integer, String
|
||||
from sqlalchemy import Column, Integer, String, Boolean
|
||||
|
||||
from lemur.database import db
|
||||
|
||||
@ -16,11 +16,4 @@ class Domain(db.Model):
|
||||
__tablename__ = 'domains'
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String(256))
|
||||
|
||||
def as_dict(self):
|
||||
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
|
||||
|
||||
def serialize(self):
|
||||
blob = self.as_dict()
|
||||
blob['certificates'] = [x.id for x in self.certificate]
|
||||
return blob
|
||||
sensitive = Column(Boolean, default=False)
|
||||
|
@ -32,6 +32,43 @@ def get_all():
|
||||
return database.find_all(query, Domain, {}).all()
|
||||
|
||||
|
||||
def get_by_name(name):
|
||||
"""
|
||||
Fetches domain by it's name
|
||||
|
||||
:param name:
|
||||
:return:
|
||||
"""
|
||||
return database.get_all(Domain, name, field="name").all()
|
||||
|
||||
|
||||
def create(name, sensitive):
|
||||
"""
|
||||
Create a new domain
|
||||
|
||||
:param name:
|
||||
:param sensitive:
|
||||
:return:
|
||||
"""
|
||||
domain = Domain(name=name, sensitive=sensitive)
|
||||
return database.create(domain)
|
||||
|
||||
|
||||
def update(domain_id, name, sensitive):
|
||||
"""
|
||||
Update an existing domain
|
||||
|
||||
:param domain_id:
|
||||
:param name:
|
||||
:param sensitive:
|
||||
:return:
|
||||
"""
|
||||
domain = get(domain_id)
|
||||
domain.name = name
|
||||
domain.sensitive = sensitive
|
||||
database.update(domain)
|
||||
|
||||
|
||||
def render(args):
|
||||
"""
|
||||
Helper to parse REST Api requests
|
||||
|
@ -12,12 +12,14 @@ from flask.ext.restful import reqparse, Api, fields
|
||||
|
||||
from lemur.domains import service
|
||||
from lemur.auth.service import AuthenticatedResource
|
||||
from lemur.auth.permissions import SensitiveDomainPermission
|
||||
|
||||
from lemur.common.utils import paginated_parser, marshal_items
|
||||
|
||||
FIELDS = {
|
||||
'id': fields.Integer,
|
||||
'name': fields.String
|
||||
'name': fields.String,
|
||||
'sensitive': fields.Boolean
|
||||
}
|
||||
|
||||
mod = Blueprint('domains', __name__)
|
||||
@ -57,10 +59,12 @@ class DomainsList(AuthenticatedResource):
|
||||
{
|
||||
"id": 1,
|
||||
"name": "www.example.com",
|
||||
"sensitive": false
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "www.example2.com",
|
||||
"sensitive": false
|
||||
}
|
||||
]
|
||||
"total": 2
|
||||
@ -68,8 +72,8 @@ class DomainsList(AuthenticatedResource):
|
||||
|
||||
:query sortBy: field to sort on
|
||||
:query sortDir: acs or desc
|
||||
:query page: int. default is 1
|
||||
:query filter: key value pair. format is k=v;
|
||||
:query page: int default is 1
|
||||
:query filter: key value pair format is k;v
|
||||
:query limit: limit number. default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
@ -79,6 +83,54 @@ class DomainsList(AuthenticatedResource):
|
||||
args = parser.parse_args()
|
||||
return service.render(args)
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
def post(self):
|
||||
"""
|
||||
.. http:post:: /domains
|
||||
|
||||
The current domain list
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /domains HTTP/1.1
|
||||
Host: example.com
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
{
|
||||
"name": "www.example.com",
|
||||
"sensitive": false
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"name": "www.example.com",
|
||||
"sensitive": false
|
||||
}
|
||||
|
||||
:query sortBy: field to sort on
|
||||
:query sortDir: acs or desc
|
||||
:query page: int default is 1
|
||||
:query filter: key value pair format is k;v
|
||||
:query limit: limit number default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
"""
|
||||
self.reqparse.add_argument('name', type=str, location='json')
|
||||
self.reqparse.add_argument('sensitive', type=bool, default=False, location='json')
|
||||
args = self.reqparse.parse_args()
|
||||
return service.create(args['name'], args['sensitive'])
|
||||
|
||||
|
||||
class Domains(AuthenticatedResource):
|
||||
def __init__(self):
|
||||
@ -111,6 +163,7 @@ class Domains(AuthenticatedResource):
|
||||
{
|
||||
"id": 1,
|
||||
"name": "www.example.com",
|
||||
"sensitive": false
|
||||
}
|
||||
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
@ -119,6 +172,53 @@ class Domains(AuthenticatedResource):
|
||||
"""
|
||||
return service.get(domain_id)
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
def put(self, domain_id):
|
||||
"""
|
||||
.. http:get:: /domains/1
|
||||
|
||||
update one domain
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /domains HTTP/1.1
|
||||
Host: example.com
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
{
|
||||
"name": "www.example.com",
|
||||
"sensitive": false
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"name": "www.example.com",
|
||||
"sensitive": false
|
||||
}
|
||||
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
"""
|
||||
self.reqparse.add_argument('name', type=str, location='json')
|
||||
self.reqparse.add_argument('sensitive', type=bool, default=False, location='json')
|
||||
args = self.reqparse.parse_args()
|
||||
|
||||
if SensitiveDomainPermission().can():
|
||||
return service.update(domain_id, args['name'], args['sensitive'])
|
||||
|
||||
return dict(message='You are not authorized to modify this domain'), 403
|
||||
|
||||
|
||||
class CertificateDomains(AuthenticatedResource):
|
||||
""" Defines the 'domains' endpoint """
|
||||
@ -153,10 +253,12 @@ class CertificateDomains(AuthenticatedResource):
|
||||
{
|
||||
"id": 1,
|
||||
"name": "www.example.com",
|
||||
"sensitive": false
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "www.example2.com",
|
||||
"sensitive": false
|
||||
}
|
||||
]
|
||||
"total": 2
|
||||
@ -164,9 +266,9 @@ class CertificateDomains(AuthenticatedResource):
|
||||
|
||||
:query sortBy: field to sort on
|
||||
:query sortDir: acs or desc
|
||||
:query page: int. default is 1
|
||||
:query filter: key value pair. format is k=v;
|
||||
:query limit: limit number. default is 10
|
||||
:query page: int default is 1
|
||||
:query filter: key value pair format is k;v
|
||||
:query limit: limit number default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
|
129
lemur/manage.py
129
lemur/manage.py
@ -4,6 +4,7 @@ import os
|
||||
import sys
|
||||
import base64
|
||||
import time
|
||||
import arrow
|
||||
import requests
|
||||
import json
|
||||
from gunicorn.config import make_settings
|
||||
@ -24,7 +25,11 @@ from lemur.certificates import service as cert_service
|
||||
from lemur.sources import service as source_service
|
||||
from lemur.notifications import service as notification_service
|
||||
|
||||
from lemur.certificates.models import get_name_from_arn
|
||||
from lemur.certificates.verify import verify_string
|
||||
|
||||
from lemur.plugins.lemur_aws import elb
|
||||
|
||||
from lemur.sources.service import sync
|
||||
|
||||
from lemur import create_app
|
||||
@ -90,6 +95,8 @@ LEMUR_DEFAULT_LOCATION = ''
|
||||
LEMUR_DEFAULT_ORGANIZATION = ''
|
||||
LEMUR_DEFAULT_ORGANIZATIONAL_UNIT = ''
|
||||
|
||||
# Authentication Providers
|
||||
ACTIVE_PROVIDERS = []
|
||||
|
||||
# Logging
|
||||
|
||||
@ -503,11 +510,77 @@ def unicode_(data):
|
||||
return data
|
||||
|
||||
|
||||
class RotateELBs(Command):
|
||||
"""
|
||||
Rotates existing certificates to a new one on an ELB
|
||||
"""
|
||||
option_list = (
|
||||
Option('-e', '--elb-list', dest='elb_list', required=True),
|
||||
Option('-p', '--chain-path', dest='chain_path'),
|
||||
Option('-c', '--cert-name', dest='cert_name'),
|
||||
Option('-a', '--cert-prefix', dest='cert_prefix'),
|
||||
Option('-d', '--description', dest='description')
|
||||
)
|
||||
|
||||
def run(self, elb_list, chain_path, cert_name, cert_prefix, description):
|
||||
|
||||
for e in open(elb_list, 'r').readlines():
|
||||
elb_name, account_id, region, from_port, to_port, protocol = e.strip().split(',')
|
||||
|
||||
if cert_name:
|
||||
arn = "arn:aws:iam::{0}:server-certificate/{1}".format(account_id, cert_name)
|
||||
|
||||
else:
|
||||
# if no cert name is provided we need to discover it
|
||||
listeners = elb.get_listeners(account_id, region, elb_name)
|
||||
|
||||
# get the listener we care about
|
||||
for listener in listeners:
|
||||
if listener[0] == int(from_port) and listener[1] == int(to_port):
|
||||
arn = listener[4]
|
||||
name = get_name_from_arn(arn)
|
||||
certificate = cert_service.get_by_name(name)
|
||||
break
|
||||
else:
|
||||
sys.stdout.write("[-] Could not find ELB {0}".format(elb_name))
|
||||
continue
|
||||
|
||||
if not certificate:
|
||||
sys.stdout.write("[-] Could not find certificate {0} in Lemur".format(name))
|
||||
continue
|
||||
|
||||
dests = []
|
||||
for d in certificate.destinations:
|
||||
dests.append({'id': d.id})
|
||||
|
||||
nots = []
|
||||
for n in certificate.notifications:
|
||||
nots.append({'id': n.id})
|
||||
|
||||
new_certificate = database.clone(certificate)
|
||||
|
||||
if cert_prefix:
|
||||
new_certificate.name = "{0}-{1}".format(cert_prefix, new_certificate.name)
|
||||
|
||||
new_certificate.chain = open(chain_path, 'r').read()
|
||||
new_certificate.description = "{0} - {1}".format(new_certificate.description, description)
|
||||
|
||||
new_certificate = database.create(new_certificate)
|
||||
database.update_list(new_certificate, 'destinations', Destination, dests)
|
||||
database.update_list(new_certificate, 'notifications', Notification, nots)
|
||||
database.update(new_certificate)
|
||||
|
||||
arn = new_certificate.get_arn(account_id)
|
||||
|
||||
elb.update_listeners(account_id, region, elb_name, [(from_port, to_port, protocol, arn)], [from_port])
|
||||
|
||||
sys.stdout.write("[+] Updated {0} to use {1}\n".format(elb_name, new_certificate.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_),
|
||||
@ -718,22 +791,46 @@ def publish_verisign_units():
|
||||
requests.post('http://localhost:8078/metrics', data=json.dumps(metric))
|
||||
|
||||
|
||||
@manager.command
|
||||
def backfill_signing_algo():
|
||||
class Rolling(Command):
|
||||
"""
|
||||
Will attempt to backfill the signing_algorithm column
|
||||
Rotates existing certificates to a new one on an ELB
|
||||
"""
|
||||
option_list = (
|
||||
Option('-w', '--window', dest='window', default=24),
|
||||
)
|
||||
|
||||
:return:
|
||||
"""
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from lemur.certificates.models import get_signing_algorithm
|
||||
for c in cert_service.get_all_certs():
|
||||
cert = x509.load_pem_x509_certificate(str(c.body), default_backend())
|
||||
c.signing_algorithm = get_signing_algorithm(cert)
|
||||
c.signing_algorithm
|
||||
database.update(c)
|
||||
print(c.signing_algorithm)
|
||||
def run(self, window):
|
||||
"""
|
||||
Simple function that queries verisign for API units and posts the mertics to
|
||||
Atlas API for other teams to consume.
|
||||
:return:
|
||||
"""
|
||||
end = arrow.utcnow()
|
||||
start = end.replace(hours=-window)
|
||||
items = Certificate.query.filter(Certificate.not_before <= end.format('YYYY-MM-DD')) \
|
||||
.filter(Certificate.not_before >= start.format('YYYY-MM-DD')).all()
|
||||
|
||||
metrics = {}
|
||||
for i in items:
|
||||
name = "{0},{1}".format(i.owner, i.issuer)
|
||||
if metrics.get(name):
|
||||
metrics[name] += 1
|
||||
else:
|
||||
metrics[name] = 1
|
||||
|
||||
for name, value in metrics.iteritems():
|
||||
owner, issuer = name.split(",")
|
||||
metric = [
|
||||
{
|
||||
"timestamp": 1321351651,
|
||||
"type": "GAUGE",
|
||||
"name": "Issued Certificates",
|
||||
"tags": {"owner": owner, "issuer": issuer, "window": window},
|
||||
"value": value
|
||||
}
|
||||
]
|
||||
|
||||
requests.post('http://localhost:8078/metrics', data=json.dumps(metric))
|
||||
|
||||
|
||||
def main():
|
||||
@ -746,6 +843,8 @@ 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.add_command("rolling", Rolling())
|
||||
manager.run()
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
@ -1,42 +0,0 @@
|
||||
"""Adding in models for certificate sources
|
||||
|
||||
Revision ID: 1ff763f5b80b
|
||||
Revises: 4dc5ddd111b8
|
||||
Create Date: 2015-08-01 15:24:20.412725
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '1ff763f5b80b'
|
||||
down_revision = '4dc5ddd111b8'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
import sqlalchemy_utils
|
||||
|
||||
|
||||
def upgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('sources',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('label', sa.String(length=32), nullable=True),
|
||||
sa.Column('options', sqlalchemy_utils.types.json.JSONType(), nullable=True),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('plugin_name', sa.String(length=32), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('certificate_source_associations',
|
||||
sa.Column('source_id', sa.Integer(), nullable=True),
|
||||
sa.Column('certificate_id', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['certificate_id'], ['certificates.id'], ondelete='cascade'),
|
||||
sa.ForeignKeyConstraint(['source_id'], ['destinations.id'], ondelete='cascade')
|
||||
)
|
||||
### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('certificate_source_associations')
|
||||
op.drop_table('sources')
|
||||
### end Alembic commands ###
|
@ -8,7 +8,7 @@ Create Date: 2015-11-30 15:40:19.827272
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '33de094da890'
|
||||
down_revision = 'ed422fc58ba'
|
||||
down_revision = None
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
@ -1,49 +0,0 @@
|
||||
"""Refactors Accounts to Destinations
|
||||
|
||||
Revision ID: 3b718f59b8ce
|
||||
Revises: None
|
||||
Create Date: 2015-07-09 17:44:55.626221
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '3b718f59b8ce'
|
||||
down_revision = None
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('certificate_account_associations')
|
||||
op.drop_table('accounts')
|
||||
op.add_column('destinations', sa.Column('plugin_name', sa.String(length=32), nullable=True))
|
||||
op.drop_index('ix_elbs_account_id', table_name='elbs')
|
||||
op.drop_constraint(u'elbs_account_id_fkey', 'elbs', type_='foreignkey')
|
||||
op.drop_column('elbs', 'account_id')
|
||||
### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('elbs', sa.Column('account_id', sa.BIGINT(), autoincrement=False, nullable=True))
|
||||
op.create_foreign_key(u'elbs_account_id_fkey', 'elbs', 'accounts', ['account_id'], ['id'])
|
||||
op.create_index('ix_elbs_account_id', 'elbs', ['account_id'], unique=False)
|
||||
op.drop_column('destinations', 'plugin_name')
|
||||
op.create_table('accounts',
|
||||
sa.Column('id', sa.INTEGER(), server_default=sa.text(u"nextval('accounts_id_seq'::regclass)"), nullable=False),
|
||||
sa.Column('account_number', sa.VARCHAR(length=32), autoincrement=False, nullable=True),
|
||||
sa.Column('label', sa.VARCHAR(length=32), autoincrement=False, nullable=True),
|
||||
sa.Column('notes', sa.TEXT(), autoincrement=False, nullable=True),
|
||||
sa.PrimaryKeyConstraint('id', name=u'accounts_pkey'),
|
||||
sa.UniqueConstraint('account_number', name=u'accounts_account_number_key'),
|
||||
postgresql_ignore_search_path=False
|
||||
)
|
||||
op.create_table('certificate_account_associations',
|
||||
sa.Column('account_id', sa.INTEGER(), autoincrement=False, nullable=True),
|
||||
sa.Column('certificate_id', sa.INTEGER(), autoincrement=False, nullable=True),
|
||||
sa.ForeignKeyConstraint(['account_id'], [u'accounts.id'], name=u'certificate_account_associations_account_id_fkey', ondelete=u'CASCADE'),
|
||||
sa.ForeignKeyConstraint(['certificate_id'], [u'certificates.id'], name=u'certificate_account_associations_certificate_id_fkey', ondelete=u'CASCADE')
|
||||
)
|
||||
### end Alembic commands ###
|
@ -1,26 +0,0 @@
|
||||
"""Adding certificate signing algorithm
|
||||
|
||||
Revision ID: 4bcfa2c36623
|
||||
Revises: 1ff763f5b80b
|
||||
Create Date: 2015-10-06 10:03:47.993204
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '4bcfa2c36623'
|
||||
down_revision = '1ff763f5b80b'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('certificates', sa.Column('signing_algorithm', sa.String(length=128), nullable=True))
|
||||
### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('certificates', 'signing_algorithm')
|
||||
### end Alembic commands ###
|
26
lemur/migrations/versions/4c50b903d1ae_.py
Normal file
26
lemur/migrations/versions/4c50b903d1ae_.py
Normal file
@ -0,0 +1,26 @@
|
||||
"""Adding ability to mark domains as 'sensitive'
|
||||
|
||||
Revision ID: 4c50b903d1ae
|
||||
Revises: 33de094da890
|
||||
Create Date: 2015-12-30 10:19:30.057791
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '4c50b903d1ae'
|
||||
down_revision = '33de094da890'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
def upgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('domains', sa.Column('sensitive', sa.Boolean(), nullable=True))
|
||||
### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('domains', 'sensitive')
|
||||
### end Alembic commands ###
|
@ -1,41 +0,0 @@
|
||||
"""Adding notifications
|
||||
|
||||
Revision ID: 4c8915e461b3
|
||||
Revises: 3b718f59b8ce
|
||||
Create Date: 2015-07-24 14:34:57.316273
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '4c8915e461b3'
|
||||
down_revision = '3b718f59b8ce'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
import sqlalchemy_utils
|
||||
|
||||
|
||||
def upgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('notifications',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('label', sa.String(length=128), nullable=True),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('options', sqlalchemy_utils.types.json.JSONType(), nullable=True),
|
||||
sa.Column('active', sa.Boolean(), nullable=True),
|
||||
sa.Column('plugin_name', sa.String(length=32), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.drop_column(u'certificates', 'challenge')
|
||||
op.drop_column(u'certificates', 'csr_config')
|
||||
### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column(u'certificates', sa.Column('csr_config', sa.TEXT(), autoincrement=False, nullable=True))
|
||||
op.add_column(u'certificates', sa.Column('challenge', postgresql.BYTEA(), autoincrement=False, nullable=True))
|
||||
op.drop_table('notifications')
|
||||
### end Alembic commands ###
|
@ -1,31 +0,0 @@
|
||||
"""Creating a one-to-many relationship for notifications
|
||||
|
||||
Revision ID: 4dc5ddd111b8
|
||||
Revises: 4c8915e461b3
|
||||
Create Date: 2015-07-24 15:02:04.398262
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '4dc5ddd111b8'
|
||||
down_revision = '4c8915e461b3'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('certificate_notification_associations',
|
||||
sa.Column('notification_id', sa.Integer(), nullable=True),
|
||||
sa.Column('certificate_id', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['certificate_id'], ['certificates.id'], ondelete='cascade'),
|
||||
sa.ForeignKeyConstraint(['notification_id'], ['notifications.id'], ondelete='cascade')
|
||||
)
|
||||
### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('certificate_notification_associations')
|
||||
### end Alembic commands ###
|
@ -1,255 +0,0 @@
|
||||
"""Migrates the private key encrypted column from AES to fernet encryption scheme.
|
||||
|
||||
Revision ID: ed422fc58ba
|
||||
Revises: 4bcfa2c36623
|
||||
Create Date: 2015-10-23 09:19:28.654126
|
||||
|
||||
"""
|
||||
import base64
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'ed422fc58ba'
|
||||
down_revision = '4bcfa2c36623'
|
||||
import six
|
||||
|
||||
from StringIO import StringIO
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.sql import text
|
||||
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||
from cryptography.fernet import Fernet, MultiFernet
|
||||
|
||||
from flask import current_app
|
||||
from lemur.common.utils import get_psuedo_random_string
|
||||
|
||||
conn = op.get_bind()
|
||||
|
||||
op.drop_table('encrypted_keys')
|
||||
op.drop_table('encrypted_passwords')
|
||||
|
||||
# helper tables to migrate data
|
||||
temp_key_table = op.create_table('encrypted_keys',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('aes', sa.Binary()),
|
||||
sa.Column('fernet', sa.Binary()),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
|
||||
# helper table to migrate data
|
||||
temp_password_table = op.create_table('encrypted_passwords',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('aes', sa.Binary()),
|
||||
sa.Column('fernet', sa.Binary()),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
|
||||
|
||||
# From http://sqlalchemy-utils.readthedocs.org/en/latest/_modules/sqlalchemy_utils/types/encrypted.html#EncryptedType
|
||||
# for migration purposes only
|
||||
class EncryptionDecryptionBaseEngine(object):
|
||||
"""A base encryption and decryption engine.
|
||||
|
||||
This class must be sub-classed in order to create
|
||||
new engines.
|
||||
"""
|
||||
|
||||
def _update_key(self, key):
|
||||
if isinstance(key, six.string_types):
|
||||
key = key.encode()
|
||||
digest = hashes.Hash(hashes.SHA256(), backend=default_backend())
|
||||
digest.update(key)
|
||||
engine_key = digest.finalize()
|
||||
|
||||
self._initialize_engine(engine_key)
|
||||
|
||||
def encrypt(self, value):
|
||||
raise NotImplementedError('Subclasses must implement this!')
|
||||
|
||||
def decrypt(self, value):
|
||||
raise NotImplementedError('Subclasses must implement this!')
|
||||
|
||||
|
||||
class AesEngine(EncryptionDecryptionBaseEngine):
|
||||
"""Provide AES encryption and decryption methods."""
|
||||
|
||||
BLOCK_SIZE = 16
|
||||
PADDING = six.b('*')
|
||||
|
||||
def _initialize_engine(self, parent_class_key):
|
||||
self.secret_key = parent_class_key
|
||||
self.iv = self.secret_key[:16]
|
||||
self.cipher = Cipher(
|
||||
algorithms.AES(self.secret_key),
|
||||
modes.CBC(self.iv),
|
||||
backend=default_backend()
|
||||
)
|
||||
|
||||
def _pad(self, value):
|
||||
"""Pad the message to be encrypted, if needed."""
|
||||
BS = self.BLOCK_SIZE
|
||||
P = self.PADDING
|
||||
padded = (value + (BS - len(value) % BS) * P)
|
||||
return padded
|
||||
|
||||
def encrypt(self, value):
|
||||
if not isinstance(value, six.string_types):
|
||||
value = repr(value)
|
||||
if isinstance(value, six.text_type):
|
||||
value = str(value)
|
||||
value = value.encode()
|
||||
value = self._pad(value)
|
||||
encryptor = self.cipher.encryptor()
|
||||
encrypted = encryptor.update(value) + encryptor.finalize()
|
||||
encrypted = base64.b64encode(encrypted)
|
||||
return encrypted
|
||||
|
||||
def decrypt(self, value):
|
||||
if isinstance(value, six.text_type):
|
||||
value = str(value)
|
||||
decryptor = self.cipher.decryptor()
|
||||
decrypted = base64.b64decode(value)
|
||||
decrypted = decryptor.update(decrypted) + decryptor.finalize()
|
||||
decrypted = decrypted.rstrip(self.PADDING)
|
||||
if not isinstance(decrypted, six.string_types):
|
||||
decrypted = decrypted.decode('utf-8')
|
||||
return decrypted
|
||||
|
||||
|
||||
def migrate_to_fernet(aes_encrypted, old_key, new_key):
|
||||
"""
|
||||
Will attempt to migrate an aes encrypted to fernet encryption
|
||||
:param aes_encrypted:
|
||||
:return: fernet encrypted value
|
||||
"""
|
||||
engine = AesEngine()
|
||||
engine._update_key(old_key)
|
||||
|
||||
if not isinstance(aes_encrypted, six.string_types):
|
||||
return
|
||||
|
||||
aes_decrypted = engine.decrypt(aes_encrypted)
|
||||
fernet_encrypted = MultiFernet([Fernet(k) for k in new_key]).encrypt(bytes(aes_decrypted))
|
||||
|
||||
# sanity check
|
||||
fernet_decrypted = MultiFernet([Fernet(k) for k in new_key]).decrypt(fernet_encrypted)
|
||||
if fernet_decrypted != aes_decrypted:
|
||||
raise Exception("WARNING: Decrypted values do not match!")
|
||||
|
||||
return fernet_encrypted
|
||||
|
||||
|
||||
def migrate_from_fernet(fernet_encrypted, old_key, new_key):
|
||||
"""
|
||||
Will attempt to migrate from a fernet encryption to aes
|
||||
:param fernet_encrypted:
|
||||
:return:
|
||||
"""
|
||||
engine = AesEngine()
|
||||
engine._update_key(new_key)
|
||||
|
||||
fernet_decrypted = MultiFernet([Fernet(k) for k in old_key]).decrypt(fernet_encrypted)
|
||||
aes_encrypted = engine.encrypt(fernet_decrypted)
|
||||
|
||||
# sanity check
|
||||
aes_decrypted = engine.decrypt(aes_encrypted)
|
||||
if fernet_decrypted != aes_decrypted:
|
||||
raise Exception("WARNING: Decrypted values do not match!")
|
||||
|
||||
return aes_encrypted
|
||||
|
||||
|
||||
def upgrade():
|
||||
old_key = current_app.config.get('LEMUR_ENCRYPTION_KEY')
|
||||
print "Using: {0} as decryption key".format(old_key)
|
||||
# generate a new fernet token
|
||||
|
||||
if current_app.config.get('LEMUR_ENCRYPTION_KEYS'):
|
||||
new_key = current_app.config.get('LEMUR_ENCRYPTION_KEYS')
|
||||
else:
|
||||
new_key = [Fernet.generate_key()]
|
||||
|
||||
print "Using: {0} as new encryption key, save this and place it in your configuration!".format(new_key)
|
||||
|
||||
# migrate private_keys
|
||||
temp_keys = []
|
||||
for id, private_key in conn.execute(text('select id, private_key from certificates where private_key is not null')):
|
||||
aes_encrypted = StringIO(private_key).read()
|
||||
fernet_encrypted = migrate_to_fernet(aes_encrypted, old_key, new_key)
|
||||
temp_keys.append({'id': id, 'aes': aes_encrypted, 'fernet': fernet_encrypted})
|
||||
|
||||
op.bulk_insert(temp_key_table, temp_keys)
|
||||
|
||||
for id, fernet in conn.execute(text('select id, fernet from encrypted_keys')):
|
||||
stmt = text("update certificates set private_key=:key where id=:id")
|
||||
stmt = stmt.bindparams(key=fernet, id=id)
|
||||
op.execute(stmt)
|
||||
print "Certificate {0} has been migrated".format(id)
|
||||
|
||||
# migrate role_passwords
|
||||
temp_passwords = []
|
||||
for id, password in conn.execute(text('select id, password from roles where password is not null')):
|
||||
aes_encrypted = StringIO(password).read()
|
||||
fernet_encrypted = migrate_to_fernet(aes_encrypted, old_key, new_key)
|
||||
temp_passwords.append({'id': id, 'aes': aes_encrypted, 'fernet': fernet_encrypted})
|
||||
|
||||
op.bulk_insert(temp_password_table, temp_passwords)
|
||||
|
||||
for id, fernet in conn.execute(text('select id, fernet from encrypted_passwords')):
|
||||
stmt = text("update roles set password=:password where id=:id")
|
||||
stmt = stmt.bindparams(password=fernet, id=id)
|
||||
print stmt
|
||||
op.execute(stmt)
|
||||
print "Password {0} has been migrated".format(id)
|
||||
|
||||
op.drop_table('encrypted_keys')
|
||||
op.drop_table('encrypted_passwords')
|
||||
|
||||
|
||||
def downgrade():
|
||||
old_key = current_app.config.get('LEMUR_ENCRYPTION_KEYS')
|
||||
print "Using: {0} as decryption key(s)".format(old_key)
|
||||
|
||||
# generate aes valid key
|
||||
if current_app.config.get('LEMUR_ENCRYPTION_KEY'):
|
||||
new_key = current_app.config.get('LEMUR_ENCRYPTION_KEY')
|
||||
else:
|
||||
new_key = get_psuedo_random_string()
|
||||
print "Using: {0} as the encryption key, save this and place it in your configuration!".format(new_key)
|
||||
|
||||
# migrate keys
|
||||
temp_keys = []
|
||||
for id, private_key in conn.execute(text('select id, private_key from certificates where private_key is not null')):
|
||||
fernet_encrypted = StringIO(private_key).read()
|
||||
aes_encrypted = migrate_from_fernet(fernet_encrypted, old_key, new_key)
|
||||
temp_keys.append({'id': id, 'aes': aes_encrypted, 'fernet': fernet_encrypted})
|
||||
|
||||
op.bulk_insert(temp_key_table, temp_keys)
|
||||
|
||||
for id, aes in conn.execute(text('select id, aes from encrypted_keys')):
|
||||
stmt = text("update certificates set private_key=:key where id=:id")
|
||||
stmt = stmt.bindparams(key=aes, id=id)
|
||||
print stmt
|
||||
op.execute(stmt)
|
||||
print "Certificate {0} has been migrated".format(id)
|
||||
|
||||
# migrate role_passwords
|
||||
temp_passwords = []
|
||||
for id, password in conn.execute(text('select id, password from roles where password is not null')):
|
||||
fernet_encrypted = StringIO(password).read()
|
||||
aes_encrypted = migrate_from_fernet(fernet_encrypted, old_key, new_key)
|
||||
temp_passwords.append({'id': id, 'aes': aes_encrypted, 'fernet': fernet_encrypted})
|
||||
|
||||
op.bulk_insert(temp_password_table, temp_passwords)
|
||||
|
||||
for id, aes in conn.execute(text('select id, aes from encrypted_passwords')):
|
||||
stmt = text("update roles set password=:password where id=:id")
|
||||
stmt = stmt.bindparams(password=aes, id=id)
|
||||
op.execute(stmt)
|
||||
print "Password {0} has been migrated".format(id)
|
||||
|
||||
op.drop_table('encrypted_keys')
|
||||
op.drop_table('encrypted_passwords')
|
@ -36,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
|
||||
|
||||
|
||||
|
@ -133,9 +133,9 @@ class NotificationsList(AuthenticatedResource):
|
||||
|
||||
:query sortBy: field to sort on
|
||||
:query sortDir: acs or desc
|
||||
:query page: int. default is 1
|
||||
:query filter: key value pair. format is k=v;
|
||||
:query limit: limit number. default is 10
|
||||
:query page: int default is 1
|
||||
:query filter: key value pair format is k;v
|
||||
:query limit: limit number default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
"""
|
||||
@ -470,9 +470,9 @@ class CertificateNotifications(AuthenticatedResource):
|
||||
|
||||
:query sortBy: field to sort on
|
||||
:query sortDir: acs or desc
|
||||
:query page: int. default is 1
|
||||
:query filter: key value pair. format is k=v;
|
||||
:query limit: limit number. default is 10
|
||||
:query page: int default is 1
|
||||
:query filter: key value pair format is k;v
|
||||
:query limit: limit number default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
"""
|
||||
|
@ -15,6 +15,7 @@ class ExportPlugin(Plugin):
|
||||
exporters will inherit from.
|
||||
"""
|
||||
type = 'export'
|
||||
requires_key = True
|
||||
|
||||
def export(self):
|
||||
raise NotImplemented
|
||||
|
@ -43,15 +43,18 @@ class AWSDestinationPlugin(DestinationPlugin):
|
||||
# }
|
||||
|
||||
def upload(self, name, body, private_key, cert_chain, options, **kwargs):
|
||||
try:
|
||||
iam.upload_cert(find_value('accountNumber', options), name, body, private_key, cert_chain=cert_chain)
|
||||
except BotoServerError as e:
|
||||
if e.error_code != 'EntityAlreadyExists':
|
||||
raise Exception(e)
|
||||
if private_key:
|
||||
try:
|
||||
iam.upload_cert(find_value('accountNumber', options), name, body, private_key, cert_chain=cert_chain)
|
||||
except BotoServerError as e:
|
||||
if e.error_code != 'EntityAlreadyExists':
|
||||
raise Exception(e)
|
||||
|
||||
e = find_value('elb', options)
|
||||
if e:
|
||||
elb.attach_certificate(kwargs['accountNumber'], ['region'], e['name'], e['port'], e['certificateId'])
|
||||
e = find_value('elb', options)
|
||||
if e:
|
||||
elb.attach_certificate(kwargs['accountNumber'], ['region'], e['name'], e['port'], e['certificateId'])
|
||||
else:
|
||||
raise Exception("Unable to upload to AWS, private key is required")
|
||||
|
||||
|
||||
class AWSSourcePlugin(SourcePlugin):
|
||||
|
File diff suppressed because one or more lines are too long
@ -52,10 +52,79 @@ def split_chain(chain):
|
||||
return certs
|
||||
|
||||
|
||||
class JavaExportPlugin(ExportPlugin):
|
||||
title = 'Java'
|
||||
slug = 'java-export'
|
||||
description = 'Attempts to generate a JKS keystore or truststore'
|
||||
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 JavaTruststoreExportPlugin(ExportPlugin):
|
||||
title = 'Java Truststore (JKS)'
|
||||
slug = 'java-truststore-jks'
|
||||
description = 'Attempts to generate a JKS truststore'
|
||||
requires_key = False
|
||||
version = java.VERSION
|
||||
|
||||
author = 'Kevin Glisson'
|
||||
@ -63,18 +132,66 @@ class JavaExportPlugin(ExportPlugin):
|
||||
|
||||
options = [
|
||||
{
|
||||
'name': 'type',
|
||||
'type': 'select',
|
||||
'required': True,
|
||||
'available': ['Java Key Store (JKS)'],
|
||||
'helpMessage': 'Choose the format you wish to export',
|
||||
'name': 'alias',
|
||||
'type': 'str',
|
||||
'required': False,
|
||||
'helpMessage': 'Enter the alias you wish to use for the truststore.',
|
||||
},
|
||||
{
|
||||
'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,}$'
|
||||
'validation': ''
|
||||
},
|
||||
]
|
||||
|
||||
def export(self, body, chain, key, options, **kwargs):
|
||||
"""
|
||||
Generates a Java Truststore
|
||||
|
||||
:param key:
|
||||
:param chain:
|
||||
:param body:
|
||||
:param options:
|
||||
:param kwargs:
|
||||
"""
|
||||
|
||||
if self.get_option('alias', options):
|
||||
alias = self.get_option('alias', options)
|
||||
else:
|
||||
alias = "blah"
|
||||
|
||||
if self.get_option('passphrase', options):
|
||||
passphrase = self.get_option('passphrase', options)
|
||||
else:
|
||||
passphrase = get_psuedo_random_string()
|
||||
|
||||
with mktemppath() as jks_tmp:
|
||||
create_truststore(body, chain, jks_tmp, alias, passphrase)
|
||||
|
||||
with open(jks_tmp, 'rb') as f:
|
||||
raw = f.read()
|
||||
|
||||
return "jks", passphrase, raw
|
||||
|
||||
|
||||
class JavaKeystoreExportPlugin(ExportPlugin):
|
||||
title = 'Java Keystore (JKS)'
|
||||
slug = 'java-keystore-jks'
|
||||
description = 'Attempts to generate a JKS keystore'
|
||||
version = java.VERSION
|
||||
|
||||
author = 'Kevin Glisson'
|
||||
author_url = 'https://github.com/netflix/lemur'
|
||||
|
||||
options = [
|
||||
{
|
||||
'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': ''
|
||||
},
|
||||
{
|
||||
'name': 'alias',
|
||||
@ -86,7 +203,7 @@ class JavaExportPlugin(ExportPlugin):
|
||||
|
||||
def export(self, body, chain, key, options, **kwargs):
|
||||
"""
|
||||
Generates a Java Keystore or Truststore
|
||||
Generates a Java Keystore
|
||||
|
||||
:param key:
|
||||
:param chain:
|
||||
@ -105,71 +222,14 @@ class JavaExportPlugin(ExportPlugin):
|
||||
else:
|
||||
alias = "blah"
|
||||
|
||||
if not key:
|
||||
raise Exception("Unable to export, no private key found.")
|
||||
with mktemppath() as jks_tmp:
|
||||
if not key:
|
||||
raise Exception("Unable to export, no private key found.")
|
||||
|
||||
with mktempfile() as cert_tmp:
|
||||
with open(cert_tmp, 'w') as f:
|
||||
f.write(body)
|
||||
create_truststore(body, chain, jks_tmp, alias, passphrase)
|
||||
create_keystore(body, jks_tmp, key, alias, passphrase)
|
||||
|
||||
with mktempfile() as key_tmp:
|
||||
with open(key_tmp, 'w') as f:
|
||||
f.write(key)
|
||||
with open(jks_tmp, 'rb') as f:
|
||||
raw = f.read()
|
||||
|
||||
# Create PKCS12 keystore from private key and public certificate
|
||||
with mktempfile() as p12_tmp:
|
||||
run_process([
|
||||
"openssl",
|
||||
"pkcs12",
|
||||
"-export",
|
||||
"-name", alias,
|
||||
"-in", cert_tmp,
|
||||
"-inkey", key_tmp,
|
||||
"-out", p12_tmp,
|
||||
"-password", "pass:{}".format(passphrase)
|
||||
])
|
||||
|
||||
# Convert PKCS12 keystore into a JKS keystore
|
||||
with mktemppath() as jks_tmp:
|
||||
run_process([
|
||||
"keytool",
|
||||
"-importkeystore",
|
||||
"-destkeystore", jks_tmp,
|
||||
"-srckeystore", p12_tmp,
|
||||
"-srcstoretype", "PKCS12",
|
||||
"-alias", alias,
|
||||
"-srcstorepass", passphrase,
|
||||
"-deststorepass", passphrase
|
||||
])
|
||||
|
||||
# Import leaf cert in to JKS keystore
|
||||
run_process([
|
||||
"keytool",
|
||||
"-importcert",
|
||||
"-file", cert_tmp,
|
||||
"-keystore", jks_tmp,
|
||||
"-alias", "{0}_cert".format(alias),
|
||||
"-storepass", passphrase,
|
||||
"-noprompt"
|
||||
])
|
||||
|
||||
# Import the entire chain
|
||||
for idx, cert in enumerate(split_chain(chain)):
|
||||
with mktempfile() as c_tmp:
|
||||
with open(c_tmp, 'w') as f:
|
||||
f.write(cert)
|
||||
# Import signed cert in to JKS keystore
|
||||
run_process([
|
||||
"keytool",
|
||||
"-importcert",
|
||||
"-file", c_tmp,
|
||||
"-keystore", jks_tmp,
|
||||
"-alias", "{0}_cert_{1}".format(alias, idx),
|
||||
"-storepass", passphrase,
|
||||
"-noprompt"
|
||||
])
|
||||
|
||||
with open(jks_tmp, 'rb') as f:
|
||||
raw = f.read()
|
||||
|
||||
return "jks", passphrase, raw
|
||||
return "jks", passphrase, raw
|
||||
|
5
lemur/plugins/lemur_openssl/__init__.py
Normal file
5
lemur/plugins/lemur_openssl/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
try:
|
||||
VERSION = __import__('pkg_resources') \
|
||||
.get_distribution(__name__).version
|
||||
except Exception as e:
|
||||
VERSION = 'unknown'
|
130
lemur/plugins/lemur_openssl/plugin.py
Normal file
130
lemur/plugins/lemur_openssl/plugin.py
Normal 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
|
1
lemur/plugins/lemur_openssl/tests/conftest.py
Normal file
1
lemur/plugins/lemur_openssl/tests/conftest.py
Normal file
@ -0,0 +1 @@
|
||||
from lemur.tests.conftest import * # noqa
|
63
lemur/plugins/lemur_openssl/tests/test_openssl.py
Normal file
63
lemur/plugins/lemur_openssl/tests/test_openssl.py
Normal file
@ -0,0 +1,63 @@
|
||||
PRIVATE_KEY_STR = b"""
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEogIBAAKCAQEAsXn+QZRATxryRmGXI4fdI+0a2oBwuVh8fC/9bcqX6c5eDmgc
|
||||
rj6esmc1hpIFxMM3DvkFXX6xISkU6B5fmYDEGZLi7NvcXF3+EoA/SCkP1MFlvqhn
|
||||
EvNhb0t1fBLs0i/0gfTS/FHBZY1ekHisd/sUetCDZ7F11RxMwws0Oc8bl7j1TpRc
|
||||
awXFAsh/aWwQOwFeyWU7TtZeAE7sMyWXInBg37tKk1wlv+mN+27WijI091+amkVy
|
||||
zIV6mA5OHfqbjuqV8uQflN8jE244Qr7shtSk7LpBpWf0M6dC7dXbuUctHFhqcDjy
|
||||
3IRUl+NisKRoMtq+a0uehfmpFNSUD7F4gdUtSwIDAQABAoIBAGITsZ+aBuPwVzzv
|
||||
x286MMoeyL1BR4oVzU1v09Rtpf/uLGo3vMnKDzc19A12+rseynl6wi1FyysxIb2Y
|
||||
s2oID9a2JrOQWLmus66TsuT01CvV6J0xQSzm1MyFXdqANuF84NlEa6hGoeK1+jFK
|
||||
jr0LQukP+9484oovxnfu5CCiRHRWNZmeuekuYhI1SJf343Tr6jwvyr6KZpnIy0Yt
|
||||
axuuIZdCfY9ZV2vFG89GwwgwVQrhf14Kv5vBMZrNh1lRGsr0Sqlx5cGkPRAy90lg
|
||||
HjrRMogrtXr3AR5Pk2qqAYXzZBU2EFhJ3k2njpwOzlSj0r0ZwTmejZ89cco0sW5j
|
||||
+eQ6aRECgYEA1tkNW75fgwU52Va5VETCzG8II/pZdqNygnoc3z8EutN+1w8f6Tr+
|
||||
PdpKSICW0z7Iq4f5k/4wrA5xw1vy5RBMH0ZP29GwHTvCPiTBboR9vWvxQvZn1jb9
|
||||
wvKa0RxE18KcF0YIyTnZMubkA17QTFlvCNyZg0iCqeyFYPyqVE+R4AkCgYEA03h1
|
||||
XrqECZDDbG9HLUdGbkZNk4VzTcF6dQ3GAPY8M/H7rw5BbvH0RZLOrzl46DDVzKTg
|
||||
B1VOReAHsxBKFdkqeq1A99CLDow6vHTIEG8DwxkA7/2QPkt8MybwdApUyYnQh5/v
|
||||
CxwkRt4Mm+EiYfn5iyL8yI+vaQSRToVO/3BND7MCgYAJQSpBJG8qzqPSR9kN1zRo
|
||||
5/N60ULfSGUbV7U8rJNAlPGmw+EFA+SFt4xxmRBmIxMzyFSo2k8waiLeXmyVD2Go
|
||||
CzhPaLXkXHmegajPYOelrCulTcXlRVMi/Z5LmaMhhCGDIyInwNUpSybROllQoJ2W
|
||||
zSHTtODj/usz5U5U+WR4OQKBgHQRosI6t2wUo96peTS18UdnmP7GeZINBuymga5X
|
||||
eJW+VLkxpuKBNOTW/lCYx+8Rlte7CyebP9oEa9VxtGgniTRKUeVy9lAm0bpMkt7K
|
||||
QBNebvBKiVhX0DS3Q7U9UmpIFUfLlcXQTW0ERYFtYZTLQpeGvZ5LlyiaFDM34jM7
|
||||
7WAXAoGANDPJdQLEuimCOAMx/xoecNWeZIP6ieB0hVBrwLNxsaZlkn1KodUMuvla
|
||||
VEowbtPRdc9o3VZRh4q9cEakssTvOD70hgUZCFcMarmc37RgRvvD2fsZmDZF6qd3
|
||||
QfHplREs9F0sW+eiirczG7up4XL+CA162TtZxW+2GAiQhwhE5jA=
|
||||
-----END RSA PRIVATE KEY-----
|
||||
"""
|
||||
|
||||
EXTERNAL_VALID_STR = b"""
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIID2zCCAsOgAwIBAgICA+0wDQYJKoZIhvcNAQELBQAwgZcxCzAJBgNVBAYTAlVT
|
||||
MRMwEQYDVQQIDApDYWxpZm9ybmlhMRIwEAYDVQQHDAlMb3MgR2F0b3MxDTALBgNV
|
||||
BAMMBHRlc3QxFjAUBgNVBAoMDU5ldGZsaXgsIEluYy4xEzARBgNVBAsMCk9wZXJh
|
||||
dGlvbnMxIzAhBgkqhkiG9w0BCQEWFGtnbGlzc29uQG5ldGZsaXguY29tMB4XDTE1
|
||||
MTEyMzIxNDIxMFoXDTE1MTEyNjIxNDIxMFowcjENMAsGA1UEAwwEdGVzdDEWMBQG
|
||||
A1UECgwNTmV0ZmxpeCwgSW5jLjETMBEGA1UECwwKT3BlcmF0aW9uczELMAkGA1UE
|
||||
BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExEjAQBgNVBAcMCUxvcyBHYXRvczCC
|
||||
ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALF5/kGUQE8a8kZhlyOH3SPt
|
||||
GtqAcLlYfHwv/W3Kl+nOXg5oHK4+nrJnNYaSBcTDNw75BV1+sSEpFOgeX5mAxBmS
|
||||
4uzb3Fxd/hKAP0gpD9TBZb6oZxLzYW9LdXwS7NIv9IH00vxRwWWNXpB4rHf7FHrQ
|
||||
g2exddUcTMMLNDnPG5e49U6UXGsFxQLIf2lsEDsBXsllO07WXgBO7DMllyJwYN+7
|
||||
SpNcJb/pjftu1ooyNPdfmppFcsyFepgOTh36m47qlfLkH5TfIxNuOEK+7IbUpOy6
|
||||
QaVn9DOnQu3V27lHLRxYanA48tyEVJfjYrCkaDLavmtLnoX5qRTUlA+xeIHVLUsC
|
||||
AwEAAaNVMFMwUQYDVR0fBEowSDBGoESgQoZAaHR0cDovL3Rlc3QuY2xvdWRjYS5j
|
||||
cmwubmV0ZmxpeC5jb20vdGVzdERlY3JpcHRpb25DQVJvb3QvY3JsLnBlbTANBgkq
|
||||
hkiG9w0BAQsFAAOCAQEAiHREBKg7zhlQ/N7hDIkxgodRSWD7CVbJGSCdkR3Pvr6+
|
||||
jHBVNTJUrYqy7sL2pIutoeiSTQEH65/Gbm30mOnNu+lvFKxTxzof6kNYv8cyc8sX
|
||||
eBuBfSrlTodPFSHXQIpOexZgA0f30LOuXegqzxgXkKg+uMXOez5Zo5pNjTUow0He
|
||||
oe+V1hfYYvL1rocCmBOkhIGWz7622FxKDawRtZTGVsGsMwMIWyvS3+KQ04K8yHhp
|
||||
bQOg9zZAoYQuHY1inKBnA0II8eW0hPpJrlZoSqN8Tp0NSBpFiUk3m7KNFP2kITIf
|
||||
tTneAgyUsgfDxNDifZryZSzg7MH31sTBcYaotSmTXw==
|
||||
-----END CERTIFICATE-----
|
||||
"""
|
||||
|
||||
|
||||
def test_export_certificate_to_jks(app):
|
||||
from lemur.plugins.base import plugins
|
||||
p = plugins.get('java-export')
|
||||
options = {'passphrase': 'test1234'}
|
||||
raw = p.export(EXTERNAL_VALID_STR, "", PRIVATE_KEY_STR, options)
|
||||
assert raw != b""
|
@ -79,7 +79,7 @@ def process_options(options):
|
||||
|
||||
if options.get('validityEnd'):
|
||||
end_date, period = get_default_issuance(options)
|
||||
data['specificEndDate'] = end_date
|
||||
data['specificEndDate'] = str(end_date)
|
||||
data['validityPeriod'] = period
|
||||
|
||||
return data
|
||||
|
@ -75,9 +75,9 @@ class RolesList(AuthenticatedResource):
|
||||
|
||||
:query sortBy: field to sort on
|
||||
:query sortDir: acs or desc
|
||||
:query page: int. default is 1
|
||||
:query filter: key value pair. format is k=v;
|
||||
:query limit: limit number. default is 10
|
||||
:query page: int default is 1
|
||||
:query filter: key value pair format is k;v
|
||||
:query limit: limit number default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
@ -367,9 +367,9 @@ class UserRolesList(AuthenticatedResource):
|
||||
|
||||
:query sortBy: field to sort on
|
||||
:query sortDir: acs or desc
|
||||
:query page: int. default is 1
|
||||
:query filter: key value pair. format is k=v;
|
||||
:query limit: limit number. default is 10
|
||||
:query page: int default is 1
|
||||
:query filter: key value pair format is k;v
|
||||
:query limit: limit number default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
"""
|
||||
@ -426,9 +426,9 @@ class AuthorityRolesList(AuthenticatedResource):
|
||||
|
||||
:query sortBy: field to sort on
|
||||
:query sortDir: acs or desc
|
||||
:query page: int. default is 1
|
||||
:query filter: key value pair. format is k=v;
|
||||
:query limit: limit number. default is 10
|
||||
:query page: int default is 1
|
||||
:query filter: key value pair format is k;v
|
||||
:query limit: limit number default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
"""
|
||||
|
@ -76,7 +76,7 @@ def sync(labels=None):
|
||||
if source.label not in labels:
|
||||
continue
|
||||
|
||||
current_app.logger.error("Retrieving certificates from {0}".format(source.label))
|
||||
current_app.logger.debug("Retrieving certificates from {0}".format(source.label))
|
||||
s = plugins.get(source.plugin_name)
|
||||
certificates = s.get_certificates(source.options)
|
||||
|
||||
|
@ -83,9 +83,9 @@ class SourcesList(AuthenticatedResource):
|
||||
|
||||
:query sortBy: field to sort on
|
||||
:query sortDir: acs or desc
|
||||
:query page: int. default is 1
|
||||
:query filter: key value pair. format is k=v;
|
||||
:query limit: limit number. default is 10
|
||||
:query page: int default is 1
|
||||
:query filter: key value pair format is k;v
|
||||
:query limit: limit number default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
"""
|
||||
@ -349,9 +349,9 @@ class CertificateSources(AuthenticatedResource):
|
||||
|
||||
:query sortBy: field to sort on
|
||||
:query sortDir: acs or desc
|
||||
:query page: int. default is 1
|
||||
:query filter: key value pair. format is k=v;
|
||||
:query limit: limit number. default is 10
|
||||
:query page: int default is 1
|
||||
:query filter: key value pair format is k;v
|
||||
:query limit: limit number default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
"""
|
||||
|
268
lemur/static/app/angular/app.js
vendored
268
lemur/static/app/angular/app.js
vendored
@ -1,138 +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',
|
||||
'ngFileSaver'
|
||||
])
|
||||
.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: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;
|
||||
_.each(providers, function(provider) {
|
||||
if ($authProvider.hasOwnProperty(provider.name)) {
|
||||
$authProvider[provider.name](provider);
|
||||
} 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;
|
||||
}
|
||||
$authProvider.oauth2(provider);
|
||||
}
|
||||
});
|
||||
|
||||
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="">«</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="">…</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="">»</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="">«</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="">…</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="">»</a> </li> </ul> </div></div>');
|
||||
}]);
|
||||
}());
|
||||
|
||||
|
||||
|
@ -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;
|
||||
});
|
||||
|
@ -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>
|
||||
|
@ -16,14 +16,14 @@
|
||||
<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 }}
|
||||
{{ 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>
|
||||
<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>
|
||||
@ -31,7 +31,7 @@
|
||||
</form>
|
||||
<div ng-show="passphrase">
|
||||
<h3>Successfully exported!</h3>
|
||||
<h4>You're passphrase is: <strong>{{ passphrase }}</strong></h4>
|
||||
<h4>Your passphrase is: <strong>{{ passphrase }}</strong></h4>
|
||||
<p ng-show="additional">{{ additional }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -48,7 +48,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-show="certificate.authority" class="form-group">
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-2">
|
||||
Certificate Template
|
||||
</label>
|
||||
@ -110,6 +110,20 @@
|
||||
</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>
|
||||
|
@ -24,8 +24,8 @@
|
||||
<tr ng-class="{'even-row': $even }" ng-repeat-start="certificate in $data track by $index">
|
||||
<td data-title="'Name'" sortable="'name'" filter="{ 'name': 'text' }">
|
||||
<ul class="list-unstyled">
|
||||
<li>{{ ::certificate.name }}</li>
|
||||
<li><span class="text-muted">{{ ::certificate.owner }}</span></li>
|
||||
<li>{{ certificate.name }}</li>
|
||||
<li><span class="text-muted">{{ certificate.owner }}</span></li>
|
||||
</ul>
|
||||
</td>
|
||||
<td data-title="'Active'" filter="{ 'active': 'select' }" filter-data="getCertificateStatus()">
|
||||
@ -35,10 +35,10 @@
|
||||
</form>
|
||||
</td>
|
||||
<td data-title="'Issuer'" sortable="'issuer'" filter="{ 'issuer': 'text' }">
|
||||
{{ ::certificate.authority.name || certificate.issuer }}
|
||||
{{ certificate.authority.name || certificate.issuer }}
|
||||
</td>
|
||||
<td data-title="'Domains'" filter="{ 'cn': 'text'}">
|
||||
{{ ::certificate.cn }}
|
||||
{{ certificate.cn }}
|
||||
</td>
|
||||
<td class="col-md-2" data-title="''">
|
||||
<div class="btn-group pull-right">
|
||||
@ -62,19 +62,19 @@
|
||||
<li class="list-group-item">
|
||||
<strong>Creator</strong>
|
||||
<span class="pull-right">
|
||||
{{ ::certificate.creator.email }}
|
||||
{{ certificate.creator.email }}
|
||||
</span>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<strong>Not Before</strong>
|
||||
<span class="pull-right" tooltip="{{ ::certificate.notBefore }}">
|
||||
{{ ::momentService.createMoment(certificate.notBefore) }}
|
||||
<span class="pull-right" tooltip="{{ certificate.notBefore }}">
|
||||
{{ momentService.createMoment(certificate.notBefore) }}
|
||||
</span>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<strong>Not After</strong>
|
||||
<span class="pull-right" tooltip="{{ ::certificate.notAfter }}">
|
||||
{{ ::momentService.createMoment(certificate.notAfter) }}
|
||||
<span class="pull-right" tooltip="{{ certificate.notAfter }}">
|
||||
{{ momentService.createMoment(certificate.notAfter) }}
|
||||
</span>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
@ -86,15 +86,15 @@
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<strong>Bits</strong>
|
||||
<span class="pull-right">{{ ::certificate.bits }}</span>
|
||||
<span class="pull-right">{{ certificate.bits }}</span>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<strong>Signing Algorithm</strong>
|
||||
<span class="pull-right">{{ ::certificate.signingAlgorithm }}</span>
|
||||
<span class="pull-right">{{ certificate.signingAlgorithm }}</span>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<strong>Serial</strong>
|
||||
<span class="pull-right">{{ ::certificate.serial }}</span>
|
||||
<span class="pull-right">{{ certificate.serial }}</span>
|
||||
</li>
|
||||
<li
|
||||
tooltip="Lemur will attempt to check a certificates validity, this is used to track whether a certificate as been revoked"
|
||||
@ -108,7 +108,7 @@
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<strong>Description</strong>
|
||||
<p>{{ ::certificate.description }}</p>
|
||||
<p>{{ certificate.description }}</p>
|
||||
</li>
|
||||
</ul>
|
||||
</tab>
|
||||
@ -116,8 +116,8 @@
|
||||
<tab-heading>Notifications</tab-heading>
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item" ng-repeat="notification in certificate.notifications">
|
||||
<strong>{{ ::notification.label }}</strong>
|
||||
<span class="pull-right">{{ ::notification.description}}</span>
|
||||
<strong>{{ notification.label }}</strong>
|
||||
<span class="pull-right">{{ notification.description}}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</tab>
|
||||
@ -125,24 +125,24 @@
|
||||
<tab-heading>Destinations</tab-heading>
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item" ng-repeat="destination in certificate.destinations">
|
||||
<strong>{{ ::destination.label }}</strong>
|
||||
<span class="pull-right">{{ ::destination.description }}</span>
|
||||
<strong>{{ destination.label }}</strong>
|
||||
<span class="pull-right">{{ destination.description }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</tab>
|
||||
<tab>
|
||||
<tab-heading>Domains</tab-heading>
|
||||
<div class="list-group">
|
||||
<a href="#/domains/{{ ::domain.id }}" class="list-group-item"
|
||||
ng-repeat="domain in certificate.domains">{{ ::domain.name }}</a>
|
||||
<a href="#/domains/{{ domain.id }}" class="list-group-item"
|
||||
ng-repeat="domain in certificate.domains">{{ domain.name }}</a>
|
||||
</div>
|
||||
</tab>
|
||||
<tab>
|
||||
<tab-heading>Replaces</tab-heading>
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item" ng-repeat="replacement in certificate.replacements">
|
||||
<strong>{{ ::replacement.name }}</strong>
|
||||
<p>{{ ::replacement.description}}</p>
|
||||
<strong>{{ replacement.name }}</strong>
|
||||
<p>{{ replacement.description}}</p>
|
||||
</li>
|
||||
</ul>
|
||||
</tab>
|
||||
@ -155,7 +155,7 @@
|
||||
tooltip="Copy chain to clipboard" tooltip-trigger="mouseenter" clipboard
|
||||
text="certificate.chain"></button>
|
||||
</tab-heading>
|
||||
<pre style="width: 100%">{{ ::certificate.chain }}</pre>
|
||||
<pre style="width: 100%">{{ certificate.chain }}</pre>
|
||||
</tab>
|
||||
<tab>
|
||||
<tab-heading>
|
||||
@ -164,7 +164,7 @@
|
||||
tooltip="Copy certificate to clipboard" tooltip-trigger="mouseenter" clipboard
|
||||
text="certificate.body"></button>
|
||||
</tab-heading>
|
||||
<pre style="width: 100%">{{ ::certificate.body }}</pre>
|
||||
<pre style="width: 100%">{{ certificate.body }}</pre>
|
||||
</tab>
|
||||
<tab ng-click="loadPrivateKey(certificate)">
|
||||
<tab-heading>
|
||||
@ -173,7 +173,7 @@
|
||||
tooltip="Copy key to clipboard" tooltip-trigger="mouseenter" clipboard
|
||||
text="certificate.privateKey"></button>
|
||||
</tab-heading>
|
||||
<pre style="width: 100%">{{ ::certificate.privateKey }}</pre>
|
||||
<pre style="width: 100%">{{ certificate.privateKey }}</pre>
|
||||
</tab>
|
||||
</tabset>
|
||||
</td>
|
||||
|
8
lemur/static/app/angular/domains/services.js
vendored
8
lemur/static/app/angular/domains/services.js
vendored
@ -12,4 +12,12 @@ angular.module('lemur')
|
||||
return domains;
|
||||
});
|
||||
};
|
||||
|
||||
DomainService.updateSensitive = function (domain) {
|
||||
return domain.put();
|
||||
};
|
||||
|
||||
DomainService.create = function (domain) {
|
||||
return DomainApi.post(domain);
|
||||
};
|
||||
});
|
||||
|
36
lemur/static/app/angular/domains/view/view.js
vendored
36
lemur/static/app/angular/domains/view/view.js
vendored
@ -10,7 +10,7 @@ angular.module('lemur')
|
||||
});
|
||||
})
|
||||
|
||||
.controller('DomainsViewController', function ($scope, DomainApi, ngTableParams) {
|
||||
.controller('DomainsViewController', function ($scope, $modal, DomainApi, DomainService, ngTableParams, toaster) {
|
||||
$scope.filter = {};
|
||||
$scope.domainsTable = new ngTableParams({
|
||||
page: 1, // show first page
|
||||
@ -29,6 +29,40 @@ angular.module('lemur')
|
||||
}
|
||||
});
|
||||
|
||||
$scope.updateSensitive = function (domain) {
|
||||
DomainService.updateSensitive(domain).then(
|
||||
function () {
|
||||
toaster.pop({
|
||||
type: 'success',
|
||||
title: domain.name,
|
||||
body: 'Updated!'
|
||||
});
|
||||
},
|
||||
function (response) {
|
||||
toaster.pop({
|
||||
type: 'error',
|
||||
title: domain.name,
|
||||
body: 'Unable to update! ' + response.data.message,
|
||||
timeout: 100000
|
||||
});
|
||||
domain.sensitive = domain.sensitive ? false : true;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.create = function () {
|
||||
var modalInstance = $modal.open({
|
||||
animation: true,
|
||||
controller: 'DomainsCreateController',
|
||||
templateUrl: '/angular/domains/domain/domain.tpl.html',
|
||||
size: 'lg'
|
||||
});
|
||||
|
||||
modalInstance.result.then(function () {
|
||||
$scope.domainsTable.reload();
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
$scope.toggleFilter = function (params) {
|
||||
params.settings().$scope.show_filter = !params.settings().$scope.show_filter;
|
||||
};
|
||||
|
@ -4,6 +4,9 @@
|
||||
<span class="text-muted"><small>Zone transfers as scary</small></span></h2>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<div class="btn-group pull-right">
|
||||
<button ng-click="create()" class="btn btn-primary">Create</button>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button ng-click="toggleFilter(domainsTable)" class="btn btn-default">Filter</button>
|
||||
</div>
|
||||
@ -16,6 +19,12 @@
|
||||
<td data-title="'Name'" sortable="'name'" filter="{ 'name': 'text' }">
|
||||
{{ domain.name }}
|
||||
</td>
|
||||
<td data-title="'Sensitive'">
|
||||
<form>
|
||||
<switch ng-change="updateSensitive(domain)" id="status" name="status"
|
||||
ng-model="domain.sensitive" class="green small"></switch>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -10,7 +10,7 @@ def test_domain_post(client):
|
||||
|
||||
|
||||
def test_domain_put(client):
|
||||
assert client.put(api.url_for(Domains, domain_id=1), data={}).status_code == 405
|
||||
assert client.put(api.url_for(Domains, domain_id=1), data={}).status_code == 401
|
||||
|
||||
|
||||
def test_domain_delete(client):
|
||||
@ -34,7 +34,7 @@ def test_auth_domain_post_(client):
|
||||
|
||||
|
||||
def test_auth_domain_put(client):
|
||||
assert client.put(api.url_for(Domains, domain_id=1), data={}, headers=VALID_USER_HEADER_TOKEN).status_code == 405
|
||||
assert client.put(api.url_for(Domains, domain_id=1), data={}, headers=VALID_USER_HEADER_TOKEN).status_code == 403
|
||||
|
||||
|
||||
def test_auth_domain_delete(client):
|
||||
@ -58,7 +58,7 @@ def test_admin_domain_post(client):
|
||||
|
||||
|
||||
def test_admin_domain_put(client):
|
||||
assert client.put(api.url_for(Domains, domain_id=1), data={}, headers=VALID_ADMIN_HEADER_TOKEN).status_code == 405
|
||||
assert client.put(api.url_for(Domains, domain_id=1), data={}, headers=VALID_ADMIN_HEADER_TOKEN).status_code == 400
|
||||
|
||||
|
||||
def test_admin_domain_delete(client):
|
||||
@ -74,7 +74,7 @@ def test_domains_get(client):
|
||||
|
||||
|
||||
def test_domains_post(client):
|
||||
assert client.post(api.url_for(DomainsList), data={}).status_code == 405
|
||||
assert client.post(api.url_for(DomainsList), data={}).status_code == 401
|
||||
|
||||
|
||||
def test_domains_put(client):
|
||||
|
@ -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):
|
||||
|
@ -96,9 +96,9 @@ class UsersList(AuthenticatedResource):
|
||||
|
||||
:query sortBy: field to sort on
|
||||
:query sortDir: acs or desc
|
||||
:query page: int. default is 1
|
||||
:query filter: key value pair. format is k=v;
|
||||
:query limit: limit number. default is 10
|
||||
:query page: int default is 1
|
||||
:query filter: key value pair format is k;v
|
||||
:query limit: limit number default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
"""
|
||||
@ -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')
|
||||
|
||||
|
@ -57,7 +57,7 @@
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "bower install --allow-root --config.interactive=false",
|
||||
"postinstall": "node_modules/.bin/bower install --allow-root --config.interactive=false",
|
||||
"pretest": "npm install && npm run build_static",
|
||||
"build_static": "gulp build",
|
||||
"prelint": "npm install",
|
||||
|
18
setup.py
18
setup.py
@ -38,20 +38,20 @@ install_requires = [
|
||||
'Flask-RESTful==0.3.3',
|
||||
'Flask-SQLAlchemy==2.1',
|
||||
'Flask-Script==2.0.5',
|
||||
'Flask-Migrate==1.6.0',
|
||||
'Flask-Migrate==1.7.0',
|
||||
'Flask-Bcrypt==0.7.1',
|
||||
'Flask-Principal==0.4.0',
|
||||
'Flask-Mail==0.9.1',
|
||||
'SQLAlchemy-Utils==0.31.3',
|
||||
'SQLAlchemy-Utils==0.31.4',
|
||||
'BeautifulSoup4==4.4.1',
|
||||
'requests==2.8.1',
|
||||
'requests==2.9.1',
|
||||
'psycopg2==2.6.1',
|
||||
'arrow==0.7.0',
|
||||
'boto==2.38.0', # we might make this optional
|
||||
'six==1.10.0',
|
||||
'gunicorn==19.4.1',
|
||||
'pycrypto==2.6.1',
|
||||
'cryptography==1.1.1',
|
||||
'cryptography==1.1.2',
|
||||
'pyopenssl==0.15.1',
|
||||
'pyjwt==1.4.0',
|
||||
'xmltodict==0.9.2',
|
||||
@ -61,9 +61,9 @@ install_requires = [
|
||||
|
||||
tests_require = [
|
||||
'pyflakes',
|
||||
'moto==0.4.18',
|
||||
'moto==0.4.19',
|
||||
'nose==1.3.7',
|
||||
'pytest==2.8.3',
|
||||
'pytest==2.8.5',
|
||||
'pytest-flask==0.10.0'
|
||||
]
|
||||
|
||||
@ -74,6 +74,8 @@ docs_require = [
|
||||
|
||||
dev_requires = [
|
||||
'flake8>=2.0,<3.0',
|
||||
'invoke',
|
||||
'twine'
|
||||
]
|
||||
|
||||
|
||||
@ -163,7 +165,9 @@ 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'
|
||||
'java_truststore_export = lemur.plugins.lemur_java.plugin:JavaTruststoreExportPlugin',
|
||||
'java_keystore_export = lemur.plugins.lemur_java.plugin:JavaKeystoreExportPlugin',
|
||||
'openssl_export = lemur.plugins.lemur_openssl.plugin:OpenSSLExportPlugin'
|
||||
],
|
||||
},
|
||||
classifiers=[
|
||||
|
Reference in New Issue
Block a user