Compare commits

..

47 Commits
0.2.1 ... 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
52 changed files with 814 additions and 850 deletions

View File

@ -1,10 +1,21 @@
Changelog
=========
0.2.2 - `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

View File

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

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,87 +334,16 @@ for those plugins.
This is the root to be used for your CA chain
Authentication
--------------
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"
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.
@ -445,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
@ -472,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.
@ -500,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
@ -515,23 +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.
.. note:: By default Alembic looks for the `migrations` folder in the current working directory.
The migrations folder is located under `<LEMUR_HOME>/lemur/migrations` if you are running the lemur command from any
location besides `<LEMUR_HOME>/lemur` you will need to pass the `-d` flag to specify the absolute file path to the
`migrations` folder.
.. _CommandLineInterface:
Command Line Interface
@ -641,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
===============
@ -697,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)

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

@ -158,6 +158,7 @@ Additional notifications can be created through the UI or API. See :ref:`Creati
.. code-block:: bash
$ 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.
@ -219,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.1"
__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

@ -291,7 +291,7 @@ class Providers(Resource):
'clientId': current_app.config.get("PING_CLIENT_ID"),
'responseType': 'code',
'scope': ['openid', 'email', 'profile', 'address'],
'scopeDelimeter': ' ',
'scopeDelimiter': ' ',
'authorizationEndpoint': current_app.config.get("PING_AUTH_ENDPOINT"),
'requiredUrlParams': ['scope'],
'type': '2.0'

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

@ -189,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
@ -338,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
@ -697,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
@ -851,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
@ -510,23 +515,66 @@ class RotateELBs(Command):
Rotates existing certificates to a new one on an ELB
"""
option_list = (
Option('-c', '--cert-name', dest='cert_name', required=True),
Option('-a', '--account-id', dest='account_id', required=True),
Option('-e', '--elb-list', dest='elb_list', required=True)
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, cert_name, account_id, elb_list):
from lemur.plugins.lemur_aws import elb
arn = "arn:aws:iam::{0}:server-certificate/{1}".format(account_id, cert_name)
def run(self, elb_list, chain_path, cert_name, cert_prefix, description):
for e in open(elb_list, 'r').readlines():
for region in elb.get_all_regions():
if str(region) in e:
name = "-".join(e.split('.')[0].split('-')[:-1])
if name.startswith("internal"):
name = "-".join(name.split("-")[1:])
elb.update_listeners(account_id, str(region), name, [(443, 7001, 'https', arn)], [443])
sys.out.write("[+] Updated {0} to use {1} on 443\n".format(name, cert_name))
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):
@ -743,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():
@ -772,6 +844,7 @@ def main():
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

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

View File

@ -120,10 +120,11 @@ def create_keystore(cert, jks_tmp, key, alias, passphrase):
])
class JavaExportPlugin(ExportPlugin):
title = 'Java'
slug = 'java-export'
description = 'Attempts to generate a JKS keystore or truststore'
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'
@ -131,18 +132,66 @@ class JavaExportPlugin(ExportPlugin):
options = [
{
'name': 'type',
'type': 'select',
'required': True,
'available': ['Truststore (JKS)', 'Keystore (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',
@ -154,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:
@ -173,21 +222,12 @@ class JavaExportPlugin(ExportPlugin):
else:
alias = "blah"
type = self.get_option('type', options)
with mktemppath() as jks_tmp:
if type == 'Truststore (JKS)':
create_truststore(body, chain, jks_tmp, alias, passphrase)
if not key:
raise Exception("Unable to export, no private key found.")
elif type == 'Keystore (JKS)':
if not key:
raise Exception("Unable to export, no private key found.")
create_truststore(body, chain, jks_tmp, alias, passphrase)
create_keystore(body, jks_tmp, key, alias, passphrase)
else:
raise Exception("Unable to export, unsupported type: {0}".format(type))
create_truststore(body, chain, jks_tmp, alias, passphrase)
create_keystore(body, jks_tmp, key, alias, passphrase)
with open(jks_tmp, 'rb') as f:
raw = f.read()

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

@ -50,7 +50,7 @@
_.each(providers, function(provider) {
if ($authProvider.hasOwnProperty(provider.name)) {
$authProvider[provider.name] = provider;
$authProvider[provider.name](provider);
} else {
$authProvider.oauth2(provider);
}

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

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

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

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

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'
]
@ -165,7 +165,8 @@ setup(
'aws_destination = lemur.plugins.lemur_aws.plugin:AWSDestinationPlugin',
'aws_source = lemur.plugins.lemur_aws.plugin:AWSSourcePlugin',
'email_notification = lemur.plugins.lemur_email.plugin:EmailNotificationPlugin',
'java_export = lemur.plugins.lemur_java.plugin:JavaExportPlugin'
'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'
],
},