Compare commits

..

83 Commits
0.2 ... 0.2.2

Author SHA1 Message Date
1c3c70d460 Merge pull request #241 from kevgliss/0.2.2.release
adding changelog
2016-02-05 13:01:35 -08:00
e8e7bdf9e0 adding changelog 2016-02-05 13:00:59 -08:00
d263e0e60c Merge pull request #240 from kevgliss/234-truststore-permission
Adding a new flag to export plugins 'requires_key' that specifies whe…
2016-01-29 12:55:41 -08:00
028d86c0bb Adding a new flag to export plugins 'requires_key' that specifies whether the export plugin needs access to the private key. Defaults to True. 2016-01-29 12:45:18 -08:00
f8b6830013 Merge pull request #239 from kevgliss/228-filter-values
Fixing documentation for filter format
2016-01-29 11:54:13 -08:00
49a40c50e8 Merge pull request #238 from kevgliss/231-authority-owner
associating new authorities with the owner roles
2016-01-29 11:47:56 -08:00
2ba48995fe Fixing documentation for filter format 2016-01-29 11:47:16 -08:00
3cc8ade6d8 associating new authorities with the owner roles 2016-01-29 10:59:04 -08:00
39c9a0a299 Merge pull request #237 from kevgliss/218_password_regex
relaxing keystore password validation
2016-01-29 10:37:49 -08:00
3ad317fb6d Merge pull request #236 from kevgliss/migration_script_fixups
Removing per 2.0 migration scripts
2016-01-29 10:30:41 -08:00
bd46440d12 relaxing keystore password validation 2016-01-29 10:29:04 -08:00
f3a28814ae Merge pull request #235 from kevgliss/226_replaces
Makes 'replacements' a non-required attribute for importing. Closes #226
2016-01-29 09:42:42 -08:00
9f8f64b9ec removing pre 2.0 migration scripts, and adding documentation for correct path during init 2016-01-29 09:22:12 -08:00
1e524a49c0 making 'replacements' a non-require attribute for importing. Closes #226 2016-01-29 09:02:51 -08:00
467c276fca Merge pull request #227 from AlexCline/fix_postinstall_for_224
Use the local bower instead of the global one.
2016-01-20 16:35:00 -08:00
f610e39418 Use the local bower instead of the global one.
This change updates package.json's postinstall command to use the
locally installed bower, rather than the global bower which might
not exist or might not be in the current user's PATH.
2016-01-20 17:10:41 -05:00
27d977b2fa Merge pull request #214 from ebgcdev/master
Minor spelling fix
2016-01-13 09:21:36 -08:00
b36e72bfcc Minor spelling fix
Using the possessive “Your” rather than “You’re” in “Your passphrase
is:”
2016-01-12 22:04:42 -08:00
e49701228d Merge pull request #212 from kevgliss/rolling
Adding a rolling metric count
2016-01-11 15:34:20 -08:00
48f8b33d7d Adding a rolling metric count 2016-01-11 15:26:32 -08:00
d87ace8c89 Merge pull request #211 from kevgliss/hotfix
fixing an issue were urllib does not like unicode
2016-01-11 10:38:45 -08:00
b1326d4145 fixing an issue were urllib does not like unicode 2016-01-11 10:31:58 -08:00
7c2862c958 Merge pull request #210 from kevgliss/hotfix
Fixes an assumption that 'subAltNames' are always passed to the API.
2016-01-11 09:08:38 -08:00
0a4f5ad64d Fixing an assumption that 'subAltNames' are always passed to the API. 2016-01-10 17:33:19 -08:00
c617a11c55 Merge pull request #209 from kevgliss/migrate_chain
Adding command to transparently rotate the chain on an ELB
2016-01-10 14:37:29 -08:00
053167965a Adding command to transparently rotate the chain on an ELB 2016-01-10 14:20:36 -08:00
a7ac45b937 Merge pull request #206 from kevgliss/syncing
Fixing issue where we were seeing AWS API errors due to certificates …
2016-01-08 16:39:51 -08:00
5482bbf4bd Fixing issue where we were seeing AWS API errors due to certificates not having private keys and could not be uploaded or 'synced' 2016-01-07 13:42:46 -08:00
0a58e106b5 Merge pull request #205 from rpicard/rpicard/fixgooglesso
Fix how the provider settings are passed to Satellizer
2016-01-05 17:31:35 -08:00
a1395a5808 Fix how the provider settings are passed to Satellizer 2016-01-05 17:26:09 -08:00
a0d50ef03a Merge pull request #203 from kevgliss/ssoHostfix
fixing typo
2016-01-05 09:41:12 -08:00
685e2c8b6d fixing typo 2016-01-05 09:40:53 -08:00
c6d9a20fe5 Merge pull request #202 from kevgliss/hotfix
reverting depedency
2016-01-04 13:58:36 -08:00
4a952d867b reverting depedency 2016-01-04 13:58:12 -08:00
cb4cf43fcf Merge pull request #201 from kevgliss/hotfix
Fixing setup.py
2016-01-04 11:48:19 -08:00
1bce7a832b Fixing setup.py 2016-01-04 11:46:07 -08:00
574234f70f Merge pull request #200 from kevgliss/requirements
updating dependencies
2016-01-04 10:41:24 -08:00
42e5470dd0 updating dependencies 2016-01-04 10:36:39 -08:00
8199365324 Merge pull request #194 from CameronNemo/patch-1
docs/quickstart: fix port number
2015-12-31 14:27:18 -08:00
86c92eb31e docs/quickstart: fix port number 2015-12-31 12:57:18 -08:00
d9fd952c03 Merge pull request #193 from kevgliss/docs
Improving documentation layout
2015-12-31 11:21:48 -08:00
967c7ded8d Improving documentation layout 2015-12-31 11:12:56 -08:00
a4bf847b56 Merge pull request #192 from kevgliss/sensitive-domains
Adds ability for domains to be marked as sensitive and only be allowe…
2015-12-30 15:36:31 -08:00
d6917155e8 Fixing tests 2015-12-30 15:32:01 -08:00
3f024c1ef4 Adds ability for domains to be marked as sensitive and only be allowed to be issued by an admin closes #5 2015-12-30 15:11:08 -08:00
96d253f0f9 Merge pull request #191 from kevgliss/bump
version bump
2015-12-30 09:15:30 -08:00
9b166fb9a9 version bump 2015-12-30 09:15:11 -08:00
b8ae8cd452 Merge pull request #190 from kevgliss/0.2.1
0.2.1 release info
2015-12-30 09:11:46 -08:00
ca82b227b9 0.2.1 release info 2015-12-30 09:11:19 -08:00
862496495f Merge pull request #189 from m4c3/patch-1
Define ACTIVE_PROVIDERS in default config
2015-12-30 08:50:05 -08:00
8bb9a8c5d1 Define ACTIVE_PROVIDERS in default config
The configuration item ACTIVE_PROVIDERS must be initialized

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

1. Add the Google auth view resource
2. Make passwords optional when creating a new user. This allows an admin
to create a user without a password so that they can only login via Google.
2015-12-22 13:44:30 -05:00
70c92fea15 Merge pull request #183 from kevgliss/rotate
Adding rotate command
2015-12-18 12:05:52 -05:00
6211b126a9 Fixing py3 syntax error 2015-12-18 11:01:08 -05:00
54c3fcc72a Adding rotate command 2015-12-17 23:17:27 -05:00
27c9088ddb Merge pull request #182 from kevgliss/176-p12
Closes #176
2015-12-17 15:01:54 -08:00
b8c2d42cad Closes #176 2015-12-17 14:52:20 -08:00
1f5ddd9530 Merge pull request #181 from kevgliss/172-export
Closes #172
2015-12-17 14:35:55 -08:00
2896ce0dad Closes #172 2015-12-16 08:18:01 -08:00
29bcde145c 0.2.1 release 2015-12-14 10:42:51 -08:00
11db429bcc adding OSSMETADATA for NetflixOSS tracking 2015-12-11 15:57:28 -08:00
75aea9f885 Merge pull request #179 from rpicard/master
Update example supervisor configuration file
2015-12-10 18:31:02 -08:00
c80559005f Update example supervisor configuration file
supervisord should run as root and spawn the lemur process as the lemur
user. I also added the LEMUR_CONF environment variable because it was
not reading the configuration file in by default.
2015-12-10 17:39:49 -08:00
9b927cfcc2 Merge pull request #177 from kevgliss/docs
clarifying upgrade process
2015-12-09 17:29:13 -08:00
4db7931aa0 clarifying upgrade process 2015-12-09 17:18:01 -08:00
1e67329c64 Merge pull request #175 from kevgliss/notifications
Fixing templates
2015-12-04 09:57:42 -08:00
6d17e4d538 Fixing templates 2015-12-04 09:51:38 -08:00
350f58ec9d Merge pull request #174 from kevgliss/binding
Disabling one-time binding
2015-12-03 17:16:19 -08:00
de9478a992 Disabling one-time binding 2015-12-03 16:57:37 -08:00
70a2c985cf Merge pull request #171 from kevgliss/packaging
Fixing the startup port
2015-12-02 17:14:24 -08:00
78037dc9ec Fixing the startup port 2015-12-02 17:13:52 -08:00
9b11efd1e5 Merge pull request #170 from kevgliss/export
Adding export plugin docs
2015-12-02 16:05:36 -08:00
3c2ee8fbb3 Adding export plugin docs 2015-12-02 16:04:40 -08:00
163cc3f795 Merge pull request #169 from kevgliss/bump
Version bump
2015-12-02 14:54:17 -08:00
041382b02f Version bump 2015-12-02 14:53:46 -08:00
67 changed files with 1418 additions and 1027 deletions

View File

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

View File

@ -1,4 +1,3 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/

1
OSSMETADATA Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -96,5 +96,4 @@ Subpackages
lemur.notifications
lemur.plugins
lemur.roles
lemur.status
lemur.users

View File

@ -1,11 +0,0 @@
status Package
==============
:mod:`views` Module
-------------------
.. automodule:: lemur.status.views
:noindex:
:members:
:undoc-members:
:show-inheritance:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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__":

View File

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

View File

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

View File

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

View File

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

View 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 ###

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,6 +15,7 @@ class ExportPlugin(Plugin):
exporters will inherit from.
"""
type = 'export'
requires_key = True
def export(self):
raise NotImplemented

View File

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

View File

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

View File

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

View File

@ -0,0 +1,130 @@
"""
.. module: lemur.plugins.lemur_openssl.plugin
:platform: Unix
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
import subprocess
from flask import current_app
from lemur.utils import mktempfile, mktemppath
from lemur.plugins.bases import ExportPlugin
from lemur.plugins import lemur_openssl as openssl
from lemur.common.utils import get_psuedo_random_string
def run_process(command):
"""
Runs a given command with pOpen and wraps some
error handling around it.
:param command:
:return:
"""
p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
current_app.logger.debug(command)
stdout, stderr = p.communicate()
if p.returncode != 0:
current_app.logger.debug(" ".join(command))
current_app.logger.error(stderr)
raise Exception(stderr)
def create_pkcs12(cert, p12_tmp, key, alias, passphrase):
"""
Creates a pkcs12 formated file.
:param cert:
:param jks_tmp:
:param key:
:param alias:
:param passphrase:
"""
with mktempfile() as key_tmp:
with open(key_tmp, 'w') as f:
f.write(key)
# Create PKCS12 keystore from private key and public certificate
with mktempfile() as cert_tmp:
with open(cert_tmp, 'w') as f:
f.write(cert)
run_process([
"openssl",
"pkcs12",
"-export",
"-name", alias,
"-in", cert_tmp,
"-inkey", key_tmp,
"-out", p12_tmp,
"-password", "pass:{}".format(passphrase)
])
class OpenSSLExportPlugin(ExportPlugin):
title = 'OpenSSL'
slug = 'openssl-export'
description = 'Is a loose interface to openssl and support various formats'
version = openssl.VERSION
author = 'Kevin Glisson'
author_url = 'https://github.com/netflix/lemur'
options = [
{
'name': 'type',
'type': 'select',
'required': True,
'available': ['PKCS12 (.p12)'],
'helpMessage': 'Choose the format you wish to export',
},
{
'name': 'passphrase',
'type': 'str',
'required': False,
'helpMessage': 'If no passphrase is given one will be generated for you, we highly recommend this. Minimum length is 8.',
'validation': '^(?=.*[A-Za-z])(?=.*\d)(?=.*[$@$!%*#?&])[A-Za-z\d$@$!%*#?&]{8,}$'
},
{
'name': 'alias',
'type': 'str',
'required': False,
'helpMessage': 'Enter the alias you wish to use for the keystore.',
}
]
def export(self, body, chain, key, options, **kwargs):
"""
Generates a Java Keystore or Truststore
:param key:
:param chain:
:param body:
:param options:
:param kwargs:
"""
if self.get_option('passphrase', options):
passphrase = self.get_option('passphrase', options)
else:
passphrase = get_psuedo_random_string()
if self.get_option('alias', options):
alias = self.get_option('alias', options)
else:
alias = "blah"
type = self.get_option('type', options)
with mktemppath() as output_tmp:
if type == 'PKCS12 (.p12)':
create_pkcs12(body, output_tmp, key, alias, passphrase)
extension = "p12"
else:
raise Exception("Unable to export, unsupported type: {0}".format(type))
with open(output_tmp, 'rb') as f:
raw = f.read()
return extension, passphrase, raw

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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="">&laquo;</a> <a ng-switch-when="first" ng-click="params.page(page.number)" href=""><span ng-bind="page.number"></span></a> <a ng-switch-when="page" ng-click="params.page(page.number)" href=""><span ng-bind="page.number"></span></a> <a ng-switch-when="more" ng-click="params.page(page.number)" href="">&#8230;</a> <a ng-switch-when="last" ng-click="params.page(page.number)" href=""><span ng-bind="page.number"></span></a> <a ng-switch-when="next" ng-click="params.page(page.number)" href="">&raquo;</a> </li> </ul> </div></div>');
}]);
lemur.service('MomentService', function () {
this.diffMoment = function (start, end) {
if (end !== 'None') {
return moment(end, 'YYYY-MM-DD HH:mm Z').diff(moment(start, 'YYYY-MM-DD HH:mm Z'), 'minutes') + ' minutes';
}
return 'Unknown';
};
this.createMoment = function (date) {
if (date !== 'None') {
return moment(date, 'YYYY-MM-DD HH:mm Z').fromNow();
}
return 'Unknown';
};
});
lemur.controller('datePickerController', function ($scope, $timeout){
$scope.open = function() {
$timeout(function() {
$scope.opened = true;
});
};
});
lemur.service('DefaultService', function (LemurRestangular) {
var DefaultService = this;
DefaultService.get = function () {
return LemurRestangular.all('defaults').customGET().then(function (defaults) {
return defaults;
});
};
});
lemur.factory('LemurRestangular', function (Restangular, $location, $auth) {
return Restangular.withConfig(function (RestangularConfigurer) {
RestangularConfigurer.setBaseUrl('http://localhost:8000/api/1');
RestangularConfigurer.setDefaultHttpFields({withCredentials: true});
RestangularConfigurer.addResponseInterceptor(function (data, operation) {
var extractedData;
// .. to look for getList operations
if (operation === 'getList') {
// .. and handle the data and meta data
extractedData = data.items;
extractedData.total = data.total;
} else {
extractedData = data;
}
return extractedData;
});
RestangularConfigurer.setErrorInterceptor(function(response) {
if (response.status === 400) {
if (response.data.message) {
var data = '';
_.each(response.data.message, function (value, key) {
data = data + ' ' + key + ' ' + value;
});
response.data.message = data;
}
}
});
RestangularConfigurer.addFullRequestInterceptor(function (element, operation, route, url, headers, params) {
// We want to make sure the user is auth'd before any requests
if (!$auth.isAuthenticated()) {
$location.path('/login');
return false;
}
var regExp = /\[([^)]+)\]/;
var s = 'sorting';
var f = 'filter';
var newParams = {};
for (var item in params) {
if (item.indexOf(s) > -1) {
newParams.sortBy = regExp.exec(item)[1];
newParams.sortDir = params[item];
} else if (item.indexOf(f) > -1) {
var key = regExp.exec(item)[1];
newParams.filter = key + ';' + params[item];
} else {
newParams[item] = params[item];
}
}
return { params: newParams };
});
});
});
lemur.run(['$templateCache', function ($templateCache) {
$templateCache.put('ng-table/pager.html', '<div class="ng-cloak ng-table-pager"> <div ng-if="params.settings().counts.length" class="ng-table-counts btn-group pull-left"> <button ng-repeat="count in params.settings().counts" type="button" ng-class="{\'active\':params.count()==count}" ng-click="params.count(count)" class="btn btn-default"> <span ng-bind="count"></span> </button></div><div class="pull-right"><ul style="margin: 0; padding: 0;" class="pagination ng-table-pagination"> <li ng-class="{\'disabled\': !page.active}" ng-repeat="page in pages" ng-switch="page.type"> <a ng-switch-when="prev" ng-click="params.page(page.number)" href="">&laquo;</a> <a ng-switch-when="first" ng-click="params.page(page.number)" href=""><span ng-bind="page.number"></span></a> <a ng-switch-when="page" ng-click="params.page(page.number)" href=""><span ng-bind="page.number"></span></a> <a ng-switch-when="more" ng-click="params.page(page.number)" href="">&#8230;</a> <a ng-switch-when="last" ng-click="params.page(page.number)" href=""><span ng-bind="page.number"></span></a> <a ng-switch-when="next" ng-click="params.page(page.number)" href="">&raquo;</a> </li> </ul> </div></div>');
}]);
}());

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,4 +12,12 @@ angular.module('lemur')
return domains;
});
};
DomainService.updateSensitive = function (domain) {
return domain.put();
};
DomainService.create = function (domain) {
return DomainApi.post(domain);
};
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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