Compare commits
129 Commits
Author | SHA1 | Date | |
---|---|---|---|
d95b1a0a41 | |||
d9cc4980e8 | |||
5e987fa8b6 | |||
42001be9ec | |||
dc198fec8c | |||
acd47d5ec9 | |||
72e3fb5bfe | |||
b2539b843b | |||
be5dff8472 | |||
76037e8b3a | |||
11f4bd503b | |||
6688b279e7 | |||
1ca38015bc | |||
656269ff17 | |||
bd727b825d | |||
e04c1e7dc9 | |||
615df76dd5 | |||
112c6252d6 | |||
b13370bf0d | |||
88aa5d3fdb | |||
b187d8f836 | |||
1763a1a717 | |||
62b61ed980 | |||
c11034b9bc | |||
58e8fe0bd0 | |||
a0c8765588 | |||
9022059dc6 | |||
7f790be1e4 | |||
93791c999d | |||
5e9f1437ad | |||
f9655213b3 | |||
008d608ec4 | |||
78c8d12ad8 | |||
df0ad4d875 | |||
776e0fcd11 | |||
6ec3bad49a | |||
52f44c3ea6 | |||
941d36ebfe | |||
db8243b4b4 | |||
f919b7360e | |||
8e1b7c0036 | |||
9b0e0fa9c2 | |||
565d7afa92 | |||
c914ba946f | |||
6f9280f64a | |||
8fe460e401 | |||
b9fe359d23 | |||
2c6d494c32 | |||
dbd1279226 | |||
b463fcf61b | |||
82b4f5125d | |||
3f89d6d009 | |||
676f843c92 | |||
c2387dc120 | |||
9a8e1534c0 | |||
dbc4964e94 | |||
00b263f345 | |||
62d03b0d41 | |||
b5a4b293a9 | |||
bfcfdb83a7 | |||
4ccbfa8164 | |||
675d10c8a6 | |||
2cde7336dc | |||
169490dbec | |||
3ceb297276 | |||
12633bfed6 | |||
5958bac2a2 | |||
37f2d5b8b0 | |||
47891d2953 | |||
af68571f4e | |||
d0ec925ca3 | |||
939194158a | |||
576265e09c | |||
dfaf45344c | |||
6c378957e9 | |||
e8f9bc80a0 | |||
a30b8b21e4 | |||
12204852aa | |||
edba980b56 | |||
ba666ddbfa | |||
35f9f59c57 | |||
ac1f493338 | |||
1c3c70d460 | |||
e8e7bdf9e0 | |||
d263e0e60c | |||
028d86c0bb | |||
f8b6830013 | |||
49a40c50e8 | |||
2ba48995fe | |||
3cc8ade6d8 | |||
39c9a0a299 | |||
3ad317fb6d | |||
bd46440d12 | |||
f3a28814ae | |||
9f8f64b9ec | |||
1e524a49c0 | |||
467c276fca | |||
f610e39418 | |||
27d977b2fa | |||
b36e72bfcc | |||
e49701228d | |||
48f8b33d7d | |||
d87ace8c89 | |||
b1326d4145 | |||
7c2862c958 | |||
0a4f5ad64d | |||
c617a11c55 | |||
053167965a | |||
a7ac45b937 | |||
5482bbf4bd | |||
0a58e106b5 | |||
a1395a5808 | |||
a0d50ef03a | |||
685e2c8b6d | |||
c6d9a20fe5 | |||
4a952d867b | |||
cb4cf43fcf | |||
1bce7a832b | |||
574234f70f | |||
42e5470dd0 | |||
8199365324 | |||
86c92eb31e | |||
d9fd952c03 | |||
967c7ded8d | |||
a4bf847b56 | |||
d6917155e8 | |||
3f024c1ef4 | |||
96d253f0f9 | |||
9b166fb9a9 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
||||
/.cache
|
||||
.coverage
|
||||
.tox
|
||||
.DS_Store
|
||||
|
@ -8,7 +8,7 @@
|
||||
"eqeqeq": true,
|
||||
"immed": true,
|
||||
"indent": 2,
|
||||
"latedef": true,
|
||||
"latedef": false,
|
||||
"newcap": false,
|
||||
"noarg": true,
|
||||
"quotmark": "single",
|
||||
@ -22,6 +22,8 @@
|
||||
"angular": false,
|
||||
"moment": false,
|
||||
"toaster": false,
|
||||
"d3": false,
|
||||
"self": false,
|
||||
"_": false
|
||||
}
|
||||
}
|
||||
|
@ -9,10 +9,8 @@ matrix:
|
||||
include:
|
||||
- python: "2.7"
|
||||
env: TOXENV=py27
|
||||
- python: "3.3"
|
||||
env: TOXENV=py33
|
||||
- python: "3.4"
|
||||
env: TOXENV=py34
|
||||
- python: "3.5"
|
||||
env: TOXENV=py35
|
||||
|
||||
cache:
|
||||
directories:
|
||||
|
@ -1,10 +1,77 @@
|
||||
Changelog
|
||||
=========
|
||||
|
||||
0.2.2 - `master` _
|
||||
0.3.1 - `master`
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
.. note:: This version is not yet released and is under active development
|
||||
|
||||
0.3.0 - `2016-06-06`
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
This is quite a large upgrade, it is highly advised you backup your database before attempting to upgrade as this release
|
||||
requires the migration of database structure as well as data.
|
||||
|
||||
|
||||
Upgrading
|
||||
---------
|
||||
|
||||
Please follow the `documentation <https://lemur.readthedocs.io/en/latest/administration.html#upgrading-lemur>`_ to upgrade Lemur.
|
||||
|
||||
|
||||
Source Plugin Owners
|
||||
--------------------
|
||||
|
||||
The dictionary returned from a source plugin has changed keys from `public_certificate` to `body` and `intermediate_certificate` to chain.
|
||||
|
||||
|
||||
Issuer Plugin Owners
|
||||
--------------------
|
||||
|
||||
This release may break your plugins, the keys in `issuer_options` have been changed from `camelCase` to `under_score`.
|
||||
This change was made to break a undue reliance on downstream options maintains a more pythonic naming convention. Renaming
|
||||
these keys should be fairly trivial, additionally pull requests have been submitted to affected plugins to help ease the transition.
|
||||
|
||||
.. note:: This change only affects issuer plugins and does not affect any other types of plugins.
|
||||
|
||||
|
||||
* Closed `#63 <https://github.com/Netflix/lemur/issues/63>`_ - Validates all endpoints with Marshmallow schemas, this allows for
|
||||
stricter input validation and better error messages when validation fails.
|
||||
* Closed `#146 <https://github.com/Netflix/lemur/issues/146>`_ - Moved authority type to first pane of authority creation wizard.
|
||||
* Closed `#147 <https://github.com/Netflix/lemur/issues/147>`_ - Added and refactored the relationship between authorities and their
|
||||
root certificates. Displays the certificates (and chains) next the the authority in question.
|
||||
* Closed `#199 <https://github.com/Netflix/lemur/issues/199>`_ - Ensures that the dates submitted to Lemur during authority and
|
||||
certificate creation are actually dates.
|
||||
* Closed `#230 <https://github.com/Netflix/lemur/issues/230>`_ - Migrated authority dropdown to a ui-select based dropdown, this
|
||||
should be easier to determine what authorities are available and when an authority has actually been selected.
|
||||
* Closed `#254 <https://github.com/Netflix/lemur/issues/254>`_ - Forces certificate names to be generally unique. If a certificate name
|
||||
(generated or otherwise) is found to be a duplicate we increment by appending a counter.
|
||||
* Closed `#254 <https://github.com/Netflix/lemur/issues/275>`_ - Switched to using Fernet generated passphrases for exported items.
|
||||
These are more sounds that pseudo random passphrases generated before and have the nice property of being in base64.
|
||||
* Closed `#278 <https://github.com/Netflix/lemur/issues/278>`_ - Added ability to specify a custom name to certificate creation, previously
|
||||
this was only available in the certificate import wizard.
|
||||
* Closed `#281 <https://github.com/Netflix/lemur/issues/281>`_ - Fixed an issue where notifications could not be removed from a certificate
|
||||
via the UI.
|
||||
* Closed `#289 <https://github.com/Netflix/lemur/issues/289>`_ - Fixed and issue where intermediates were not being properly exported.
|
||||
* Closed `#315 <https://github.com/Netflix/lemur/issues/315>`_ - Made how roles are associated with certificates and authorities much more
|
||||
explict, including adding the ability to add roles directly to certificates and authorities on creation.
|
||||
|
||||
|
||||
|
||||
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
|
||||
@ -19,7 +86,7 @@ Changelog
|
||||
|
||||
|
||||
0.2.0 - 2015-12-02
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* Closed #120 - Error messages not displaying long enough
|
||||
* Closed #121 - Certificate create form should not be valid until a Certificate Authority object is available
|
||||
@ -35,7 +102,7 @@ Changelog
|
||||
|
||||
|
||||
0.1.5 - 2015-10-26
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* **SECURITY ISSUE**: Switched from use a AES static key to Fernet encryption.
|
||||
Affects all versions prior to 0.1.5. If upgrading this will require a data migration.
|
||||
|
1
LICENSE
1
LICENSE
@ -1,4 +1,3 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
@ -5,10 +5,6 @@ Lemur
|
||||
:alt: Join the chat at https://gitter.im/Netflix/lemur
|
||||
:target: https://gitter.im/Netflix/lemur?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
|
||||
|
||||
.. image:: https://img.shields.io/pypi/v/lemur.svg
|
||||
:target: https://pypi.python.org/pypi/lemur/
|
||||
:alt: Latest Version
|
||||
|
||||
.. image:: https://readthedocs.org/projects/lemur/badge/?version=latest
|
||||
:target: https://lemur.readthedocs.org
|
||||
:alt: Latest Docs
|
||||
@ -20,10 +16,6 @@ Lemur
|
||||
:target: https://requires.io/github/Netflix/lemur/requirements/?branch=master
|
||||
:alt: Requirements Status
|
||||
|
||||
.. image:: https://badge.waffle.io/Netflix/lemur.png?label=ready&title=Ready
|
||||
:target: https://waffle.io/Netflix/lemur
|
||||
:alt: 'Stories in Ready'
|
||||
|
||||
Lemur manages TLS certificate creation. While not able to issue certificates itself, Lemur acts as a broker between CAs
|
||||
and environments providing a central portal for developers to issue TLS certificates with 'sane' defaults.
|
||||
|
||||
|
67
bower.json
67
bower.json
@ -6,43 +6,46 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"angular": "1.3",
|
||||
"json3": "~3.3",
|
||||
"es5-shim": "~4.0",
|
||||
"jquery": "~2.1",
|
||||
"angular-resource": "1.2.15",
|
||||
"angular-cookies": "1.2.15",
|
||||
"angular-sanitize": "1.2.15",
|
||||
"angular-route": "1.2.15",
|
||||
"angular-strap": "~2.0.2",
|
||||
"restangular": "~1.4.0",
|
||||
"ng-table": "~0.5.4",
|
||||
"ngAnimate": "*",
|
||||
"moment": "~2.6.0",
|
||||
"angular-animate": "~1.4.0",
|
||||
"angular-loading-bar": "~0.6.0",
|
||||
"fontawesome": "~4.2.0",
|
||||
"jquery": "~2.2.0",
|
||||
"angular-wizard": "~0.4.0",
|
||||
"bootswatch": "3.3.1+2",
|
||||
"angular": "1.4.9",
|
||||
"json3": "~3.3",
|
||||
"es5-shim": "~4.5.0",
|
||||
"bootstrap": "~3.3.6",
|
||||
"angular-bootstrap": "~1.1.1",
|
||||
"angular-animate": "~1.4.9",
|
||||
"restangular": "~1.5.1",
|
||||
"ng-table": "~0.8.3",
|
||||
"moment": "~2.11.1",
|
||||
"angular-loading-bar": "~0.8.0",
|
||||
"angular-moment": "~0.10.3",
|
||||
"moment-range": "~2.1.0",
|
||||
"angular-spinkit": "~0.3.3",
|
||||
"angular-bootstrap": "~0.12.0",
|
||||
"angular-ui-switch": "~0.1.0",
|
||||
"angular-chart.js": "~0.7.1",
|
||||
"satellizer": "~0.9.4",
|
||||
"angularjs-toaster": "~0.4.14",
|
||||
"ngletteravatar": "~3.0.1",
|
||||
"angular-clipboard": "~1.3.0",
|
||||
"angularjs-toaster": "~1.0.0",
|
||||
"angular-chart.js": "~0.8.8",
|
||||
"ngletteravatar": "~4.0.0",
|
||||
"bootswatch": "~3.3.6",
|
||||
"fontawesome": "~4.5.0",
|
||||
"satellizer": "~0.13.4",
|
||||
"angular-ui-router": "~0.2.15",
|
||||
"angular-clipboard": "~1.1.1",
|
||||
"angular-file-saver": "~1.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"angular-mocks": "~1.3",
|
||||
"angular-scenario": "~1.3",
|
||||
"ngletteravatar": "~3.0.1"
|
||||
"font-awesome": "~4.5.0",
|
||||
"lodash": "~4.0.1",
|
||||
"underscore": "~1.8.3",
|
||||
"angular-smart-table": "~2.1.6",
|
||||
"angular-strap": ">= 2.2.2",
|
||||
"angular-underscore": "^0.5.0",
|
||||
"angular-translate": "^2.9.0",
|
||||
"angular-ui-switch": "~0.1.0",
|
||||
"angular-sanitize": "^1.5.0",
|
||||
"angular-file-saver": "~1.0.1",
|
||||
"angular-ui-select": "~0.17.1",
|
||||
"d3": "^3.5.17"
|
||||
},
|
||||
"resolutions": {
|
||||
"bootstrap": "~3.3.1",
|
||||
"angular": "1.3"
|
||||
"moment": ">=2.8.0 <2.11.0",
|
||||
"lodash": ">=1.3.0 <2.5.0",
|
||||
"angular": "1.4.9"
|
||||
},
|
||||
"ignore": [
|
||||
"**/.*",
|
||||
|
@ -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
|
||||
@ -601,24 +588,33 @@ All commands default to `~/.lemur/lemur.conf.py` if a configuration is not speci
|
||||
|
||||
Traverses every certificate that Lemur is aware of and attempts to understand its validity.
|
||||
It utilizes both OCSP and CRL. If Lemur is unable to come to a conclusion about a certificates
|
||||
validity its status is marked 'unknown'
|
||||
validity its status is marked 'unknown'.
|
||||
|
||||
|
||||
.. data:: sync
|
||||
|
||||
Sync attempts to discover certificates in the environment that were not created by Lemur. If you wish to only sync
|
||||
a few sources you can pass a comma delimited list of sources to sync
|
||||
a few sources you can pass a comma delimited list of sources to sync.
|
||||
|
||||
::
|
||||
|
||||
lemur sync source1,source2
|
||||
lemur sync -s source1,source2
|
||||
|
||||
|
||||
Additionally you can also list the available sources that Lemur can sync
|
||||
Additionally you can also list the available sources that Lemur can sync.
|
||||
|
||||
::
|
||||
|
||||
lemur sync -list
|
||||
lemur sync
|
||||
|
||||
|
||||
.. data:: notify
|
||||
|
||||
Will traverse all current notifications and see if any of them need to be triggered.
|
||||
|
||||
::
|
||||
|
||||
lemur notify
|
||||
|
||||
|
||||
Sub-commands
|
||||
@ -641,32 +637,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 +667,61 @@ After you have the latest version of the Lemur code base you must run any needed
|
||||
|
||||
|
||||
This will ensure that any needed tables or columns are created or destroyed.
|
||||
|
||||
.. note::
|
||||
Internally, this uses `Alembic <https://alembic.readthedocs.org/en/latest/>`_ to manage database migrations.
|
||||
|
||||
.. note::
|
||||
By default Alembic looks for the `migrations` folder in the current working directory.The migrations folder is
|
||||
located under `<LEMUR_HOME>/lemur/migrations` if you are running the lemur command from any location besides
|
||||
`<LEMUR_HOME>/lemur` you will need to pass the `-d` flag to specify the absolute file path to the `migrations` folder.
|
||||
|
||||
Plugins
|
||||
=======
|
||||
|
||||
There are several interfaces currently available to extend Lemur. These are a work in
|
||||
progress and the API is not frozen.
|
||||
|
||||
Bundled Plugins
|
||||
---------------
|
||||
|
||||
Lemur includes several plugins by default. Including extensive support for AWS, VeriSign/Symantec and CloudCA services.
|
||||
|
||||
3rd Party Extensions
|
||||
--------------------
|
||||
|
||||
The following extensions are available and maintained by members of the Lemur community:
|
||||
|
||||
Have an extension that should be listed here? Submit a `pull request <https://github.com/netflix/lemur>`_ and we'll
|
||||
get it added.
|
||||
|
||||
Want to create your own extension? See :doc:`../developer/plugins/index` to get started.
|
||||
|
||||
|
||||
Identity and Access Management
|
||||
==============================
|
||||
|
||||
Lemur uses a Role Based Access Control (RBAC) mechanism to control which users have access to which resources. When a
|
||||
user is first created in Lemur they can be assigned one or more roles. These roles are typically dynamically created
|
||||
depending on a external identity provider (Google, LDAP, etc.,) or are hardcoded within Lemur and associated with special
|
||||
meaning.
|
||||
|
||||
Within Lemur there are three main permissions: AdminPermission, CreatorPermission, OwnerPermission. Sub-permissions such
|
||||
as ViewPrivateKeyPermission are compositions of these three main Permissions.
|
||||
|
||||
Lets take a look at how these permissions are used:
|
||||
|
||||
Each `Authority` has a set of roles associated with it. If a user is also associated with the same roles
|
||||
that the `Authority` is associated with, Lemur allows that user to user/view/update that `Authority`.
|
||||
|
||||
This RBAC is also used when determining which users can access which certificate private key. Lemur's current permission
|
||||
structure is setup such that if the user is a `Creator` or `Owner` of a given certificate they are allow to view that
|
||||
private key. Owners can also be a role name, such that any user with the same role as owner will be allowed to view the
|
||||
private key information.
|
||||
|
||||
These permissions are applied to the user upon login and refreshed on every request.
|
||||
|
||||
.. seealso::
|
||||
|
||||
`Flask-Principal <https://pythonhosted.org/Flask-Principal>`_
|
||||
|
10
docs/conf.py
10
docs/conf.py
@ -101,9 +101,13 @@ pygments_style = 'sphinx'
|
||||
|
||||
# -- Options for HTML output ----------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
html_theme = 'default'
|
||||
# on_rtd is whether we are on readthedocs.org, this line of code grabbed from docs.readthedocs.org
|
||||
on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
|
||||
|
||||
if not on_rtd: # only import and set the theme if we're building docs locally
|
||||
import sphinx_rtd_theme
|
||||
html_theme = 'sphinx_rtd_theme'
|
||||
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
|
@ -144,6 +144,17 @@ If you've made changes and need to compile them by hand for any reason, you can
|
||||
|
||||
The minified and processed files should be committed alongside the unprocessed changes.
|
||||
|
||||
It's also important to note that Lemur's frontend and API are not tied together. The API does not serve any of the static assets, we rely on nginx or some other file server to server all of the static assets.
|
||||
During development that means we need an additional server to serve those static files for the GUI.
|
||||
|
||||
This is accomplished with a Gulp task:
|
||||
|
||||
::
|
||||
|
||||
./node_modules/.bin/gulp serve
|
||||
|
||||
The gulp task compiles all the JS/CSS/HTML files and opens the Lemur welcome page in your default browsers. Additionally any changes to made to the JS/CSS/HTML with be reloaded in your browsers.
|
||||
|
||||
Developing with Flask
|
||||
----------------------
|
||||
|
||||
@ -180,19 +191,92 @@ You can see a list of open pull requests (pending changes) by visiting https://g
|
||||
|
||||
Pull requests should be against **master** and pass all TravisCI checks
|
||||
|
||||
Plugins
|
||||
=======
|
||||
|
||||
Writing a Plugin
|
||||
================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:maxdepth: 2
|
||||
|
||||
plugins/index
|
||||
|
||||
|
||||
REST API
|
||||
========
|
||||
|
||||
Lemur's front end is entirely API driven. Any action that you can accomplish via the UI can also be accomplished by the
|
||||
UI. The following is documents and provides examples on how to make requests to the Lemur API.
|
||||
|
||||
Authentication
|
||||
--------------
|
||||
|
||||
.. automodule:: lemur.auth.views
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Destinations
|
||||
------------
|
||||
|
||||
.. automodule:: lemur.destinations.views
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Notifications
|
||||
-------------
|
||||
|
||||
.. automodule:: lemur.notifications.views
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Users
|
||||
-----
|
||||
|
||||
.. automodule:: lemur.users.views
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Roles
|
||||
-----
|
||||
|
||||
.. automodule:: lemur.roles.views
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Certificates
|
||||
------------
|
||||
|
||||
.. automodule:: lemur.certificates.views
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Authorities
|
||||
-----------
|
||||
|
||||
.. automodule:: lemur.authorities.views
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Domains
|
||||
-------
|
||||
|
||||
.. automodule:: lemur.domains.views
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
Internals
|
||||
=========
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:maxdepth: 2
|
||||
|
||||
internals/lemur
|
||||
|
||||
|
@ -27,6 +27,5 @@ Subpackages
|
||||
lemur.plugins.base
|
||||
lemur.plugins.bases
|
||||
lemur.plugins.lemur_aws
|
||||
lemur.plugins.lemur_cloudca
|
||||
lemur.plugins.lemur_email
|
||||
lemur.plugins.lemur_verisign
|
||||
|
@ -96,5 +96,4 @@ Subpackages
|
||||
lemur.notifications
|
||||
lemur.plugins
|
||||
lemur.roles
|
||||
lemur.status
|
||||
lemur.users
|
||||
|
@ -1,11 +0,0 @@
|
||||
status Package
|
||||
==============
|
||||
|
||||
:mod:`views` Module
|
||||
-------------------
|
||||
|
||||
.. automodule:: lemur.status.views
|
||||
:noindex:
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
@ -1,6 +1,3 @@
|
||||
Writing a Plugin
|
||||
================
|
||||
|
||||
Several interfaces exist for extending Lemur:
|
||||
|
||||
* Issuer (lemur.plugins.base.issuer)
|
||||
@ -214,8 +211,8 @@ 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
|
||||
.. 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
|
||||
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.
|
||||
|
||||
@ -226,8 +223,8 @@ The `SourcePlugin` object requires implementation of one function::
|
||||
# request.get("some source of certificates")
|
||||
|
||||
|
||||
.. Note::
|
||||
Often times to facilitate code re-use it makes sense put source and destination plugins into one package.
|
||||
.. note::
|
||||
Often times to facilitate code re-use it makes sense put source and destination plugins into one package.
|
||||
|
||||
|
||||
Export
|
||||
@ -247,9 +244,8 @@ The `ExportPlugin` object requires the implementation of one function::
|
||||
# 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.
|
||||
.. note::
|
||||
Support of various formats sometimes relies on external tools system calls. Always be mindful of sanitizing any input to these calls.
|
||||
|
||||
|
||||
Testing
|
||||
|
@ -1,66 +0,0 @@
|
||||
Lemur's front end is entirely API driven. Any action that you can accomplish via the UI can also be accomplished by the
|
||||
UI. The following is documents and provides examples on how to make requests to the Lemur API.
|
||||
|
||||
Authentication
|
||||
--------------
|
||||
|
||||
.. automodule:: lemur.auth.views
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Destinations
|
||||
------------
|
||||
|
||||
.. automodule:: lemur.destinations.views
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Notifications
|
||||
-------------
|
||||
|
||||
.. automodule:: lemur.notifications.views
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Users
|
||||
-----
|
||||
|
||||
.. automodule:: lemur.users.views
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Roles
|
||||
-----
|
||||
|
||||
.. automodule:: lemur.roles.views
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Certificates
|
||||
------------
|
||||
|
||||
.. automodule:: lemur.certificates.views
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Authorities
|
||||
-----------
|
||||
|
||||
.. automodule:: lemur.authorities.views
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Domains
|
||||
-------
|
||||
|
||||
.. automodule:: lemur.domains.views
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
@ -27,8 +27,7 @@ Administration
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
administration/index
|
||||
plugins/index
|
||||
administration
|
||||
|
||||
Developers
|
||||
----------
|
||||
@ -38,14 +37,21 @@ Developers
|
||||
|
||||
developer/index
|
||||
|
||||
|
||||
REST API
|
||||
Security
|
||||
--------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
developer/rest
|
||||
security
|
||||
|
||||
Doing a Release
|
||||
---------------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
doing-a-release
|
||||
|
||||
FAQ
|
||||
----
|
||||
|
@ -1,20 +0,0 @@
|
||||
Plugins
|
||||
=======
|
||||
|
||||
There are several interfaces currently available to extend Lemur. These are a work in
|
||||
progress and the API is not frozen.
|
||||
|
||||
Bundled Plugins
|
||||
---------------
|
||||
|
||||
Lemur includes several plugins by default. Including extensive support for AWS, VeriSign/Symantec and CloudCA services.
|
||||
|
||||
3rd Party Extensions
|
||||
--------------------
|
||||
|
||||
The following extensions are available and maintained by members of the Lemur community:
|
||||
|
||||
Have an extension that should be listed here? Submit a `pull request <https://github.com/netflix/lemur>`_ and we'll
|
||||
get it added.
|
||||
|
||||
Want to create your own extension? See :doc:`../developer/plugins/index` to get started.
|
@ -110,7 +110,7 @@ You can make some adjustments to get a better user experience::
|
||||
error_log /var/log/nginx/log/lemur.error.log;
|
||||
|
||||
location /api {
|
||||
proxy_pass http://127.0.0.1:5000;
|
||||
proxy_pass http://127.0.0.1:8000;
|
||||
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
|
||||
proxy_redirect off;
|
||||
proxy_buffering off;
|
||||
@ -176,7 +176,7 @@ sensitive nature of Lemur and what it controls makes this essential. This is a s
|
||||
resolver <IP DNS resolver>;
|
||||
|
||||
location /api {
|
||||
proxy_pass http://127.0.0.1:5000;
|
||||
proxy_pass http://127.0.0.1:8000;
|
||||
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
|
||||
proxy_redirect off;
|
||||
proxy_buffering off;
|
||||
@ -295,3 +295,25 @@ Then you can manage the process by running::
|
||||
It will start a shell from which you can start/stop/restart the service.
|
||||
|
||||
You can read all errors that might occur from /tmp/lemur.log.
|
||||
|
||||
|
||||
Periodic Tasks
|
||||
==============
|
||||
|
||||
Lemur contains a few tasks that are run and scheduled basis, currently the recommend way to run these tasks is to create
|
||||
a cron job that runs the commands.
|
||||
|
||||
There are currently three commands that could/should be run on a periodic basis:
|
||||
|
||||
- `notify`
|
||||
- `check_revoked`
|
||||
- `sync`
|
||||
|
||||
How often you run these commands is largely up to the user. `notify` and `check_revoked` are typically run at least once a day.
|
||||
`sync` is typically run every 15 minutes.
|
||||
|
||||
Example cron entries::
|
||||
|
||||
0 22 * * * lemuruser export LEMUR_CONF=/Users/me/.lemur/lemur.conf.py; /www/lemur/bin/lemur notify
|
||||
*/15 * * * * lemuruser export LEMUR_CONF=/Users/me/.lemur/lemur.conf.py; /www/lemur/bin/lemur sync -s all
|
||||
0 22 * * * lemuruser export LEMUR_CONF=/Users/me/.lemur/lemur.conf.py; /www/lemur/bin/lemur check_revoked
|
@ -27,7 +27,7 @@ If installing Lemur on a bare Ubuntu OS you will need to grab the following pack
|
||||
.. code-block:: bash
|
||||
|
||||
$ sudo apt-get update
|
||||
$ sudo apt-get install install nodejs-legacy python-pip python-dev libpq-dev build-essential libssl-dev libffi-dev nginx git supervisor npm postgresql
|
||||
$ sudo apt-get install nodejs-legacy python-pip python-dev libpq-dev build-essential libssl-dev libffi-dev nginx git supervisor npm postgresql
|
||||
|
||||
.. note:: PostgreSQL is only required if your database is going to be on the same host as the webserver. npm is needed if you're installing Lemur from the source (e.g., from git).
|
||||
|
||||
@ -118,7 +118,7 @@ First, set a password for the postgres user. For this guide, we will use ``lemu
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ sudo -u postgres psql postgres
|
||||
$ sudo -u postgres -i
|
||||
# \password postgres
|
||||
Enter new password: lemur
|
||||
Enter it again: lemur
|
||||
@ -133,17 +133,8 @@ Next, we will create our new database:
|
||||
|
||||
.. _InitializingLemur:
|
||||
|
||||
Set a password for lemur user inside Postgres:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ sudo -u postgres psql postgres
|
||||
\password lemur
|
||||
Enter new password: lemur
|
||||
Enter it again: lemur
|
||||
|
||||
Again, enter CTRL-D to exit the Postgres shell.
|
||||
|
||||
.. note::
|
||||
For this guide we assume you will use the `postgres` user to connect to your database, when deploying to a VM or container this is often all you will need. If you have a shared database it is recommend you give Lemur it's own user.
|
||||
|
||||
Initializing Lemur
|
||||
------------------
|
||||
@ -158,8 +149,10 @@ 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.
|
||||
|
||||
|
||||
@ -177,7 +170,7 @@ You'll use the builtin ``HttpProxyModule`` within Nginx to handle proxying. Edi
|
||||
::
|
||||
|
||||
location /api {
|
||||
proxy_pass http://127.0.0.1:5000;
|
||||
proxy_pass http://127.0.0.1:8000;
|
||||
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
|
||||
proxy_redirect off;
|
||||
proxy_buffering off;
|
||||
@ -219,7 +212,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
|
||||
@ -250,13 +243,14 @@ See :ref:`Using Supervisor <UsingSupervisor>` for more details on using Supervis
|
||||
Syncing
|
||||
-------
|
||||
|
||||
Lemur uses periodic sync tasks to make sure it is up-to-date with its environment. As always, things can change outside of Lemur, but we do our best to reconcile those changes, for example, using Cron:
|
||||
Lemur uses periodic sync tasks to make sure it is up-to-date with its environment. Things change outside of Lemur we do our best to reconcile those changes. The recommended method is to use CRON:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ crontab -e
|
||||
* 3 * * * lemur sync --all
|
||||
* 3 * * * lemur check_revoked
|
||||
*/15 * * * * lemur sync -s all
|
||||
0 22 * * * lemur check_revoked
|
||||
0 22 * * * lemur notify
|
||||
|
||||
|
||||
Additional Utilities
|
||||
|
@ -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
|
||||
|
@ -81,6 +81,7 @@ gulp.task('dev:styles', function () {
|
||||
'bower_components/angular-wizard/dist/angular-wizard.css',
|
||||
'bower_components/ng-table/ng-table.css',
|
||||
'bower_components/angularjs-toaster/toaster.css',
|
||||
'bower_components/angular-ui-select/dist/select.css',
|
||||
'lemur/static/app/styles/lemur.css'
|
||||
];
|
||||
|
||||
|
@ -9,7 +9,7 @@ __title__ = "lemur"
|
||||
__summary__ = ("Certificate management and orchestration service")
|
||||
__uri__ = "https://github.com/Netflix/lemur"
|
||||
|
||||
__version__ = "0.2.1"
|
||||
__version__ = "0.3.0"
|
||||
|
||||
__author__ = "The Lemur developers"
|
||||
__email__ = "security@netflix.com"
|
||||
|
@ -11,6 +11,7 @@
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
from lemur import factory
|
||||
from lemur.extensions import metrics
|
||||
|
||||
from lemur.users.views import mod as users_bp
|
||||
from lemur.roles.views import mod as roles_bp
|
||||
@ -70,8 +71,17 @@ def configure_hook(app):
|
||||
def after(response):
|
||||
return response
|
||||
|
||||
@app.errorhandler(500)
|
||||
def internal_error(error):
|
||||
metrics.send('500_status_code', 'counter', 1)
|
||||
|
||||
@app.errorhandler(400)
|
||||
def response_error(error):
|
||||
metrics.send('400_status_code', 'counter', 1)
|
||||
|
||||
@app.errorhandler(PermissionDenied)
|
||||
def handle_invalid_usage(error):
|
||||
def permission_denied_error(error):
|
||||
metrics.send('403_status_code', 'counter', 1)
|
||||
response = {'message': 'You are not allow to access this resource'}
|
||||
response.status_code = 403
|
||||
return response
|
||||
|
@ -18,6 +18,14 @@ admin_permission = Permission(RoleNeed('admin'))
|
||||
CertificateCreator = namedtuple('certificate', ['method', 'value'])
|
||||
CertificateCreatorNeed = partial(CertificateCreator, 'key')
|
||||
|
||||
CertificateOwner = namedtuple('certificate', ['method', 'value'])
|
||||
CertificateOwnerNeed = partial(CertificateOwner, 'role')
|
||||
|
||||
|
||||
class SensitiveDomainPermission(Permission):
|
||||
def __init__(self):
|
||||
super(SensitiveDomainPermission, self).__init__(RoleNeed('admin'))
|
||||
|
||||
|
||||
class ViewKeyPermission(Permission):
|
||||
def __init__(self, certificate_id, owner):
|
||||
@ -31,6 +39,15 @@ class UpdateCertificatePermission(Permission):
|
||||
super(UpdateCertificatePermission, self).__init__(c_need, RoleNeed(owner), RoleNeed('admin'))
|
||||
|
||||
|
||||
class CertificatePermission(Permission):
|
||||
def __init__(self, certificate_id, roles):
|
||||
needs = [RoleNeed('admin'), CertificateCreatorNeed(certificate_id)]
|
||||
for r in roles:
|
||||
needs.append(CertificateOwnerNeed(str(r)))
|
||||
|
||||
super(CertificatePermission, self).__init__(*needs)
|
||||
|
||||
|
||||
RoleUser = namedtuple('role', ['method', 'value'])
|
||||
ViewRoleCredentialsNeed = partial(RoleUser, 'roleView')
|
||||
|
||||
|
@ -165,7 +165,7 @@ def on_identity_loaded(sender, identity):
|
||||
# identity with the roles that the user provides
|
||||
if hasattr(user, 'roles'):
|
||||
for role in user.roles:
|
||||
identity.provides.add(ViewRoleCredentialsNeed(role.id))
|
||||
identity.provides.add(ViewRoleCredentialsNeed(role.name))
|
||||
identity.provides.add(RoleNeed(role.name))
|
||||
|
||||
# apply ownership for authorities
|
||||
|
@ -9,11 +9,12 @@ import jwt
|
||||
import base64
|
||||
import requests
|
||||
|
||||
from flask import g, Blueprint, current_app
|
||||
from flask import Blueprint, current_app
|
||||
|
||||
from flask.ext.restful import reqparse, Resource, Api
|
||||
from flask.ext.principal import Identity, identity_changed
|
||||
|
||||
from lemur.extensions import metrics
|
||||
from lemur.common.utils import get_psuedo_random_string
|
||||
|
||||
from lemur.users import service as user_service
|
||||
@ -96,13 +97,12 @@ class Login(Resource):
|
||||
# Tell Flask-Principal the identity changed
|
||||
identity_changed.send(current_app._get_current_object(),
|
||||
identity=Identity(user.id))
|
||||
metrics.send('successful_login', 'counter', 1)
|
||||
return dict(token=create_token(user))
|
||||
|
||||
metrics.send('invalid_login', 'counter', 1)
|
||||
return dict(message='The supplied credentials are invalid'), 401
|
||||
|
||||
def get(self):
|
||||
return {'username': g.current_user.username, 'roles': [r.name for r in g.current_user.roles]}
|
||||
|
||||
|
||||
class Ping(Resource):
|
||||
"""
|
||||
@ -179,6 +179,7 @@ class Ping(Resource):
|
||||
profile = r.json()
|
||||
|
||||
user = user_service.get_by_email(profile['email'])
|
||||
metrics.send('successful_login', 'counter', 1)
|
||||
|
||||
# update their google 'roles'
|
||||
roles = []
|
||||
@ -266,6 +267,7 @@ class Google(Resource):
|
||||
user = user_service.get_by_email(profile['email'])
|
||||
|
||||
if user:
|
||||
metrics.send('successful_login', 'counter', 1)
|
||||
return dict(token=create_token(user))
|
||||
|
||||
|
||||
@ -291,7 +293,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'
|
||||
|
@ -6,53 +6,35 @@
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy import Column, Integer, String, Text, func, ForeignKey, DateTime, PassiveDefault, Boolean
|
||||
from sqlalchemy.dialects.postgresql import JSON
|
||||
|
||||
from lemur.database import db
|
||||
from lemur.certificates.models import get_cn, get_not_after, get_not_before
|
||||
from lemur.models import roles_authorities
|
||||
|
||||
|
||||
class Authority(db.Model):
|
||||
__tablename__ = 'authorities'
|
||||
id = Column(Integer, primary_key=True)
|
||||
owner = Column(String(128))
|
||||
owner = Column(String(128), nullable=False)
|
||||
name = Column(String(128), unique=True)
|
||||
body = Column(Text())
|
||||
chain = Column(Text())
|
||||
bits = Column(Integer())
|
||||
cn = Column(String(128))
|
||||
not_before = Column(DateTime)
|
||||
not_after = Column(DateTime)
|
||||
active = Column(Boolean, default=True)
|
||||
date_created = Column(DateTime, PassiveDefault(func.now()), nullable=False)
|
||||
plugin_name = Column(String(64))
|
||||
description = Column(Text)
|
||||
options = Column(JSON)
|
||||
roles = relationship('Role', backref=db.backref('authority'), lazy='dynamic')
|
||||
date_created = Column(DateTime, PassiveDefault(func.now()), nullable=False)
|
||||
roles = relationship('Role', secondary=roles_authorities, passive_deletes=True, backref=db.backref('authority'), lazy='dynamic')
|
||||
user_id = Column(Integer, ForeignKey('users.id'))
|
||||
certificates = relationship("Certificate", backref='authority')
|
||||
authority_certificate = relationship("Certificate", backref='root_authority', uselist=False, foreign_keys='Certificate.root_authority_id')
|
||||
certificates = relationship("Certificate", backref='authority', foreign_keys='Certificate.authority_id')
|
||||
|
||||
def __init__(self, name, owner, plugin_name, body, roles=None, chain=None, description=None):
|
||||
self.name = name
|
||||
self.body = body
|
||||
self.chain = chain
|
||||
self.owner = owner
|
||||
self.plugin_name = plugin_name
|
||||
cert = x509.load_pem_x509_certificate(str(body), default_backend())
|
||||
self.cn = get_cn(cert)
|
||||
self.not_before = get_not_before(cert)
|
||||
self.not_after = get_not_after(cert)
|
||||
self.roles = roles
|
||||
self.description = description
|
||||
|
||||
def as_dict(self):
|
||||
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
|
||||
|
||||
def serialize(self):
|
||||
blob = self.as_dict()
|
||||
return blob
|
||||
def __init__(self, **kwargs):
|
||||
self.owner = kwargs['owner']
|
||||
self.roles = kwargs.get('roles', [])
|
||||
self.name = kwargs.get('name')
|
||||
self.description = kwargs.get('description')
|
||||
self.authority_certificate = kwargs['authority_certificate']
|
||||
self.plugin_name = kwargs['plugin']['slug']
|
||||
|
112
lemur/authorities/schemas.py
Normal file
112
lemur/authorities/schemas.py
Normal file
@ -0,0 +1,112 @@
|
||||
"""
|
||||
.. module: lemur.authorities.schemas
|
||||
:platform: unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
from flask import current_app
|
||||
|
||||
from marshmallow import fields, validates_schema
|
||||
from marshmallow import validate
|
||||
from marshmallow.exceptions import ValidationError
|
||||
|
||||
from lemur.schemas import PluginInputSchema, PluginOutputSchema, ExtensionSchema, AssociatedAuthoritySchema, AssociatedRoleSchema
|
||||
from lemur.users.schemas import UserNestedOutputSchema
|
||||
from lemur.common.schema import LemurInputSchema, LemurOutputSchema
|
||||
from lemur.common import validators
|
||||
|
||||
|
||||
class AuthorityInputSchema(LemurInputSchema):
|
||||
name = fields.String(required=True)
|
||||
owner = fields.Email(required=True)
|
||||
description = fields.String()
|
||||
common_name = fields.String(required=True, validate=validators.sensitive_domain)
|
||||
|
||||
validity_start = fields.Date()
|
||||
validity_end = fields.Date()
|
||||
validity_years = fields.Integer()
|
||||
|
||||
# certificate body fields
|
||||
organizational_unit = fields.String(missing=lambda: current_app.config.get('LEMUR_DEFAULT_ORGANIZATIONAL_UNIT'))
|
||||
organization = fields.String(missing=lambda: current_app.config.get('LEMUR_DEFAULT_ORGANIZATION'))
|
||||
location = fields.String(missing=lambda: current_app.config.get('LEMUR_DEFAULT_LOCATION'))
|
||||
country = fields.String(missing=lambda: current_app.config.get('LEMUR_DEFAULT_COUNTRY'))
|
||||
state = fields.String(missing=lambda: current_app.config.get('LEMUR_DEFAULT_STATE'))
|
||||
|
||||
plugin = fields.Nested(PluginInputSchema)
|
||||
|
||||
# signing related options
|
||||
type = fields.String(validate=validate.OneOf(['root', 'subca']), missing='root')
|
||||
parent = fields.Nested(AssociatedAuthoritySchema)
|
||||
signing_algorithm = fields.String(validate=validate.OneOf(['sha256WithRSA', 'sha1WithRSA']), missing='sha256WithRSA')
|
||||
key_type = fields.String(validate=validate.OneOf(['RSA2048', 'RSA4096']), missing='RSA2048')
|
||||
key_name = fields.String()
|
||||
sensitivity = fields.String(validate=validate.OneOf(['medium', 'high']), missing='medium')
|
||||
serial_number = fields.Integer()
|
||||
first_serial = fields.Integer(missing=1)
|
||||
|
||||
extensions = fields.Nested(ExtensionSchema)
|
||||
|
||||
roles = fields.Nested(AssociatedRoleSchema(many=True))
|
||||
|
||||
@validates_schema
|
||||
def validate_dates(self, data):
|
||||
validators.dates(data)
|
||||
|
||||
@validates_schema
|
||||
def validate_subca(self, data):
|
||||
if data['type'] == 'subca':
|
||||
if not data.get('parent'):
|
||||
raise ValidationError("If generating a subca parent 'authority' must be specified.")
|
||||
|
||||
|
||||
class AuthorityUpdateSchema(LemurInputSchema):
|
||||
owner = fields.Email(required=True)
|
||||
description = fields.String()
|
||||
active = fields.Boolean()
|
||||
roles = fields.Nested(AssociatedRoleSchema(many=True))
|
||||
|
||||
|
||||
class RootAuthorityCertificateOutputSchema(LemurOutputSchema):
|
||||
__envelope__ = False
|
||||
id = fields.Integer()
|
||||
active = fields.Boolean()
|
||||
bits = fields.Integer()
|
||||
body = fields.String()
|
||||
chain = fields.String()
|
||||
description = fields.String()
|
||||
name = fields.String()
|
||||
cn = fields.String()
|
||||
not_after = fields.DateTime()
|
||||
not_before = fields.DateTime()
|
||||
owner = fields.Email()
|
||||
status = fields.Boolean()
|
||||
user = fields.Nested(UserNestedOutputSchema)
|
||||
|
||||
|
||||
class AuthorityOutputSchema(LemurOutputSchema):
|
||||
id = fields.Integer()
|
||||
description = fields.String()
|
||||
name = fields.String()
|
||||
owner = fields.Email()
|
||||
plugin = fields.Nested(PluginOutputSchema)
|
||||
active = fields.Boolean()
|
||||
options = fields.Dict()
|
||||
roles = fields.List(fields.Nested(AssociatedRoleSchema))
|
||||
authority_certificate = fields.Nested(RootAuthorityCertificateOutputSchema)
|
||||
|
||||
|
||||
class AuthorityNestedOutputSchema(LemurOutputSchema):
|
||||
id = fields.Integer()
|
||||
description = fields.String()
|
||||
name = fields.String()
|
||||
owner = fields.Email()
|
||||
plugin = fields.Nested(PluginOutputSchema)
|
||||
active = fields.Boolean()
|
||||
|
||||
|
||||
authority_update_schema = AuthorityUpdateSchema()
|
||||
authority_input_schema = AuthorityInputSchema()
|
||||
authority_output_schema = AuthorityOutputSchema()
|
||||
authorities_output_schema = AuthorityOutputSchema(many=True)
|
@ -9,17 +9,13 @@
|
||||
|
||||
"""
|
||||
from flask import g
|
||||
from flask import current_app
|
||||
|
||||
from lemur import database
|
||||
from lemur.extensions import metrics
|
||||
from lemur.authorities.models import Authority
|
||||
from lemur.roles import service as role_service
|
||||
from lemur.notifications import service as notification_service
|
||||
|
||||
from lemur.roles.models import Role
|
||||
from lemur.certificates.models import Certificate
|
||||
|
||||
from lemur.plugins.base import plugins
|
||||
from lemur.certificates.service import upload
|
||||
|
||||
|
||||
def update(authority_id, description=None, owner=None, active=None, roles=None):
|
||||
@ -28,12 +24,12 @@ 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)
|
||||
|
||||
if roles:
|
||||
authority = database.update_list(authority, 'roles', Role, roles)
|
||||
authority.roles = roles
|
||||
|
||||
if active:
|
||||
authority.active = active
|
||||
@ -43,46 +39,31 @@ def update(authority_id, description=None, owner=None, active=None, roles=None):
|
||||
return database.update(authority)
|
||||
|
||||
|
||||
def create(kwargs):
|
||||
def mint(**kwargs):
|
||||
"""
|
||||
Create a new authority.
|
||||
Creates the authority based on the plugin provided.
|
||||
"""
|
||||
issuer = kwargs['plugin']['plugin_object']
|
||||
body, chain, roles = issuer.create_authority(kwargs)
|
||||
roles = create_authority_roles(roles, kwargs['owner'], kwargs['plugin']['plugin_object'].title)
|
||||
return body, chain, roles
|
||||
|
||||
:rtype : Authority
|
||||
|
||||
def create_authority_roles(roles, owner, plugin_title):
|
||||
"""
|
||||
Creates all of the necessary authority roles.
|
||||
:param roles:
|
||||
:return:
|
||||
"""
|
||||
|
||||
issuer = plugins.get(kwargs.get('pluginName'))
|
||||
|
||||
kwargs['creator'] = g.current_user.email
|
||||
cert_body, intermediate, issuer_roles = issuer.create_authority(kwargs)
|
||||
|
||||
cert = Certificate(cert_body, chain=intermediate)
|
||||
cert.owner = kwargs['ownerEmail']
|
||||
|
||||
if kwargs['caType'] == 'subca':
|
||||
cert.description = "This is the ROOT certificate for the {0} sub certificate authority the parent \
|
||||
authority is {1}.".format(kwargs.get('caName'), kwargs.get('caParent'))
|
||||
else:
|
||||
cert.description = "This is the ROOT certificate for the {0} certificate authority.".format(
|
||||
kwargs.get('caName')
|
||||
)
|
||||
|
||||
cert.user = g.current_user
|
||||
|
||||
cert.notifications = notification_service.create_default_expiration_notifications(
|
||||
'DEFAULT_SECURITY',
|
||||
current_app.config.get('LEMUR_SECURITY_TEAM_EMAIL')
|
||||
)
|
||||
|
||||
# we create and attach any roles that the issuer gives us
|
||||
role_objs = []
|
||||
for r in issuer_roles:
|
||||
|
||||
role = role_service.create(
|
||||
r['name'],
|
||||
password=r['password'],
|
||||
description="{0} auto generated role".format(kwargs.get('pluginName')),
|
||||
username=r['username'])
|
||||
for r in roles:
|
||||
role = role_service.get_by_name(r['name'])
|
||||
if not role:
|
||||
role = role_service.create(
|
||||
r['name'],
|
||||
password=r['password'],
|
||||
description="Auto generated role for {0}".format(plugin_title),
|
||||
username=r['username'])
|
||||
|
||||
# the user creating the authority should be able to administer it
|
||||
if role.username == 'admin':
|
||||
@ -90,21 +71,51 @@ def create(kwargs):
|
||||
|
||||
role_objs.append(role)
|
||||
|
||||
authority = Authority(
|
||||
kwargs.get('caName'),
|
||||
kwargs['ownerEmail'],
|
||||
kwargs['pluginName'],
|
||||
cert_body,
|
||||
description=kwargs['caDescription'],
|
||||
chain=intermediate,
|
||||
roles=role_objs
|
||||
)
|
||||
# create an role for the owner and assign it
|
||||
owner_role = role_service.get_by_name(owner)
|
||||
if not owner_role:
|
||||
owner_role = role_service.create(
|
||||
owner,
|
||||
description="Auto generated role based on owner: {0}".format(owner)
|
||||
)
|
||||
|
||||
database.update(cert)
|
||||
role_objs.append(owner_role)
|
||||
return role_objs
|
||||
|
||||
|
||||
def create(**kwargs):
|
||||
"""
|
||||
Creates a new authority.
|
||||
"""
|
||||
kwargs['creator'] = g.user.email
|
||||
body, chain, roles = mint(**kwargs)
|
||||
|
||||
kwargs['body'] = body
|
||||
kwargs['chain'] = chain
|
||||
|
||||
if kwargs.get('roles'):
|
||||
kwargs['roles'] += roles
|
||||
else:
|
||||
kwargs['roles'] = roles
|
||||
|
||||
if kwargs['type'] == 'subca':
|
||||
description = "This is the ROOT certificate for the {0} sub certificate authority the parent \
|
||||
authority is {1}.".format(kwargs.get('name'), kwargs.get('parent'))
|
||||
else:
|
||||
description = "This is the ROOT certificate for the {0} certificate authority.".format(
|
||||
kwargs.get('name')
|
||||
)
|
||||
|
||||
kwargs['description'] = description
|
||||
|
||||
cert = upload(**kwargs)
|
||||
kwargs['authority_certificate'] = cert
|
||||
|
||||
authority = Authority(**kwargs)
|
||||
authority = database.create(authority)
|
||||
g.user.authorities.append(authority)
|
||||
|
||||
g.current_user.authorities.append(authority)
|
||||
|
||||
metrics.send('authority_created', 'counter', 1, metric_tags=dict(owner=authority.owner))
|
||||
return authority
|
||||
|
||||
|
||||
@ -123,7 +134,6 @@ def get(authority_id):
|
||||
"""
|
||||
Retrieves an authority given it's ID
|
||||
|
||||
:rtype : Authority
|
||||
:param authority_id:
|
||||
:return:
|
||||
"""
|
||||
@ -135,7 +145,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')
|
||||
@ -149,14 +158,9 @@ def get_authority_role(ca_name):
|
||||
:param ca_name:
|
||||
"""
|
||||
if g.current_user.is_admin:
|
||||
authority = get_by_name(ca_name)
|
||||
# TODO we should pick admin ca roles for admin
|
||||
return authority.roles[0]
|
||||
return role_service.get_by_name("{0}_admin".format(ca_name))
|
||||
else:
|
||||
for role in g.current_user.roles:
|
||||
if role.authority:
|
||||
if role.authority.name == ca_name:
|
||||
return role
|
||||
return role_service.get_by_name("{0}_operator".format(ca_name))
|
||||
|
||||
|
||||
def render(args):
|
||||
@ -166,10 +170,6 @@ def render(args):
|
||||
:return:
|
||||
"""
|
||||
query = database.session_query(Authority)
|
||||
sort_by = args.pop('sort_by')
|
||||
sort_dir = args.pop('sort_dir')
|
||||
page = args.pop('page')
|
||||
count = args.pop('count')
|
||||
filt = args.pop('filter')
|
||||
|
||||
if filt:
|
||||
@ -183,13 +183,8 @@ def render(args):
|
||||
if not g.current_user.is_admin:
|
||||
authority_ids = []
|
||||
for role in g.current_user.roles:
|
||||
if role.authority:
|
||||
authority_ids.append(role.authority.id)
|
||||
for authority in role.authorities:
|
||||
authority_ids.append(authority.id)
|
||||
query = query.filter(Authority.id.in_(authority_ids))
|
||||
|
||||
query = database.find_all(query, Authority, args)
|
||||
|
||||
if sort_by and sort_dir:
|
||||
query = database.sort(query, Authority, sort_by, sort_dir)
|
||||
|
||||
return database.paginate(query, page, count)
|
||||
return database.sort_and_page(query, Authority, args)
|
||||
|
@ -5,32 +5,19 @@
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
from flask import Blueprint, g
|
||||
from flask.ext.restful import reqparse, fields, Api
|
||||
from flask import Blueprint
|
||||
from flask.ext.restful import reqparse, Api
|
||||
|
||||
from lemur.authorities import service
|
||||
from lemur.roles import service as role_service
|
||||
from lemur.certificates import service as certificate_service
|
||||
from lemur.common.utils import paginated_parser
|
||||
from lemur.common.schema import validate_schema
|
||||
from lemur.auth.service import AuthenticatedResource
|
||||
|
||||
from lemur.auth.permissions import AuthorityPermission
|
||||
|
||||
from lemur.common.utils import paginated_parser, marshal_items
|
||||
from lemur.certificates import service as certificate_service
|
||||
|
||||
from lemur.authorities import service
|
||||
from lemur.authorities.schemas import authority_input_schema, authority_output_schema, authorities_output_schema, authority_update_schema
|
||||
|
||||
FIELDS = {
|
||||
'name': fields.String,
|
||||
'owner': fields.String,
|
||||
'description': fields.String,
|
||||
'options': fields.Raw,
|
||||
'pluginName': fields.String,
|
||||
'body': fields.String,
|
||||
'chain': fields.String,
|
||||
'active': fields.Boolean,
|
||||
'notBefore': fields.DateTime(dt_format='iso8601', attribute='not_before'),
|
||||
'notAfter': fields.DateTime(dt_format='iso8601', attribute='not_after'),
|
||||
'id': fields.Integer,
|
||||
}
|
||||
|
||||
mod = Blueprint('authorities', __name__)
|
||||
api = Api(mod)
|
||||
@ -42,7 +29,7 @@ class AuthoritiesList(AuthenticatedResource):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(AuthoritiesList, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
@validate_schema(None, authorities_output_schema)
|
||||
def get(self):
|
||||
"""
|
||||
.. http:get:: /authorities
|
||||
@ -66,28 +53,52 @@ class AuthoritiesList(AuthenticatedResource):
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "authority1",
|
||||
"description": "this is authority1",
|
||||
"pluginName": null,
|
||||
"chain": "-----Begin ...",
|
||||
"body": "-----Begin ...",
|
||||
"active": true,
|
||||
"notBefore": "2015-06-05T17:09:39",
|
||||
"notAfter": "2015-06-10T17:09:39"
|
||||
"options": null
|
||||
}
|
||||
]
|
||||
"items": [{
|
||||
"name": "TestAuthority",
|
||||
"roles": [{
|
||||
"id": 123,
|
||||
"name": "secure@example.com"
|
||||
}, {
|
||||
"id": 564,
|
||||
"name": "TestAuthority_admin"
|
||||
}, {
|
||||
"id": 565,
|
||||
"name": "TestAuthority_operator"
|
||||
}],
|
||||
"options": null,
|
||||
"active": true,
|
||||
"authorityCertificate": {
|
||||
"body": "-----BEGIN CERTIFICATE-----IyMzU5MTVaMHk...",
|
||||
"status": true,
|
||||
"cn": "AcommonName",
|
||||
"description": "This is the ROOT certificate for the TestAuthority certificate authority.",
|
||||
"chain": "",
|
||||
"notBefore": "2016-06-02T00:00:15+00:00",
|
||||
"notAfter": "2023-06-02T23:59:15+00:00",
|
||||
"owner": "secure@example.com",
|
||||
"user": {
|
||||
"username": "joe@example.com",
|
||||
"active": true,
|
||||
"email": "joe@example.com",
|
||||
"id": 3
|
||||
},
|
||||
"active": true,
|
||||
"bits": 2048,
|
||||
"id": 2235,
|
||||
"name": "TestAuthority"
|
||||
},
|
||||
"owner": "secure@example.com",
|
||||
"id": 43,
|
||||
"description": "This is the ROOT certificate for the TestAuthority certificate authority."
|
||||
}
|
||||
"total": 1
|
||||
}
|
||||
|
||||
: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 count: count number default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
@ -98,8 +109,8 @@ class AuthoritiesList(AuthenticatedResource):
|
||||
args = parser.parse_args()
|
||||
return service.render(args)
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
def post(self):
|
||||
@validate_schema(authority_input_schema, authority_output_schema)
|
||||
def post(self, data=None):
|
||||
"""
|
||||
.. http:post:: /authorities
|
||||
|
||||
@ -113,31 +124,30 @@ class AuthoritiesList(AuthenticatedResource):
|
||||
Host: example.com
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
{
|
||||
"caDN": {
|
||||
"country": "US",
|
||||
"state": "CA",
|
||||
"location": "A Location",
|
||||
"organization": "ExampleInc",
|
||||
"organizationalUnit": "Operations",
|
||||
"commonName": "a common name"
|
||||
},
|
||||
"caType": "root",
|
||||
"caSigningAlgo": "sha256WithRSA",
|
||||
"caSensitivity": "medium",
|
||||
{
|
||||
"country": "US",
|
||||
"state": "California",
|
||||
"location": "Los Gatos",
|
||||
"organization": "Netflix",
|
||||
"organizationalUnit": "Operations",
|
||||
"type": "root",
|
||||
"signingAlgorithm": "sha256WithRSA",
|
||||
"sensitivity": "medium",
|
||||
"keyType": "RSA2048",
|
||||
"pluginName": "cloudca",
|
||||
"validityStart": "2015-06-11T07:00:00.000Z",
|
||||
"validityEnd": "2015-06-13T07:00:00.000Z",
|
||||
"caName": "DoctestCA",
|
||||
"ownerEmail": "jimbob@example.com",
|
||||
"caDescription": "Example CA",
|
||||
"extensions": {
|
||||
"subAltNames": {
|
||||
"names": []
|
||||
}
|
||||
"plugin": {
|
||||
"slug": "cloudca-issuer",
|
||||
},
|
||||
}
|
||||
"name": "TimeTestAuthority5",
|
||||
"owner": "secure@example.com",
|
||||
"description": "test",
|
||||
"commonName": "AcommonName",
|
||||
"validityYears": "20",
|
||||
"extensions": {
|
||||
"subAltNames": {
|
||||
"names": []
|
||||
},
|
||||
"custom": []
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
@ -148,57 +158,67 @@ class AuthoritiesList(AuthenticatedResource):
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"name": "authority1",
|
||||
"description": "this is authority1",
|
||||
"pluginName": null,
|
||||
"chain": "-----Begin ...",
|
||||
"body": "-----Begin ...",
|
||||
"name": "TestAuthority",
|
||||
"roles": [{
|
||||
"id": 123,
|
||||
"name": "secure@example.com"
|
||||
}, {
|
||||
"id": 564,
|
||||
"name": "TestAuthority_admin"
|
||||
}, {
|
||||
"id": 565,
|
||||
"name": "TestAuthority_operator"
|
||||
}],
|
||||
"options": null,
|
||||
"active": true,
|
||||
"notBefore": "2015-06-05T17:09:39",
|
||||
"notAfter": "2015-06-10T17:09:39"
|
||||
"options": null
|
||||
"authorityCertificate": {
|
||||
"body": "-----BEGIN CERTIFICATE-----IyMzU5MTVaMHk...",
|
||||
"status": true,
|
||||
"cn": "AcommonName",
|
||||
"description": "This is the ROOT certificate for the TestAuthority certificate authority.",
|
||||
"chain": "",
|
||||
"notBefore": "2016-06-02T00:00:15+00:00",
|
||||
"notAfter": "2023-06-02T23:59:15+00:00",
|
||||
"owner": "secure@example.com",
|
||||
"user": {
|
||||
"username": "joe@example.com",
|
||||
"active": true,
|
||||
"email": "joe@example.com",
|
||||
"id": 3
|
||||
},
|
||||
"active": true,
|
||||
"bits": 2048,
|
||||
"id": 2235,
|
||||
"name": "TestAuthority"
|
||||
},
|
||||
"owner": "secure@example.com",
|
||||
"id": 43,
|
||||
"description": "This is the ROOT certificate for the TestAuthority certificate authority."
|
||||
}
|
||||
|
||||
:arg caName: authority's name
|
||||
:arg caDescription: a sensible description about what the CA with be used for
|
||||
:arg ownerEmail: the team or person who 'owns' this authority
|
||||
|
||||
:arg name: authority's name
|
||||
:arg description: a sensible description about what the CA with be used for
|
||||
:arg owner: the team or person who 'owns' this authority
|
||||
:arg validityStart: when this authority should start issuing certificates
|
||||
:arg validityEnd: when this authority should stop issuing certificates
|
||||
:arg validityYears: starting from `now` how many years into the future the authority should be valid
|
||||
:arg extensions: certificate extensions
|
||||
:arg pluginName: name of the plugin to create the authority
|
||||
:arg caType: the type of authority (root/subca)
|
||||
:arg caParent: the parent authority if this is to be a subca
|
||||
:arg caSigningAlgo: algorithm used to sign the authority
|
||||
:arg plugin: name of the plugin to create the authority
|
||||
:arg type: the type of authority (root/subca)
|
||||
:arg parent: the parent authority if this is to be a subca
|
||||
:arg signingAlgorithm: algorithm used to sign the authority
|
||||
:arg keyType: key type
|
||||
:arg caSensitivity: the sensitivity of the root key, for CloudCA this determines if the root keys are stored
|
||||
:arg sensitivity: the sensitivity of the root key, for CloudCA this determines if the root keys are stored
|
||||
in an HSM
|
||||
:arg caKeyName: name of the key to store in the HSM (CloudCA)
|
||||
:arg caSerialNumber: serial number of the authority
|
||||
:arg caFirstSerial: specifies the starting serial number for certificates issued off of this authority
|
||||
:arg keyName: name of the key to store in the HSM (CloudCA)
|
||||
:arg serialNumber: serial number of the authority
|
||||
:arg firstSerial: specifies the starting serial number for certificates issued off of this authority
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 403: unauthenticated
|
||||
:statuscode 200: no error
|
||||
"""
|
||||
self.reqparse.add_argument('caName', type=str, location='json', required=True)
|
||||
self.reqparse.add_argument('caDescription', type=str, location='json', required=False)
|
||||
self.reqparse.add_argument('ownerEmail', type=str, location='json', required=True)
|
||||
self.reqparse.add_argument('caDN', type=dict, location='json', required=False)
|
||||
self.reqparse.add_argument('validityStart', type=str, location='json', required=False) # TODO validate
|
||||
self.reqparse.add_argument('validityEnd', type=str, location='json', required=False) # TODO validate
|
||||
self.reqparse.add_argument('extensions', type=dict, location='json', required=False)
|
||||
self.reqparse.add_argument('pluginName', type=str, location='json', required=True)
|
||||
self.reqparse.add_argument('caType', type=str, location='json', required=False)
|
||||
self.reqparse.add_argument('caParent', type=str, location='json', required=False)
|
||||
self.reqparse.add_argument('caSigningAlgo', type=str, location='json', required=False)
|
||||
self.reqparse.add_argument('keyType', type=str, location='json', required=False)
|
||||
self.reqparse.add_argument('caSensitivity', type=str, location='json', required=False)
|
||||
self.reqparse.add_argument('caKeyName', type=str, location='json', required=False)
|
||||
self.reqparse.add_argument('caSerialNumber', type=int, location='json', required=False)
|
||||
self.reqparse.add_argument('caFirstSerial', type=int, location='json', required=False)
|
||||
|
||||
args = self.reqparse.parse_args()
|
||||
return service.create(args)
|
||||
return service.create(**data)
|
||||
|
||||
|
||||
class Authorities(AuthenticatedResource):
|
||||
@ -206,7 +226,7 @@ class Authorities(AuthenticatedResource):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(Authorities, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
@validate_schema(None, authority_output_schema)
|
||||
def get(self, authority_id):
|
||||
"""
|
||||
.. http:get:: /authorities/1
|
||||
@ -230,26 +250,36 @@ class Authorities(AuthenticatedResource):
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"name": "authority1",
|
||||
"description": "this is authority1",
|
||||
"pluginName": null,
|
||||
"chain": "-----Begin ...",
|
||||
"body": "-----Begin ...",
|
||||
"roles": [{
|
||||
"id": 123,
|
||||
"name": "secure@example.com"
|
||||
}, {
|
||||
"id": 564,
|
||||
"name": "TestAuthority_admin"
|
||||
}, {
|
||||
"id": 565,
|
||||
"name": "TestAuthority_operator"
|
||||
}],
|
||||
"active": true,
|
||||
"notBefore": "2015-06-05T17:09:39",
|
||||
"notAfter": "2015-06-10T17:09:39"
|
||||
"options": null
|
||||
"owner": "secure@example.com",
|
||||
"id": 43,
|
||||
"description": "This is the ROOT certificate for the TestAuthority certificate authority."
|
||||
}
|
||||
|
||||
:arg description: a sensible description about what the CA with be used for
|
||||
:arg owner: the team or person who 'owns' this authority
|
||||
:arg active: set whether this authoritity is currently in use
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 403: unauthenticated
|
||||
:statuscode 200: no error
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
"""
|
||||
return service.get(authority_id)
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
def put(self, authority_id):
|
||||
@validate_schema(authority_update_schema, authority_output_schema)
|
||||
def put(self, authority_id, data=None):
|
||||
"""
|
||||
.. http:put:: /authorities/1
|
||||
|
||||
@ -264,11 +294,42 @@ class Authorities(AuthenticatedResource):
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
{
|
||||
"roles": [],
|
||||
"active": false,
|
||||
"owner": "bob@example.com",
|
||||
"description": "this is authority1"
|
||||
}
|
||||
"name": "TestAuthority5",
|
||||
"roles": [{
|
||||
"id": 566,
|
||||
"name": "TestAuthority5_admin"
|
||||
}, {
|
||||
"id": 567,
|
||||
"name": "TestAuthority5_operator"
|
||||
}, {
|
||||
"id": 123,
|
||||
"name": "secure@example.com"
|
||||
}],
|
||||
"active": true,
|
||||
"authorityCertificate": {
|
||||
"body": "-----BEGIN CERTIFICATE-----",
|
||||
"status": null,
|
||||
"cn": "AcommonName",
|
||||
"description": "This is the ROOT certificate for the TestAuthority5 certificate authority.",
|
||||
"chain": "",
|
||||
"notBefore": "2016-06-03T00:00:51+00:00",
|
||||
"notAfter": "2036-06-03T23:59:51+00:00",
|
||||
"owner": "secure@example.com",
|
||||
"user": {
|
||||
"username": "joe@example.com",
|
||||
"active": true,
|
||||
"email": "joe@example.com",
|
||||
"id": 3
|
||||
},
|
||||
"active": true,
|
||||
"bits": 2048,
|
||||
"id": 2280,
|
||||
"name": "TestAuthority5"
|
||||
},
|
||||
"owner": "secure@example.com",
|
||||
"id": 44,
|
||||
"description": "This is the ROOT certificate for the TestAuthority5 certificate authority."
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
@ -279,64 +340,74 @@ class Authorities(AuthenticatedResource):
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"name": "authority1",
|
||||
"description": "this is authority1",
|
||||
"pluginName": null,
|
||||
"chain": "-----begin ...",
|
||||
"body": "-----begin ...",
|
||||
"active": false,
|
||||
"notBefore": "2015-06-05t17:09:39",
|
||||
"notAfter": "2015-06-10t17:09:39"
|
||||
"options": null
|
||||
"name": "TestAuthority",
|
||||
"roles": [{
|
||||
"id": 123,
|
||||
"name": "secure@example.com"
|
||||
}, {
|
||||
"id": 564,
|
||||
"name": "TestAuthority_admin"
|
||||
}, {
|
||||
"id": 565,
|
||||
"name": "TestAuthority_operator"
|
||||
}],
|
||||
"options": null,
|
||||
"active": true,
|
||||
"authorityCertificate": {
|
||||
"body": "-----BEGIN CERTIFICATE-----IyMzU5MTVaMHk...",
|
||||
"status": true,
|
||||
"cn": "AcommonName",
|
||||
"description": "This is the ROOT certificate for the TestAuthority certificate authority.",
|
||||
"chain": "",
|
||||
"notBefore": "2016-06-02T00:00:15+00:00",
|
||||
"notAfter": "2023-06-02T23:59:15+00:00",
|
||||
"owner": "secure@example.com",
|
||||
"user": {
|
||||
"username": "joe@example.com",
|
||||
"active": true,
|
||||
"email": "joe@example.com",
|
||||
"id": 3
|
||||
},
|
||||
"active": true,
|
||||
"bits": 2048,
|
||||
"id": 2235,
|
||||
"name": "TestAuthority"
|
||||
},
|
||||
"owner": "secure@example.com",
|
||||
"id": 43,
|
||||
"description": "This is the ROOT certificate for the TestAuthority certificate authority."
|
||||
}
|
||||
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
"""
|
||||
self.reqparse.add_argument('roles', type=list, default=[], location='json')
|
||||
self.reqparse.add_argument('active', type=str, location='json', required=True)
|
||||
self.reqparse.add_argument('owner', type=str, location='json', required=True)
|
||||
self.reqparse.add_argument('description', type=str, location='json', required=True)
|
||||
args = self.reqparse.parse_args()
|
||||
|
||||
authority = service.get(authority_id)
|
||||
role = role_service.get_by_name(authority.owner)
|
||||
|
||||
if not authority:
|
||||
return dict(message='Not Found'), 404
|
||||
|
||||
# all the authority role members should be allowed
|
||||
roles = [x.name for x in authority.roles]
|
||||
|
||||
# allow "owner" roles by team DL
|
||||
roles.append(role)
|
||||
permission = AuthorityPermission(authority_id, roles)
|
||||
|
||||
# we want to make sure that we cannot add roles that we are not members of
|
||||
if not g.current_user.is_admin:
|
||||
role_ids = set([r['id'] for r in args['roles']])
|
||||
user_role_ids = set([r.id for r in g.current_user.roles])
|
||||
|
||||
if not role_ids.issubset(user_role_ids):
|
||||
return dict(message="You are not allowed to associate a role which you are not a member of"), 400
|
||||
|
||||
if permission.can():
|
||||
return service.update(
|
||||
authority_id,
|
||||
owner=args['owner'],
|
||||
description=args['description'],
|
||||
active=args['active'],
|
||||
roles=args['roles']
|
||||
owner=data['owner'],
|
||||
description=data['description'],
|
||||
active=data['active'],
|
||||
roles=data['roles']
|
||||
)
|
||||
|
||||
return dict(message="You are not authorized to update this authority"), 403
|
||||
return dict(message="You are not authorized to update this authority."), 403
|
||||
|
||||
|
||||
class CertificateAuthority(AuthenticatedResource):
|
||||
def __init__(self):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(CertificateAuthority, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
@validate_schema(None, authority_output_schema)
|
||||
def get(self, certificate_id):
|
||||
"""
|
||||
.. http:get:: /certificates/1/authority
|
||||
@ -360,16 +431,42 @@ class CertificateAuthority(AuthenticatedResource):
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"name": "authority1",
|
||||
"description": "this is authority1",
|
||||
"pluginName": null,
|
||||
"chain": "-----Begin ...",
|
||||
"body": "-----Begin ...",
|
||||
"name": "TestAuthority",
|
||||
"roles": [{
|
||||
"id": 123,
|
||||
"name": "secure@example.com"
|
||||
}, {
|
||||
"id": 564,
|
||||
"name": "TestAuthority_admin"
|
||||
}, {
|
||||
"id": 565,
|
||||
"name": "TestAuthority_operator"
|
||||
}],
|
||||
"options": null,
|
||||
"active": true,
|
||||
"notBefore": "2015-06-05T17:09:39",
|
||||
"notAfter": "2015-06-10T17:09:39"
|
||||
"options": null
|
||||
"authorityCertificate": {
|
||||
"body": "-----BEGIN CERTIFICATE-----IyMzU5MTVaMHk...",
|
||||
"status": true,
|
||||
"cn": "AcommonName",
|
||||
"description": "This is the ROOT certificate for the TestAuthority certificate authority.",
|
||||
"chain": "",
|
||||
"notBefore": "2016-06-02T00:00:15+00:00",
|
||||
"notAfter": "2023-06-02T23:59:15+00:00",
|
||||
"owner": "secure@example.com",
|
||||
"user": {
|
||||
"username": "joe@example.com",
|
||||
"active": true,
|
||||
"email": "joe@example.com",
|
||||
"id": 3
|
||||
},
|
||||
"active": true,
|
||||
"bits": 2048,
|
||||
"id": 2235,
|
||||
"name": "TestAuthority"
|
||||
},
|
||||
"owner": "secure@example.com",
|
||||
"id": 43,
|
||||
"description": "This is the ROOT certificate for the TestAuthority certificate authority."
|
||||
}
|
||||
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
@ -378,10 +475,35 @@ class CertificateAuthority(AuthenticatedResource):
|
||||
"""
|
||||
cert = certificate_service.get(certificate_id)
|
||||
if not cert:
|
||||
return dict(message="Certificate not found"), 404
|
||||
return dict(message="Certificate not found."), 404
|
||||
|
||||
return cert.authority
|
||||
|
||||
|
||||
class AuthorityVisualizations(AuthenticatedResource):
|
||||
def get(self, authority_id):
|
||||
"""
|
||||
{"name": "flare",
|
||||
"children": [
|
||||
{
|
||||
"name": "analytics",
|
||||
"children": [
|
||||
{
|
||||
"name": "cluster",
|
||||
"children": [
|
||||
{"name": "AgglomerativeCluster", "size": 3938},
|
||||
{"name": "CommunityStructure", "size": 3812},
|
||||
{"name": "HierarchicalCluster", "size": 6714},
|
||||
{"name": "MergeEdge", "size": 743}
|
||||
]
|
||||
}
|
||||
}
|
||||
]}
|
||||
"""
|
||||
authority = service.get(authority_id)
|
||||
return dict(name=authority.name, children=[{"name": c.name} for c in authority.certificates])
|
||||
|
||||
api.add_resource(AuthoritiesList, '/authorities', endpoint='authorities')
|
||||
api.add_resource(Authorities, '/authorities/<int:authority_id>', endpoint='authority')
|
||||
api.add_resource(AuthorityVisualizations, '/authorities/<int:authority_id>/visualize', endpoint='authority_visualizations')
|
||||
api.add_resource(CertificateAuthority, '/certificates/<int:certificate_id>/authority', endpoint='certificateAuthority')
|
||||
|
@ -6,262 +6,102 @@
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
import datetime
|
||||
|
||||
from flask import current_app
|
||||
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy import event, Integer, ForeignKey, String, DateTime, PassiveDefault, func, Column, Text, Boolean
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from lemur.utils import Vault
|
||||
from lemur.database import db
|
||||
from lemur.plugins.base import plugins
|
||||
|
||||
from lemur.domains.models import Domain
|
||||
|
||||
from lemur.constants import SAN_NAMING_TEMPLATE, DEFAULT_NAMING_TEMPLATE
|
||||
|
||||
from lemur.models import certificate_associations, certificate_source_associations, \
|
||||
certificate_destination_associations, certificate_notification_associations, \
|
||||
certificate_replacement_associations
|
||||
certificate_replacement_associations, roles_certificates
|
||||
from lemur.plugins.base import plugins
|
||||
from lemur.utils import Vault
|
||||
|
||||
from lemur.common import defaults
|
||||
from lemur.domains.models import Domain
|
||||
|
||||
|
||||
def create_name(issuer, not_before, not_after, subject, san):
|
||||
"""
|
||||
Create a name for our certificate. A naming standard
|
||||
is based on a series of templates. The name includes
|
||||
useful information such as Common Name, Validation dates,
|
||||
and Issuer.
|
||||
def get_or_increase_name(name):
|
||||
count = Certificate.query.filter(Certificate.name.ilike('{0}%'.format(name))).count()
|
||||
|
||||
:param san:
|
||||
:param subject:
|
||||
:param not_after:
|
||||
:param issuer:
|
||||
:param not_before:
|
||||
:rtype : str
|
||||
:return:
|
||||
"""
|
||||
if san:
|
||||
t = SAN_NAMING_TEMPLATE
|
||||
else:
|
||||
t = DEFAULT_NAMING_TEMPLATE
|
||||
if count >= 1:
|
||||
return name + '-' + str(count)
|
||||
|
||||
temp = t.format(
|
||||
subject=subject,
|
||||
issuer=issuer,
|
||||
not_before=not_before.strftime('%Y%m%d'),
|
||||
not_after=not_after.strftime('%Y%m%d')
|
||||
)
|
||||
|
||||
# NOTE we may want to give more control over naming
|
||||
# aws doesn't allow special chars except '-'
|
||||
disallowed_chars = ''.join(c for c in map(chr, range(256)) if not c.isalnum())
|
||||
disallowed_chars = disallowed_chars.replace("-", "")
|
||||
disallowed_chars = disallowed_chars.replace(".", "")
|
||||
temp = temp.replace('*', "WILDCARD")
|
||||
|
||||
for c in disallowed_chars:
|
||||
temp = temp.replace(c, "")
|
||||
|
||||
# white space is silly too
|
||||
return temp.replace(" ", "-")
|
||||
|
||||
|
||||
def get_signing_algorithm(cert):
|
||||
return cert.signature_hash_algorithm.name
|
||||
|
||||
|
||||
def get_cn(cert):
|
||||
"""
|
||||
Attempts to get a sane common name from a given certificate.
|
||||
|
||||
:param cert:
|
||||
:return: Common name or None
|
||||
"""
|
||||
return cert.subject.get_attributes_for_oid(
|
||||
x509.OID_COMMON_NAME
|
||||
)[0].value.strip()
|
||||
|
||||
|
||||
def get_domains(cert):
|
||||
"""
|
||||
Attempts to get an domains listed in a certificate.
|
||||
If 'subjectAltName' extension is not available we simply
|
||||
return the common name.
|
||||
|
||||
:param cert:
|
||||
:return: List of domains
|
||||
"""
|
||||
domains = []
|
||||
try:
|
||||
ext = cert.extensions.get_extension_for_oid(x509.OID_SUBJECT_ALTERNATIVE_NAME)
|
||||
entries = ext.value.get_values_for_type(x509.DNSName)
|
||||
for entry in entries:
|
||||
domains.append(entry)
|
||||
except Exception as e:
|
||||
current_app.logger.warning("Failed to get SubjectAltName: {0}".format(e))
|
||||
|
||||
return domains
|
||||
|
||||
|
||||
def get_serial(cert):
|
||||
"""
|
||||
Fetch the serial number from the certificate.
|
||||
|
||||
:param cert:
|
||||
:return: serial number
|
||||
"""
|
||||
return cert.serial
|
||||
|
||||
|
||||
def is_san(cert):
|
||||
"""
|
||||
Determines if a given certificate is a SAN certificate.
|
||||
SAN certificates are simply certificates that cover multiple domains.
|
||||
|
||||
:param cert:
|
||||
:return: Bool
|
||||
"""
|
||||
if len(get_domains(cert)) > 1:
|
||||
return True
|
||||
|
||||
|
||||
def is_wildcard(cert):
|
||||
"""
|
||||
Determines if certificate is a wildcard certificate.
|
||||
|
||||
:param cert:
|
||||
:return: Bool
|
||||
"""
|
||||
domains = get_domains(cert)
|
||||
if len(domains) == 1 and domains[0][0:1] == "*":
|
||||
return True
|
||||
|
||||
if cert.subject.get_attributes_for_oid(x509.OID_COMMON_NAME)[0].value[0:1] == "*":
|
||||
return True
|
||||
|
||||
|
||||
def get_bitstrength(cert):
|
||||
"""
|
||||
Calculates a certificates public key bit length.
|
||||
|
||||
:param cert:
|
||||
:return: Integer
|
||||
"""
|
||||
return cert.public_key().key_size
|
||||
|
||||
|
||||
def get_issuer(cert):
|
||||
"""
|
||||
Gets a sane issuer from a given certificate.
|
||||
|
||||
:param cert:
|
||||
:return: Issuer
|
||||
"""
|
||||
delchars = ''.join(c for c in map(chr, range(256)) if not c.isalnum())
|
||||
try:
|
||||
issuer = str(cert.issuer.get_attributes_for_oid(x509.OID_ORGANIZATION_NAME)[0].value)
|
||||
for c in delchars:
|
||||
issuer = issuer.replace(c, "")
|
||||
return issuer
|
||||
except Exception as e:
|
||||
current_app.logger.error("Unable to get issuer! {0}".format(e))
|
||||
|
||||
|
||||
def get_not_before(cert):
|
||||
"""
|
||||
Gets the naive datetime of the certificates 'not_before' field.
|
||||
This field denotes the first date in time which the given certificate
|
||||
is valid.
|
||||
|
||||
:param cert:
|
||||
:return: Datetime
|
||||
"""
|
||||
return cert.not_valid_before
|
||||
|
||||
|
||||
def get_not_after(cert):
|
||||
"""
|
||||
Gets the naive datetime of the certificates 'not_after' field.
|
||||
This field denotes the last date in time which the given certificate
|
||||
is valid.
|
||||
|
||||
:param cert:
|
||||
:return: Datetime
|
||||
"""
|
||||
return cert.not_valid_after
|
||||
|
||||
|
||||
def get_name_from_arn(arn):
|
||||
"""
|
||||
Extract the certificate name from an arn.
|
||||
|
||||
:param arn: IAM SSL arn
|
||||
:return: name of the certificate as uploaded to AWS
|
||||
"""
|
||||
return arn.split("/", 1)[1]
|
||||
|
||||
|
||||
def get_account_number(arn):
|
||||
"""
|
||||
Extract the account number from an arn.
|
||||
|
||||
:param arn: IAM SSL arn
|
||||
:return: account number associated with ARN
|
||||
"""
|
||||
return arn.split(":")[4]
|
||||
return name
|
||||
|
||||
|
||||
class Certificate(db.Model):
|
||||
__tablename__ = 'certificates'
|
||||
id = Column(Integer, primary_key=True)
|
||||
owner = Column(String(128))
|
||||
body = Column(Text())
|
||||
private_key = Column(Vault)
|
||||
status = Column(String(128))
|
||||
deleted = Column(Boolean, index=True)
|
||||
name = Column(String(128))
|
||||
owner = Column(String(128), nullable=False)
|
||||
name = Column(String(128)) # , unique=True) TODO make all names unique
|
||||
description = Column(String(1024))
|
||||
active = Column(Boolean, default=True)
|
||||
|
||||
body = Column(Text(), nullable=False)
|
||||
chain = Column(Text())
|
||||
bits = Column(Integer())
|
||||
private_key = Column(Vault)
|
||||
|
||||
issuer = Column(String(128))
|
||||
serial = Column(String(128))
|
||||
cn = Column(String(128))
|
||||
description = Column(String(1024))
|
||||
active = Column(Boolean, default=True)
|
||||
san = Column(String(1024))
|
||||
deleted = Column(Boolean, index=True)
|
||||
|
||||
not_before = Column(DateTime)
|
||||
not_after = Column(DateTime)
|
||||
date_created = Column(DateTime, PassiveDefault(func.now()), nullable=False)
|
||||
|
||||
signing_algorithm = Column(String(128))
|
||||
status = Column(String(128))
|
||||
bits = Column(Integer())
|
||||
san = Column(String(1024)) # TODO this should be migrated to boolean
|
||||
|
||||
user_id = Column(Integer, ForeignKey('users.id'))
|
||||
authority_id = Column(Integer, ForeignKey('authorities.id'))
|
||||
authority_id = Column(Integer, ForeignKey('authorities.id', ondelete="CASCADE"))
|
||||
root_authority_id = Column(Integer, ForeignKey('authorities.id', ondelete="CASCADE"))
|
||||
|
||||
notifications = relationship("Notification", secondary=certificate_notification_associations, backref='certificate')
|
||||
destinations = relationship("Destination", secondary=certificate_destination_associations, backref='certificate')
|
||||
sources = relationship("Source", secondary=certificate_source_associations, backref='certificate')
|
||||
domains = relationship("Domain", secondary=certificate_associations, backref="certificate")
|
||||
roles = relationship("Role", secondary=roles_certificates, backref="certificate")
|
||||
replaces = relationship("Certificate",
|
||||
secondary=certificate_replacement_associations,
|
||||
primaryjoin=id == certificate_replacement_associations.c.certificate_id, # noqa
|
||||
secondaryjoin=id == certificate_replacement_associations.c.replaced_certificate_id, # noqa
|
||||
backref='replaced')
|
||||
sources = relationship("Source", secondary=certificate_source_associations, backref='certificate')
|
||||
domains = relationship("Domain", secondary=certificate_associations, backref="certificate")
|
||||
|
||||
def __init__(self, body, private_key=None, chain=None):
|
||||
self.body = body
|
||||
# We encrypt the private_key on creation
|
||||
self.private_key = private_key
|
||||
self.chain = chain
|
||||
cert = x509.load_pem_x509_certificate(str(self.body), default_backend())
|
||||
self.signing_algorithm = get_signing_algorithm(cert)
|
||||
self.bits = get_bitstrength(cert)
|
||||
self.issuer = get_issuer(cert)
|
||||
self.serial = get_serial(cert)
|
||||
self.cn = get_cn(cert)
|
||||
self.san = is_san(cert)
|
||||
self.not_before = get_not_before(cert)
|
||||
self.not_after = get_not_after(cert)
|
||||
self.name = create_name(self.issuer, self.not_before, self.not_after, self.cn, self.san)
|
||||
def __init__(self, **kwargs):
|
||||
cert = defaults.parse_certificate(kwargs['body'])
|
||||
|
||||
for domain in get_domains(cert):
|
||||
self.issuer = defaults.issuer(cert)
|
||||
self.cn = defaults.common_name(cert)
|
||||
self.san = defaults.san(cert)
|
||||
self.not_before = defaults.not_before(cert)
|
||||
self.not_after = defaults.not_after(cert)
|
||||
|
||||
# when destinations are appended they require a valid name.
|
||||
if kwargs.get('name'):
|
||||
self.name = kwargs['name']
|
||||
else:
|
||||
self.name = get_or_increase_name(defaults.certificate_name(self.cn, self.issuer, self.not_before, self.not_after, self.san))
|
||||
|
||||
self.owner = kwargs['owner']
|
||||
self.body = kwargs['body']
|
||||
self.private_key = kwargs.get('private_key')
|
||||
self.chain = kwargs.get('chain')
|
||||
self.destinations = kwargs.get('destinations', [])
|
||||
self.notifications = kwargs.get('notifications', [])
|
||||
self.description = kwargs.get('description')
|
||||
self.roles = list(set(kwargs.get('roles', [])))
|
||||
self.replaces = kwargs.get('replacements', [])
|
||||
self.signing_algorithm = defaults.signing_algorithm(cert)
|
||||
self.bits = defaults.bitstrength(cert)
|
||||
self.serial = defaults.serial(cert)
|
||||
|
||||
for domain in defaults.domains(cert):
|
||||
self.domains.append(Domain(name=domain))
|
||||
|
||||
@property
|
||||
@ -303,7 +143,10 @@ def update_destinations(target, value, initiator):
|
||||
:return:
|
||||
"""
|
||||
destination_plugin = plugins.get(value.plugin_name)
|
||||
destination_plugin.upload(target.name, target.body, target.private_key, target.chain, value.options)
|
||||
try:
|
||||
destination_plugin.upload(target.name, target.body, target.private_key, target.chain, value.options)
|
||||
except Exception as e:
|
||||
current_app.logger.exception(e)
|
||||
|
||||
|
||||
@event.listens_for(Certificate.replaces, 'append')
|
||||
|
155
lemur/certificates/schemas.py
Normal file
155
lemur/certificates/schemas.py
Normal file
@ -0,0 +1,155 @@
|
||||
"""
|
||||
.. module: lemur.certificates.schemas
|
||||
:platform: unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
from flask import current_app
|
||||
from marshmallow import fields, validates_schema, post_load
|
||||
from marshmallow.exceptions import ValidationError
|
||||
|
||||
from lemur.schemas import AssociatedAuthoritySchema, AssociatedDestinationSchema, AssociatedCertificateSchema, \
|
||||
AssociatedNotificationSchema, PluginInputSchema, ExtensionSchema, AssociatedRoleSchema
|
||||
|
||||
from lemur.authorities.schemas import AuthorityNestedOutputSchema
|
||||
from lemur.destinations.schemas import DestinationNestedOutputSchema
|
||||
from lemur.notifications.schemas import NotificationNestedOutputSchema
|
||||
from lemur.roles.schemas import RoleNestedOutputSchema
|
||||
from lemur.domains.schemas import DomainNestedOutputSchema
|
||||
from lemur.users.schemas import UserNestedOutputSchema
|
||||
|
||||
from lemur.common.schema import LemurInputSchema, LemurOutputSchema
|
||||
from lemur.common import validators
|
||||
from lemur.notifications import service as notification_service
|
||||
|
||||
|
||||
class CertificateSchema(LemurInputSchema):
|
||||
owner = fields.Email(required=True)
|
||||
description = fields.String()
|
||||
|
||||
@post_load
|
||||
def default_notifications(self, data):
|
||||
if not data['notifications']:
|
||||
notification_name = "DEFAULT_{0}".format(data['owner'].split('@')[0].upper())
|
||||
data['notifications'] += notification_service.create_default_expiration_notifications(notification_name, [data['owner']])
|
||||
|
||||
notification_name = 'DEFAULT_SECURITY'
|
||||
data['notifications'] += notification_service.create_default_expiration_notifications(notification_name, current_app.config.get('LEMUR_SECURITY_TEAM_EMAIL'))
|
||||
return data
|
||||
|
||||
|
||||
class CertificateInputSchema(CertificateSchema):
|
||||
name = fields.String()
|
||||
common_name = fields.String(required=True, validate=validators.sensitive_domain)
|
||||
authority = fields.Nested(AssociatedAuthoritySchema, required=True)
|
||||
|
||||
validity_start = fields.DateTime()
|
||||
validity_end = fields.DateTime()
|
||||
validity_years = fields.Integer()
|
||||
|
||||
destinations = fields.Nested(AssociatedDestinationSchema, missing=[], many=True)
|
||||
notifications = fields.Nested(AssociatedNotificationSchema, missing=[], many=True)
|
||||
replacements = fields.Nested(AssociatedCertificateSchema, missing=[], many=True)
|
||||
roles = fields.Nested(AssociatedRoleSchema, missing=[], many=True)
|
||||
|
||||
csr = fields.String(validate=validators.csr)
|
||||
|
||||
# certificate body fields
|
||||
organizational_unit = fields.String(missing=lambda: current_app.config.get('LEMUR_DEFAULT_ORGANIZATIONAL_UNIT'))
|
||||
organization = fields.String(missing=lambda: current_app.config.get('LEMUR_DEFAULT_ORGANIZATION'))
|
||||
location = fields.String(missing=lambda: current_app.config.get('LEMUR_DEFAULT_LOCATION'))
|
||||
country = fields.String(missing=lambda: current_app.config.get('LEMUR_DEFAULT_COUNTRY'))
|
||||
state = fields.String(missing=lambda: current_app.config.get('LEMUR_DEFAULT_STATE'))
|
||||
|
||||
extensions = fields.Nested(ExtensionSchema)
|
||||
|
||||
@validates_schema
|
||||
def validate_dates(self, data):
|
||||
validators.dates(data)
|
||||
|
||||
|
||||
class CertificateEditInputSchema(CertificateSchema):
|
||||
active = fields.Boolean()
|
||||
destinations = fields.Nested(AssociatedDestinationSchema, missing=[], many=True)
|
||||
notifications = fields.Nested(AssociatedNotificationSchema, missing=[], many=True)
|
||||
replacements = fields.Nested(AssociatedCertificateSchema, missing=[], many=True)
|
||||
roles = fields.Nested(AssociatedRoleSchema, missing=[], many=True)
|
||||
|
||||
|
||||
class CertificateNestedOutputSchema(LemurOutputSchema):
|
||||
__envelope__ = False
|
||||
id = fields.Integer()
|
||||
active = fields.Boolean()
|
||||
bits = fields.Integer()
|
||||
body = fields.String()
|
||||
chain = fields.String()
|
||||
description = fields.String()
|
||||
name = fields.String()
|
||||
cn = fields.String()
|
||||
not_after = fields.DateTime()
|
||||
not_before = fields.DateTime()
|
||||
owner = fields.Email()
|
||||
status = fields.Boolean()
|
||||
creator = fields.Nested(UserNestedOutputSchema)
|
||||
issuer = fields.Nested(AuthorityNestedOutputSchema)
|
||||
|
||||
|
||||
class CertificateOutputSchema(LemurOutputSchema):
|
||||
id = fields.Integer()
|
||||
active = fields.Boolean()
|
||||
bits = fields.Integer()
|
||||
body = fields.String()
|
||||
chain = fields.String()
|
||||
deleted = fields.Boolean(default=False)
|
||||
description = fields.String()
|
||||
issuer = fields.String()
|
||||
name = fields.String()
|
||||
cn = fields.String()
|
||||
not_after = fields.DateTime()
|
||||
not_before = fields.DateTime()
|
||||
owner = fields.Email()
|
||||
san = fields.Boolean()
|
||||
serial = fields.String()
|
||||
signing_algorithm = fields.String()
|
||||
status = fields.Boolean()
|
||||
user = fields.Nested(UserNestedOutputSchema)
|
||||
domains = fields.Nested(DomainNestedOutputSchema, many=True)
|
||||
destinations = fields.Nested(DestinationNestedOutputSchema, many=True)
|
||||
notifications = fields.Nested(NotificationNestedOutputSchema, many=True)
|
||||
replaces = fields.Nested(CertificateNestedOutputSchema, many=True)
|
||||
authority = fields.Nested(AuthorityNestedOutputSchema)
|
||||
roles = fields.Nested(RoleNestedOutputSchema, many=True)
|
||||
endpoints = fields.List(fields.Dict(), missing=[])
|
||||
|
||||
|
||||
class CertificateUploadInputSchema(CertificateSchema):
|
||||
name = fields.String()
|
||||
active = fields.Boolean(missing=True)
|
||||
|
||||
private_key = fields.String(validate=validators.private_key)
|
||||
body = fields.String(required=True, validate=validators.public_certificate)
|
||||
chain = fields.String(validate=validators.public_certificate) # TODO this could be multiple certificates
|
||||
|
||||
destinations = fields.Nested(AssociatedDestinationSchema, missing=[], many=True)
|
||||
notifications = fields.Nested(AssociatedNotificationSchema, missing=[], many=True)
|
||||
replacements = fields.Nested(AssociatedCertificateSchema, missing=[], many=True)
|
||||
roles = fields.Nested(AssociatedRoleSchema, missing=[], many=True)
|
||||
|
||||
@validates_schema
|
||||
def keys(self, data):
|
||||
if data.get('destinations'):
|
||||
if not data.get('private_key'):
|
||||
raise ValidationError('Destinations require private key.')
|
||||
|
||||
|
||||
class CertificateExportInputSchema(LemurInputSchema):
|
||||
plugin = fields.Nested(PluginInputSchema)
|
||||
|
||||
|
||||
certificate_input_schema = CertificateInputSchema()
|
||||
certificate_output_schema = CertificateOutputSchema()
|
||||
certificates_output_schema = CertificateOutputSchema(many=True)
|
||||
certificate_upload_input_schema = CertificateUploadInputSchema()
|
||||
certificate_export_input_schema = CertificateExportInputSchema()
|
||||
certificate_edit_input_schema = CertificateEditInputSchema()
|
@ -11,6 +11,7 @@ from sqlalchemy import func, or_
|
||||
from flask import g, current_app
|
||||
|
||||
from lemur import database
|
||||
from lemur.extensions import metrics
|
||||
from lemur.plugins.base import plugins
|
||||
from lemur.certificates.models import Certificate
|
||||
|
||||
@ -20,6 +21,7 @@ from lemur.authorities.models import Authority
|
||||
from lemur.domains.models import Domain
|
||||
|
||||
from lemur.roles.models import Role
|
||||
from lemur.roles import service as role_service
|
||||
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
@ -87,11 +89,10 @@ def export(cert, export_plugin):
|
||||
:return:
|
||||
"""
|
||||
plugin = plugins.get(export_plugin['slug'])
|
||||
|
||||
return plugin.export(cert.body, cert.chain, cert.private_key, export_plugin['pluginOptions'])
|
||||
|
||||
|
||||
def update(cert_id, owner, description, active, destinations, notifications, replaces):
|
||||
def update(cert_id, owner, description, active, destinations, notifications, replaces, roles):
|
||||
"""
|
||||
Updates a certificate
|
||||
:param cert_id:
|
||||
@ -103,59 +104,49 @@ def update(cert_id, owner, description, active, destinations, notifications, rep
|
||||
:param replaces:
|
||||
:return:
|
||||
"""
|
||||
from lemur.notifications import service as notification_service
|
||||
cert = get(cert_id)
|
||||
cert.active = active
|
||||
cert.description = description
|
||||
|
||||
# we might have to create new notifications if the owner changes
|
||||
new_notifications = []
|
||||
# get existing names to remove
|
||||
notification_name = "DEFAULT_{0}".format(cert.owner.split('@')[0].upper())
|
||||
for n in notifications:
|
||||
if notification_name not in n.label:
|
||||
new_notifications.append(n)
|
||||
|
||||
notification_name = "DEFAULT_{0}".format(owner.split('@')[0].upper())
|
||||
new_notifications += notification_service.create_default_expiration_notifications(notification_name, owner)
|
||||
|
||||
cert.notifications = new_notifications
|
||||
|
||||
database.update_list(cert, 'destinations', Destination, destinations)
|
||||
database.update_list(cert, 'replaces', Certificate, replaces)
|
||||
|
||||
cert.destinations = destinations
|
||||
cert.notifications = notifications
|
||||
cert.roles = roles
|
||||
cert.replaces = replaces
|
||||
cert.owner = owner
|
||||
|
||||
return database.update(cert)
|
||||
|
||||
|
||||
def mint(issuer_options):
|
||||
def create_certificate_roles(**kwargs):
|
||||
# create an role for the owner and assign it
|
||||
owner_role = role_service.get_by_name(kwargs['owner'])
|
||||
if not owner_role:
|
||||
owner_role = role_service.create(
|
||||
kwargs['owner'],
|
||||
description="Auto generated role based on owner: {0}".format(kwargs['owner'])
|
||||
)
|
||||
|
||||
return [owner_role]
|
||||
|
||||
|
||||
def mint(**kwargs):
|
||||
"""
|
||||
Minting is slightly different for each authority.
|
||||
Support for multiple authorities is handled by individual plugins.
|
||||
|
||||
:param issuer_options:
|
||||
"""
|
||||
authority = issuer_options['authority']
|
||||
authority = kwargs['authority']
|
||||
|
||||
issuer = plugins.get(authority.plugin_name)
|
||||
|
||||
# allow the CSR to be specified by the user
|
||||
if not issuer_options.get('csr'):
|
||||
csr, private_key = create_csr(issuer_options)
|
||||
if not kwargs.get('csr'):
|
||||
csr, private_key = create_csr(**kwargs)
|
||||
else:
|
||||
csr = issuer_options.get('csr')
|
||||
csr = str(kwargs.get('csr'))
|
||||
private_key = None
|
||||
|
||||
issuer_options['creator'] = g.user.email
|
||||
cert_body, cert_chain = issuer.create_certificate(csr, issuer_options)
|
||||
|
||||
cert = Certificate(cert_body, private_key, cert_chain)
|
||||
|
||||
cert.user = g.user
|
||||
cert.authority = authority
|
||||
database.update(cert)
|
||||
return cert, private_key, cert_chain,
|
||||
cert_body, cert_chain = issuer.create_certificate(csr, kwargs)
|
||||
return cert_body, private_key, cert_chain,
|
||||
|
||||
|
||||
def import_certificate(**kwargs):
|
||||
@ -172,66 +163,32 @@ def import_certificate(**kwargs):
|
||||
:param kwargs:
|
||||
"""
|
||||
from lemur.users import service as user_service
|
||||
from lemur.notifications import service as notification_service
|
||||
cert = Certificate(kwargs['public_certificate'], chain=kwargs['intermediate_certificate'])
|
||||
|
||||
# TODO future source plugins might have a better understanding of who the 'owner' is we should support this
|
||||
cert.owner = kwargs.get('owner', current_app.config.get('LEMUR_SECURITY_TEAM_EMAIL')[0])
|
||||
cert.creator = kwargs.get('creator', user_service.get_by_email('lemur@nobody'))
|
||||
if not kwargs.get('owner'):
|
||||
kwargs['owner'] = current_app.config.get('LEMUR_SECURITY_TEAM_EMAIL')[0]
|
||||
|
||||
# NOTE existing certs may not follow our naming standard we will
|
||||
# overwrite the generated name with the actual cert name
|
||||
if kwargs.get('name'):
|
||||
cert.name = kwargs.get('name')
|
||||
if not kwargs.get('creator'):
|
||||
kwargs['creator'] = user_service.get_by_email('lemur@nobody')
|
||||
|
||||
if kwargs.get('user'):
|
||||
cert.user = kwargs.get('user')
|
||||
|
||||
notification_name = 'DEFAULT_SECURITY'
|
||||
notifications = notification_service.create_default_expiration_notifications(notification_name, current_app.config.get('LEMUR_SECURITY_TEAM_EMAIL'))
|
||||
database.update_list(cert, 'replaces', Certificate, kwargs['replacements'])
|
||||
cert.notifications = notifications
|
||||
|
||||
cert = database.create(cert)
|
||||
return cert
|
||||
return upload(**kwargs)
|
||||
|
||||
|
||||
def upload(**kwargs):
|
||||
"""
|
||||
Allows for pre-made certificates to be imported into Lemur.
|
||||
"""
|
||||
from lemur.notifications import service as notification_service
|
||||
cert = Certificate(
|
||||
kwargs.get('public_cert'),
|
||||
kwargs.get('private_key'),
|
||||
kwargs.get('intermediate_cert'),
|
||||
)
|
||||
roles = create_certificate_roles(**kwargs)
|
||||
|
||||
# we override the generated name if one is provided
|
||||
if kwargs.get('name'):
|
||||
cert.name = kwargs['name']
|
||||
if kwargs.get('roles'):
|
||||
kwargs['roles'] += roles
|
||||
else:
|
||||
kwargs['roles'] = roles
|
||||
|
||||
cert.description = kwargs.get('description')
|
||||
cert = Certificate(**kwargs)
|
||||
|
||||
cert.owner = kwargs['owner']
|
||||
cert = database.create(cert)
|
||||
|
||||
g.user.certificates.append(cert)
|
||||
|
||||
database.update_list(cert, 'destinations', Destination, kwargs.get('destinations'))
|
||||
database.update_list(cert, 'notifications', Notification, kwargs.get('notifications'))
|
||||
database.update_list(cert, 'replaces', Certificate, kwargs['replacements'])
|
||||
|
||||
# create default notifications for this certificate if none are provided
|
||||
notifications = []
|
||||
if not kwargs.get('notifications'):
|
||||
notification_name = "DEFAULT_{0}".format(cert.owner.split('@')[0].upper())
|
||||
notifications += notification_service.create_default_expiration_notifications(notification_name, [cert.owner])
|
||||
|
||||
notification_name = 'DEFAULT_SECURITY'
|
||||
notifications += notification_service.create_default_expiration_notifications(notification_name, current_app.config.get('LEMUR_SECURITY_TEAM_EMAIL'))
|
||||
cert.notifications = notifications
|
||||
|
||||
database.update(cert)
|
||||
return cert
|
||||
|
||||
@ -240,34 +197,26 @@ def create(**kwargs):
|
||||
"""
|
||||
Creates a new certificate.
|
||||
"""
|
||||
from lemur.notifications import service as notification_service
|
||||
cert, private_key, cert_chain = mint(kwargs)
|
||||
kwargs['creator'] = g.user.email
|
||||
cert_body, private_key, cert_chain = mint(**kwargs)
|
||||
kwargs['body'] = cert_body
|
||||
kwargs['private_key'] = private_key
|
||||
kwargs['chain'] = cert_chain
|
||||
|
||||
cert.owner = kwargs['owner']
|
||||
roles = create_certificate_roles(**kwargs)
|
||||
|
||||
if kwargs.get('roles'):
|
||||
kwargs['roles'] += roles
|
||||
else:
|
||||
kwargs['roles'] = roles
|
||||
|
||||
cert = Certificate(**kwargs)
|
||||
|
||||
database.create(cert)
|
||||
cert.description = kwargs['description']
|
||||
g.user.certificates.append(cert)
|
||||
database.update(g.user)
|
||||
cert.authority = kwargs['authority']
|
||||
database.commit()
|
||||
|
||||
# do this after the certificate has already been created because if it fails to upload to the third party
|
||||
# we do not want to lose the certificate information.
|
||||
database.update_list(cert, 'destinations', Destination, kwargs.get('destinations'))
|
||||
database.update_list(cert, 'replaces', Certificate, kwargs['replacements'])
|
||||
database.update_list(cert, 'notifications', Notification, kwargs.get('notifications'))
|
||||
|
||||
# create default notifications for this certificate if none are provided
|
||||
notifications = cert.notifications
|
||||
if not kwargs.get('notifications'):
|
||||
notification_name = "DEFAULT_{0}".format(cert.owner.split('@')[0].upper())
|
||||
notifications += notification_service.create_default_expiration_notifications(notification_name, [cert.owner])
|
||||
|
||||
notification_name = 'DEFAULT_SECURITY'
|
||||
notifications += notification_service.create_default_expiration_notifications(notification_name,
|
||||
current_app.config.get('LEMUR_SECURITY_TEAM_EMAIL'))
|
||||
cert.notifications = notifications
|
||||
|
||||
database.update(cert)
|
||||
metrics.send('certificate_issued', 'counter', 1, metric_tags=dict(owner=cert.owner, issuer=cert.issuer))
|
||||
return cert
|
||||
|
||||
|
||||
@ -343,7 +292,7 @@ def render(args):
|
||||
return database.sort_and_page(query, Certificate, args)
|
||||
|
||||
|
||||
def create_csr(csr_config):
|
||||
def create_csr(**csr_config):
|
||||
"""
|
||||
Given a list of domains create the appropriate csr
|
||||
for those domains
|
||||
@ -359,9 +308,9 @@ def create_csr(csr_config):
|
||||
# TODO When we figure out a better way to validate these options they should be parsed as str
|
||||
builder = x509.CertificateSigningRequestBuilder()
|
||||
builder = builder.subject_name(x509.Name([
|
||||
x509.NameAttribute(x509.OID_COMMON_NAME, csr_config['commonName']),
|
||||
x509.NameAttribute(x509.OID_COMMON_NAME, csr_config['common_name']),
|
||||
x509.NameAttribute(x509.OID_ORGANIZATION_NAME, csr_config['organization']),
|
||||
x509.NameAttribute(x509.OID_ORGANIZATIONAL_UNIT_NAME, csr_config['organizationalUnit']),
|
||||
x509.NameAttribute(x509.OID_ORGANIZATIONAL_UNIT_NAME, csr_config['organizational_unit']),
|
||||
x509.NameAttribute(x509.OID_COUNTRY_NAME, csr_config['country']),
|
||||
x509.NameAttribute(x509.OID_STATE_OR_PROVINCE_NAME, csr_config['state']),
|
||||
x509.NameAttribute(x509.OID_LOCALITY_NAME, csr_config['location']),
|
||||
@ -373,11 +322,11 @@ def create_csr(csr_config):
|
||||
|
||||
if csr_config.get('extensions'):
|
||||
for k, v in csr_config.get('extensions', {}).items():
|
||||
if k == 'subAltNames':
|
||||
if k == 'sub_alt_names':
|
||||
# map types to their x509 objects
|
||||
general_names = []
|
||||
for name in v['names']:
|
||||
if name['nameType'] == 'DNSName':
|
||||
if name['name_type'] == 'DNSName':
|
||||
general_names.append(x509.DNSName(name['value']))
|
||||
|
||||
builder = builder.add_extension(
|
||||
@ -432,7 +381,7 @@ def create_csr(csr_config):
|
||||
)
|
||||
|
||||
# serialize our private key and CSR
|
||||
pem = private_key.private_bytes(
|
||||
private_key = private_key.private_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PrivateFormat.TraditionalOpenSSL, # would like to use PKCS8 but AWS ELBs don't like it
|
||||
encryption_algorithm=serialization.NoEncryption()
|
||||
@ -442,7 +391,7 @@ def create_csr(csr_config):
|
||||
encoding=serialization.Encoding.PEM
|
||||
)
|
||||
|
||||
return csr, pem
|
||||
return csr, private_key
|
||||
|
||||
|
||||
def stats(**kwargs):
|
||||
@ -473,3 +422,23 @@ def stats(**kwargs):
|
||||
values.append(count)
|
||||
|
||||
return {'labels': keys, 'values': values}
|
||||
|
||||
|
||||
def get_account_number(arn):
|
||||
"""
|
||||
Extract the account number from an arn.
|
||||
|
||||
:param arn: IAM SSL arn
|
||||
:return: account number associated with ARN
|
||||
"""
|
||||
return arn.split(":")[4]
|
||||
|
||||
|
||||
def get_name_from_arn(arn):
|
||||
"""
|
||||
Extract the certificate name from an arn.
|
||||
|
||||
:param arn: IAM SSL arn
|
||||
:return: name of the certificate as uploaded to AWS
|
||||
"""
|
||||
return arn.split("/", 1)[1]
|
||||
|
@ -7,91 +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.certificates import service
|
||||
from lemur.authorities.models import Authority
|
||||
from flask.ext.restful import reqparse, Api
|
||||
|
||||
from lemur.common.schema import validate_schema
|
||||
from lemur.common.utils import paginated_parser
|
||||
|
||||
from lemur.auth.service import AuthenticatedResource
|
||||
from lemur.auth.permissions import ViewKeyPermission, AuthorityPermission, UpdateCertificatePermission
|
||||
from lemur.auth.permissions import ViewKeyPermission, AuthorityPermission, CertificatePermission
|
||||
|
||||
from lemur.certificates import service
|
||||
from lemur.certificates.schemas import certificate_input_schema, certificate_output_schema, \
|
||||
certificate_upload_input_schema, certificates_output_schema, certificate_export_input_schema, certificate_edit_input_schema
|
||||
|
||||
from lemur.roles import service as role_service
|
||||
from lemur.common.utils import marshal_items, paginated_parser
|
||||
from lemur.notifications.views import notification_list
|
||||
|
||||
|
||||
mod = Blueprint('certificates', __name__)
|
||||
api = Api(mod)
|
||||
|
||||
FIELDS = {
|
||||
'name': fields.String,
|
||||
'id': fields.Integer,
|
||||
'bits': fields.Integer,
|
||||
'deleted': fields.String,
|
||||
'issuer': fields.String,
|
||||
'serial': fields.String,
|
||||
'owner': fields.String,
|
||||
'chain': fields.String,
|
||||
'san': fields.String,
|
||||
'active': fields.Boolean,
|
||||
'description': fields.String,
|
||||
'notBefore': fields.DateTime(dt_format='iso8601', attribute='not_before'),
|
||||
'notAfter': fields.DateTime(dt_format='iso8601', attribute='not_after'),
|
||||
'cn': fields.String,
|
||||
'signingAlgorithm': fields.String(attribute='signing_algorithm'),
|
||||
'status': fields.String,
|
||||
'body': fields.String
|
||||
}
|
||||
|
||||
|
||||
def valid_authority(authority_options):
|
||||
"""
|
||||
Defends against invalid authorities
|
||||
|
||||
:param authority_options:
|
||||
:return: :raise ValueError:
|
||||
"""
|
||||
name = authority_options['name']
|
||||
authority = Authority.query.filter(Authority.name == name).one()
|
||||
|
||||
if not authority:
|
||||
raise ValueError("Unable to find authority specified")
|
||||
|
||||
if not authority.active:
|
||||
raise ValueError("Selected authority [{0}] is not currently active".format(name))
|
||||
|
||||
return authority
|
||||
|
||||
|
||||
def pem_str(value, name):
|
||||
"""
|
||||
Used to validate that the given string is a PEM formatted string
|
||||
|
||||
:param value:
|
||||
:param name:
|
||||
:return: :raise ValueError:
|
||||
"""
|
||||
try:
|
||||
x509.load_pem_x509_certificate(bytes(value), default_backend())
|
||||
except Exception:
|
||||
raise ValueError("The parameter '{0}' needs to be a valid PEM string".format(name))
|
||||
return value
|
||||
|
||||
|
||||
def private_key_str(value, name):
|
||||
"""
|
||||
User to validate that a given string is a RSA private key
|
||||
|
||||
:param value:
|
||||
:param name:
|
||||
:return: :raise ValueError:
|
||||
"""
|
||||
try:
|
||||
serialization.load_pem_private_key(bytes(value), None, backend=default_backend())
|
||||
except Exception:
|
||||
raise ValueError("The parameter '{0}' needs to be a valid RSA private key".format(name))
|
||||
return value
|
||||
|
||||
|
||||
class CertificatesList(AuthenticatedResource):
|
||||
""" Defines the 'certificates' endpoint """
|
||||
@ -100,7 +35,7 @@ class CertificatesList(AuthenticatedResource):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(CertificatesList, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
@validate_schema(None, certificates_output_schema)
|
||||
def get(self):
|
||||
"""
|
||||
.. http:get:: /certificates
|
||||
@ -124,37 +59,65 @@ class CertificatesList(AuthenticatedResource):
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "cert1",
|
||||
"description": "this is cert1",
|
||||
"bits": 2048,
|
||||
"deleted": false,
|
||||
"issuer": "ExampeInc.",
|
||||
"serial": "123450",
|
||||
"chain": "-----Begin ...",
|
||||
"body": "-----Begin ...",
|
||||
"san": true,
|
||||
"owner": 'bob@example.com",
|
||||
"active": true,
|
||||
"notBefore": "2015-06-05T17:09:39",
|
||||
"notAfter": "2015-06-10T17:09:39",
|
||||
"cn": "example.com",
|
||||
"status": "unknown"
|
||||
}
|
||||
]
|
||||
"items": [{
|
||||
"status": null,
|
||||
"cn": "*.test.example.net",
|
||||
"chain": "",
|
||||
"authority": {
|
||||
"active": true,
|
||||
"owner": "secure@example.com",
|
||||
"id": 1,
|
||||
"description": "verisign test authority",
|
||||
"name": "verisign"
|
||||
},
|
||||
"owner": "joe@example.com",
|
||||
"serial": "82311058732025924142789179368889309156",
|
||||
"id": 2288,
|
||||
"issuer": "SymantecCorporation",
|
||||
"notBefore": "2016-06-03T00:00:00+00:00",
|
||||
"notAfter": "2018-01-12T23:59:59+00:00",
|
||||
"destinations": [],
|
||||
"bits": 2048,
|
||||
"body": "-----BEGIN CERTIFICATE-----...",
|
||||
"description": null,
|
||||
"deleted": null,
|
||||
"notifications": [{
|
||||
"id": 1
|
||||
}]
|
||||
"signingAlgorithm": "sha256",
|
||||
"user": {
|
||||
"username": "jane",
|
||||
"active": true,
|
||||
"email": "jane@example.com",
|
||||
"id": 2
|
||||
},
|
||||
"active": true,
|
||||
"domains": [{
|
||||
"sensitive": false,
|
||||
"id": 1090,
|
||||
"name": "*.test.example.net"
|
||||
}],
|
||||
"replaces": [],
|
||||
"name": "WILDCARD.test.example.net-SymantecCorporation-20160603-20180112",
|
||||
"roles": [{
|
||||
"id": 464,
|
||||
"description": "This is a google group based role created by Lemur",
|
||||
"name": "joe@example.com"
|
||||
}],
|
||||
"san": null
|
||||
}],
|
||||
"total": 1
|
||||
}
|
||||
|
||||
: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 count: count number. default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
|
||||
"""
|
||||
parser = paginated_parser.copy()
|
||||
parser.add_argument('timeRange', type=int, dest='time_range', location='args')
|
||||
@ -168,8 +131,8 @@ class CertificatesList(AuthenticatedResource):
|
||||
args = parser.parse_args()
|
||||
return service.render(args)
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
def post(self):
|
||||
@validate_schema(certificate_input_schema, certificate_output_schema)
|
||||
def post(self, data=None):
|
||||
"""
|
||||
.. http:post:: /certificates
|
||||
|
||||
@ -183,91 +146,6 @@ class CertificatesList(AuthenticatedResource):
|
||||
Host: example.com
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
{
|
||||
"country": "US",
|
||||
"state": "CA",
|
||||
"location": "A Place",
|
||||
"organization": "ExampleInc.",
|
||||
"organizationalUnit": "Operations",
|
||||
"owner": "bob@example.com",
|
||||
"description": "test",
|
||||
"selectedAuthority": "timetest2",
|
||||
"csr",
|
||||
"authority": {
|
||||
"body": "-----BEGIN...",
|
||||
"name": "timetest2",
|
||||
"chain": "",
|
||||
"notBefore": "2015-06-05T15:20:59",
|
||||
"active": true,
|
||||
"id": 50,
|
||||
"notAfter": "2015-06-17T15:21:08",
|
||||
"description": "dsfdsf"
|
||||
},
|
||||
"notifications": [
|
||||
{
|
||||
"description": "Default 30 day expiration notification",
|
||||
"notificationOptions": [
|
||||
{
|
||||
"name": "interval",
|
||||
"required": true,
|
||||
"value": 30,
|
||||
"helpMessage": "Number of days to be alert before expiration.",
|
||||
"validation": "^\\d+$",
|
||||
"type": "int"
|
||||
},
|
||||
{
|
||||
"available": [
|
||||
"days",
|
||||
"weeks",
|
||||
"months"
|
||||
],
|
||||
"name": "unit",
|
||||
"required": true,
|
||||
"value": "days",
|
||||
"helpMessage": "Interval unit",
|
||||
"validation": "",
|
||||
"type": "select"
|
||||
},
|
||||
{
|
||||
"name": "recipients",
|
||||
"required": true,
|
||||
"value": "bob@example.com",
|
||||
"helpMessage": "Comma delimited list of email addresses",
|
||||
"validation": "^([\\w+-.%]+@[\\w-.]+\\.[A-Za-z]{2,4},?)+$",
|
||||
"type": "str"
|
||||
}
|
||||
],
|
||||
"label": "DEFAULT_KGLISSON_30_DAY",
|
||||
"pluginName": "email-notification",
|
||||
"active": true,
|
||||
"id": 7
|
||||
}
|
||||
],
|
||||
"extensions": {
|
||||
"basicConstraints": {},
|
||||
"keyUsage": {
|
||||
"isCritical": true,
|
||||
"useKeyEncipherment": true,
|
||||
"useDigitalSignature": true
|
||||
},
|
||||
"extendedKeyUsage": {
|
||||
"isCritical": true,
|
||||
"useServerAuthentication": true
|
||||
},
|
||||
"subjectKeyIdentifier": {
|
||||
"includeSKI": true
|
||||
},
|
||||
"subAltNames": {
|
||||
"names": []
|
||||
}
|
||||
},
|
||||
"commonName": "test",
|
||||
"validityStart": "2015-06-05T07:00:00.000Z",
|
||||
"validityEnd": "2015-06-16T07:00:00.000Z",
|
||||
"replacements": [
|
||||
{'id': 123}
|
||||
]
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
@ -278,24 +156,54 @@ class CertificatesList(AuthenticatedResource):
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"name": "cert1",
|
||||
"description": "this is cert1",
|
||||
"status": null,
|
||||
"cn": "*.test.example.net",
|
||||
"chain": "",
|
||||
"authority": {
|
||||
"active": true,
|
||||
"owner": "secure@example.com",
|
||||
"id": 1,
|
||||
"description": "verisign test authority",
|
||||
"name": "verisign"
|
||||
},
|
||||
"owner": "joe@example.com",
|
||||
"serial": "82311058732025924142789179368889309156",
|
||||
"id": 2288,
|
||||
"issuer": "SymantecCorporation",
|
||||
"notBefore": "2016-06-03T00:00:00+00:00",
|
||||
"notAfter": "2018-01-12T23:59:59+00:00",
|
||||
"destinations": [],
|
||||
"bits": 2048,
|
||||
"deleted": false,
|
||||
"issuer": "ExampeInc.",
|
||||
"serial": "123450",
|
||||
"chain": "-----Begin ...",
|
||||
"body": "-----Begin ...",
|
||||
"san": true,
|
||||
"owner": "jimbob@example.com",
|
||||
"active": false,
|
||||
"notBefore": "2015-06-05T17:09:39",
|
||||
"notAfter": "2015-06-10T17:09:39",
|
||||
"cn": "example.com",
|
||||
"status": "unknown"
|
||||
"body": "-----BEGIN CERTIFICATE-----...",
|
||||
"description": null,
|
||||
"deleted": null,
|
||||
"notifications": [{
|
||||
"id": 1
|
||||
}]
|
||||
"signingAlgorithm": "sha256",
|
||||
"user": {
|
||||
"username": "jane",
|
||||
"active": true,
|
||||
"email": "jane@example.com",
|
||||
"id": 2
|
||||
},
|
||||
"active": true,
|
||||
"domains": [{
|
||||
"sensitive": false,
|
||||
"id": 1090,
|
||||
"name": "*.test.example.net"
|
||||
}],
|
||||
"replaces": [],
|
||||
"name": "WILDCARD.test.example.net-SymantecCorporation-20160603-20180112",
|
||||
"roles": [{
|
||||
"id": 464,
|
||||
"description": "This is a google group based role created by Lemur",
|
||||
"name": "joe@example.com"
|
||||
}],
|
||||
"san": null
|
||||
}
|
||||
|
||||
|
||||
:arg extensions: extensions to be used in the certificate
|
||||
:arg description: description for new certificate
|
||||
:arg owner: owner email
|
||||
@ -306,44 +214,25 @@ class CertificatesList(AuthenticatedResource):
|
||||
:arg state: state for the CSR
|
||||
:arg location: location for the CSR
|
||||
:arg organization: organization for CSR
|
||||
:arg commonName: certiifcate common name
|
||||
:arg commonName: certificate common name
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
|
||||
"""
|
||||
self.reqparse.add_argument('extensions', type=dict, location='json')
|
||||
self.reqparse.add_argument('destinations', type=list, default=[], location='json')
|
||||
self.reqparse.add_argument('notifications', type=list, default=[], location='json')
|
||||
self.reqparse.add_argument('replacements', type=list, default=[], location='json')
|
||||
self.reqparse.add_argument('validityStart', type=str, location='json') # TODO validate
|
||||
self.reqparse.add_argument('validityEnd', type=str, location='json') # TODO validate
|
||||
self.reqparse.add_argument('authority', type=valid_authority, location='json', required=True)
|
||||
self.reqparse.add_argument('description', type=str, location='json')
|
||||
self.reqparse.add_argument('country', type=str, location='json', required=True)
|
||||
self.reqparse.add_argument('state', type=str, location='json', required=True)
|
||||
self.reqparse.add_argument('location', type=str, location='json', required=True)
|
||||
self.reqparse.add_argument('organization', type=str, location='json', required=True)
|
||||
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()
|
||||
|
||||
authority = args['authority']
|
||||
role = role_service.get_by_name(authority.owner)
|
||||
role = role_service.get_by_name(data['authority'].owner)
|
||||
|
||||
# all the authority role members should be allowed
|
||||
roles = [x.name for x in authority.roles]
|
||||
roles = [x.name for x in data['authority'].roles]
|
||||
|
||||
# allow "owner" roles by team DL
|
||||
roles.append(role)
|
||||
permission = AuthorityPermission(authority.id, roles)
|
||||
authority_permission = AuthorityPermission(data['authority'].id, roles)
|
||||
|
||||
if permission.can():
|
||||
return service.create(**args)
|
||||
if authority_permission.can():
|
||||
return service.create(**data)
|
||||
|
||||
return dict(message="You are not authorized to use {0}".format(args['authority'].name)), 403
|
||||
return dict(message="You are not authorized to use {0}".format(data['authority'].name)), 403
|
||||
|
||||
|
||||
class CertificatesUpload(AuthenticatedResource):
|
||||
@ -353,8 +242,8 @@ class CertificatesUpload(AuthenticatedResource):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(CertificatesUpload, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
def post(self):
|
||||
@validate_schema(certificate_upload_input_schema, certificate_output_schema)
|
||||
def post(self, data=None):
|
||||
"""
|
||||
.. http:post:: /certificates/upload
|
||||
|
||||
@ -388,23 +277,51 @@ class CertificatesUpload(AuthenticatedResource):
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"name": "cert1",
|
||||
"description": "this is cert1",
|
||||
"bits": 2048,
|
||||
"deleted": false,
|
||||
"issuer": "ExampeInc.",
|
||||
"serial": "123450",
|
||||
"chain": "-----Begin ...",
|
||||
"body": "-----Begin ...",
|
||||
"san": true,
|
||||
"owner": "joe@example.com",
|
||||
"active": true,
|
||||
"notBefore": "2015-06-05T17:09:39",
|
||||
"notAfter": "2015-06-10T17:09:39",
|
||||
"signingAlgorithm": "sha2"
|
||||
"cn": "example.com",
|
||||
"status": "unknown"
|
||||
"status": null,
|
||||
"cn": "*.test.example.net",
|
||||
"chain": "",
|
||||
"authority": {
|
||||
"active": true,
|
||||
"owner": "secure@example.com",
|
||||
"id": 1,
|
||||
"description": "verisign test authority",
|
||||
"name": "verisign"
|
||||
},
|
||||
"owner": "joe@example.com",
|
||||
"serial": "82311058732025924142789179368889309156",
|
||||
"id": 2288,
|
||||
"issuer": "SymantecCorporation",
|
||||
"notBefore": "2016-06-03T00:00:00+00:00",
|
||||
"notAfter": "2018-01-12T23:59:59+00:00",
|
||||
"destinations": [],
|
||||
"bits": 2048,
|
||||
"body": "-----BEGIN CERTIFICATE-----...",
|
||||
"description": null,
|
||||
"deleted": null,
|
||||
"notifications": [{
|
||||
"id": 1
|
||||
}]
|
||||
"signingAlgorithm": "sha256",
|
||||
"user": {
|
||||
"username": "jane",
|
||||
"active": true,
|
||||
"email": "jane@example.com",
|
||||
"id": 2
|
||||
},
|
||||
"active": true,
|
||||
"domains": [{
|
||||
"sensitive": false,
|
||||
"id": 1090,
|
||||
"name": "*.test.example.net"
|
||||
}],
|
||||
"replaces": [],
|
||||
"name": "WILDCARD.test.example.net-SymantecCorporation-20160603-20180112",
|
||||
"roles": [{
|
||||
"id": 464,
|
||||
"description": "This is a google group based role created by Lemur",
|
||||
"name": "joe@example.com"
|
||||
}],
|
||||
"san": null
|
||||
}
|
||||
|
||||
:arg owner: owner email for certificate
|
||||
@ -415,24 +332,14 @@ class CertificatesUpload(AuthenticatedResource):
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 403: unauthenticated
|
||||
:statuscode 200: no error
|
||||
"""
|
||||
self.reqparse.add_argument('description', type=str, location='json')
|
||||
self.reqparse.add_argument('owner', type=str, required=True, location='json')
|
||||
self.reqparse.add_argument('name', type=str, location='json')
|
||||
self.reqparse.add_argument('publicCert', type=pem_str, required=True, dest='public_cert', location='json')
|
||||
self.reqparse.add_argument('destinations', type=list, default=[], location='json')
|
||||
self.reqparse.add_argument('notifications', type=list, default=[], location='json')
|
||||
self.reqparse.add_argument('replacements', type=list, default=[], location='json')
|
||||
self.reqparse.add_argument('intermediateCert', type=pem_str, dest='intermediate_cert', location='json')
|
||||
self.reqparse.add_argument('privateKey', type=private_key_str, dest='private_key', location='json')
|
||||
|
||||
args = self.reqparse.parse_args()
|
||||
if args.get('destinations'):
|
||||
if args.get('private_key'):
|
||||
return service.upload(**args)
|
||||
"""
|
||||
if data.get('destinations'):
|
||||
if data.get('private_key'):
|
||||
return service.upload(**data)
|
||||
else:
|
||||
raise Exception("Private key must be provided in order to upload certificate to AWS")
|
||||
return service.upload(**args)
|
||||
return service.upload(**data)
|
||||
|
||||
|
||||
class CertificatesStats(AuthenticatedResource):
|
||||
@ -510,7 +417,7 @@ class Certificates(AuthenticatedResource):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(Certificates, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
@validate_schema(None, certificate_output_schema)
|
||||
def get(self, certificate_id):
|
||||
"""
|
||||
.. http:get:: /certificates/1
|
||||
@ -534,33 +441,62 @@ class Certificates(AuthenticatedResource):
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"name": "cert1",
|
||||
"description": "this is cert1",
|
||||
"status": null,
|
||||
"cn": "*.test.example.net",
|
||||
"chain": "",
|
||||
"authority": {
|
||||
"active": true,
|
||||
"owner": "secure@example.com",
|
||||
"id": 1,
|
||||
"description": "verisign test authority",
|
||||
"name": "verisign"
|
||||
},
|
||||
"owner": "joe@example.com",
|
||||
"serial": "82311058732025924142789179368889309156",
|
||||
"id": 2288,
|
||||
"issuer": "SymantecCorporation",
|
||||
"notBefore": "2016-06-03T00:00:00+00:00",
|
||||
"notAfter": "2018-01-12T23:59:59+00:00",
|
||||
"destinations": [],
|
||||
"bits": 2048,
|
||||
"deleted": false,
|
||||
"issuer": "ExampeInc.",
|
||||
"serial": "123450",
|
||||
"chain": "-----Begin ...",
|
||||
"body": "-----Begin ...",
|
||||
"san": true,
|
||||
"owner": "bob@example.com",
|
||||
"body": "-----BEGIN CERTIFICATE-----...",
|
||||
"description": null,
|
||||
"deleted": null,
|
||||
"notifications": [{
|
||||
"id": 1
|
||||
}]
|
||||
"signingAlgorithm": "sha256",
|
||||
"user": {
|
||||
"username": "jane",
|
||||
"active": true,
|
||||
"email": "jane@example.com",
|
||||
"id": 2
|
||||
},
|
||||
"active": true,
|
||||
"notBefore": "2015-06-05T17:09:39",
|
||||
"notAfter": "2015-06-10T17:09:39",
|
||||
"signingAlgorithm": "sha2",
|
||||
"cn": "example.com",
|
||||
"status": "unknown"
|
||||
"domains": [{
|
||||
"sensitive": false,
|
||||
"id": 1090,
|
||||
"name": "*.test.example.net"
|
||||
}],
|
||||
"replaces": [],
|
||||
"name": "WILDCARD.test.example.net-SymantecCorporation-20160603-20180112",
|
||||
"roles": [{
|
||||
"id": 464,
|
||||
"description": "This is a google group based role created by Lemur",
|
||||
"name": "joe@example.com"
|
||||
}],
|
||||
"san": null
|
||||
}
|
||||
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
|
||||
"""
|
||||
return service.get(certificate_id)
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
def put(self, certificate_id):
|
||||
@validate_schema(certificate_edit_input_schema, certificate_output_schema)
|
||||
def put(self, certificate_id, data=None):
|
||||
"""
|
||||
.. http:put:: /certificates/1
|
||||
|
||||
@ -591,50 +527,72 @@ class Certificates(AuthenticatedResource):
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"name": "cert1",
|
||||
"description": "this is cert1",
|
||||
"status": null,
|
||||
"cn": "*.test.example.net",
|
||||
"chain": "",
|
||||
"authority": {
|
||||
"active": true,
|
||||
"owner": "secure@example.com",
|
||||
"id": 1,
|
||||
"description": "verisign test authority",
|
||||
"name": "verisign"
|
||||
},
|
||||
"owner": "joe@example.com",
|
||||
"serial": "82311058732025924142789179368889309156",
|
||||
"id": 2288,
|
||||
"issuer": "SymantecCorporation",
|
||||
"notBefore": "2016-06-03T00:00:00+00:00",
|
||||
"notAfter": "2018-01-12T23:59:59+00:00",
|
||||
"destinations": [],
|
||||
"bits": 2048,
|
||||
"deleted": false,
|
||||
"issuer": "ExampeInc.",
|
||||
"serial": "123450",
|
||||
"chain": "-----Begin ...",
|
||||
"body": "-----Begin ...",
|
||||
"san": true,
|
||||
"owner": "jimbob@example.com",
|
||||
"active": false,
|
||||
"notBefore": "2015-06-05T17:09:39",
|
||||
"notAfter": "2015-06-10T17:09:39",
|
||||
"cn": "example.com",
|
||||
"status": "unknown",
|
||||
"body": "-----BEGIN CERTIFICATE-----...",
|
||||
"description": null,
|
||||
"deleted": null,
|
||||
"notifications": [{
|
||||
"id": 1
|
||||
}]
|
||||
"signingAlgorithm": "sha256",
|
||||
"user": {
|
||||
"username": "jane",
|
||||
"active": true,
|
||||
"email": "jane@example.com",
|
||||
"id": 2
|
||||
},
|
||||
"active": true,
|
||||
"domains": [{
|
||||
"sensitive": false,
|
||||
"id": 1090,
|
||||
"name": "*.test.example.net"
|
||||
}],
|
||||
"replaces": [],
|
||||
"name": "WILDCARD.test.example.net-SymantecCorporation-20160603-20180112",
|
||||
"roles": [{
|
||||
"id": 464,
|
||||
"description": "This is a google group based role created by Lemur",
|
||||
"name": "joe@example.com"
|
||||
}],
|
||||
"san": null
|
||||
}
|
||||
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
|
||||
"""
|
||||
self.reqparse.add_argument('active', type=bool, location='json')
|
||||
self.reqparse.add_argument('owner', type=str, location='json')
|
||||
self.reqparse.add_argument('description', type=str, location='json')
|
||||
self.reqparse.add_argument('destinations', type=list, default=[], location='json')
|
||||
self.reqparse.add_argument('notifications', type=notification_list, default=[], location='json')
|
||||
self.reqparse.add_argument('replacements', type=list, default=[], location='json')
|
||||
args = self.reqparse.parse_args()
|
||||
|
||||
cert = service.get(certificate_id)
|
||||
role = role_service.get_by_name(cert.owner)
|
||||
|
||||
permission = UpdateCertificatePermission(certificate_id, getattr(role, 'name', None))
|
||||
permission = CertificatePermission(cert.id, [x.name for x in cert.roles])
|
||||
|
||||
if permission.can():
|
||||
return service.update(
|
||||
certificate_id,
|
||||
args['owner'],
|
||||
args['description'],
|
||||
args['active'],
|
||||
args['destinations'],
|
||||
args['notifications'],
|
||||
args['replacements']
|
||||
data['owner'],
|
||||
data['description'],
|
||||
data['active'],
|
||||
data['destinations'],
|
||||
data['notifications'],
|
||||
data['replacements'],
|
||||
data['roles']
|
||||
)
|
||||
|
||||
return dict(message='You are not authorized to update this certificate'), 403
|
||||
@ -647,7 +605,7 @@ class NotificationCertificatesList(AuthenticatedResource):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(NotificationCertificatesList, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
@validate_schema(None, certificates_output_schema)
|
||||
def get(self, notification_id):
|
||||
"""
|
||||
.. http:get:: /notifications/1/certificates
|
||||
@ -671,38 +629,65 @@ class NotificationCertificatesList(AuthenticatedResource):
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "cert1",
|
||||
"description": "this is cert1",
|
||||
"bits": 2048,
|
||||
"deleted": false,
|
||||
"issuer": "ExampeInc.",
|
||||
"serial": "123450",
|
||||
"chain": "-----Begin ...",
|
||||
"body": "-----Begin ...",
|
||||
"san": true,
|
||||
"owner": 'bob@example.com",
|
||||
"active": true,
|
||||
"notBefore": "2015-06-05T17:09:39",
|
||||
"notAfter": "2015-06-10T17:09:39",
|
||||
"signingAlgorithm": "sha2",
|
||||
"cn": "example.com",
|
||||
"status": "unknown"
|
||||
}
|
||||
]
|
||||
"items": [{
|
||||
"status": null,
|
||||
"cn": "*.test.example.net",
|
||||
"chain": "",
|
||||
"authority": {
|
||||
"active": true,
|
||||
"owner": "secure@example.com",
|
||||
"id": 1,
|
||||
"description": "verisign test authority",
|
||||
"name": "verisign"
|
||||
},
|
||||
"owner": "joe@example.com",
|
||||
"serial": "82311058732025924142789179368889309156",
|
||||
"id": 2288,
|
||||
"issuer": "SymantecCorporation",
|
||||
"notBefore": "2016-06-03T00:00:00+00:00",
|
||||
"notAfter": "2018-01-12T23:59:59+00:00",
|
||||
"destinations": [],
|
||||
"bits": 2048,
|
||||
"body": "-----BEGIN CERTIFICATE-----...",
|
||||
"description": null,
|
||||
"deleted": null,
|
||||
"notifications": [{
|
||||
"id": 1
|
||||
}]
|
||||
"signingAlgorithm": "sha256",
|
||||
"user": {
|
||||
"username": "jane",
|
||||
"active": true,
|
||||
"email": "jane@example.com",
|
||||
"id": 2
|
||||
},
|
||||
"active": true,
|
||||
"domains": [{
|
||||
"sensitive": false,
|
||||
"id": 1090,
|
||||
"name": "*.test.example.net"
|
||||
}],
|
||||
"replaces": [],
|
||||
"name": "WILDCARD.test.example.net-SymantecCorporation-20160603-20180112",
|
||||
"roles": [{
|
||||
"id": 464,
|
||||
"description": "This is a google group based role created by Lemur",
|
||||
"name": "joe@example.com"
|
||||
}],
|
||||
"san": null
|
||||
}],
|
||||
"total": 1
|
||||
}
|
||||
|
||||
: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 count: count number default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
|
||||
"""
|
||||
parser = paginated_parser.copy()
|
||||
parser.add_argument('timeRange', type=int, dest='time_range', location='args')
|
||||
@ -723,7 +708,7 @@ class CertificatesReplacementsList(AuthenticatedResource):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(CertificatesReplacementsList, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
@validate_schema(None, certificates_output_schema)
|
||||
def get(self, certificate_id):
|
||||
"""
|
||||
.. http:get:: /certificates/1/replacements
|
||||
@ -746,29 +731,61 @@ class CertificatesReplacementsList(AuthenticatedResource):
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
|
||||
[{
|
||||
"id": 1,
|
||||
"name": "cert1",
|
||||
"description": "this is cert1",
|
||||
"bits": 2048,
|
||||
"deleted": false,
|
||||
"issuer": "ExampeInc.",
|
||||
"serial": "123450",
|
||||
"chain": "-----Begin ...",
|
||||
"body": "-----Begin ...",
|
||||
"san": true,
|
||||
"owner": "bob@example.com",
|
||||
"active": true,
|
||||
"notBefore": "2015-06-05T17:09:39",
|
||||
"notAfter": "2015-06-10T17:09:39",
|
||||
"signingAlgorithm": "sha2",
|
||||
"cn": "example.com",
|
||||
"status": "unknown"
|
||||
}]
|
||||
{
|
||||
"items": [{
|
||||
"status": null,
|
||||
"cn": "*.test.example.net",
|
||||
"chain": "",
|
||||
"authority": {
|
||||
"active": true,
|
||||
"owner": "secure@example.com",
|
||||
"id": 1,
|
||||
"description": "verisign test authority",
|
||||
"name": "verisign"
|
||||
},
|
||||
"owner": "joe@example.com",
|
||||
"serial": "82311058732025924142789179368889309156",
|
||||
"id": 2288,
|
||||
"issuer": "SymantecCorporation",
|
||||
"notBefore": "2016-06-03T00:00:00+00:00",
|
||||
"notAfter": "2018-01-12T23:59:59+00:00",
|
||||
"destinations": [],
|
||||
"bits": 2048,
|
||||
"body": "-----BEGIN CERTIFICATE-----...",
|
||||
"description": null,
|
||||
"deleted": null,
|
||||
"notifications": [{
|
||||
"id": 1
|
||||
}]
|
||||
"signingAlgorithm": "sha256",
|
||||
"user": {
|
||||
"username": "jane",
|
||||
"active": true,
|
||||
"email": "jane@example.com",
|
||||
"id": 2
|
||||
},
|
||||
"active": true,
|
||||
"domains": [{
|
||||
"sensitive": false,
|
||||
"id": 1090,
|
||||
"name": "*.test.example.net"
|
||||
}],
|
||||
"replaces": [],
|
||||
"name": "WILDCARD.test.example.net-SymantecCorporation-20160603-20180112",
|
||||
"roles": [{
|
||||
"id": 464,
|
||||
"description": "This is a google group based role created by Lemur",
|
||||
"name": "joe@example.com"
|
||||
}],
|
||||
"san": null
|
||||
}],
|
||||
"total": 1
|
||||
}
|
||||
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
|
||||
"""
|
||||
return service.get(certificate_id).replaces
|
||||
|
||||
@ -778,7 +795,8 @@ class CertificateExport(AuthenticatedResource):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(CertificateExport, self).__init__()
|
||||
|
||||
def post(self, certificate_id):
|
||||
@validate_schema(certificate_export_input_schema, None)
|
||||
def post(self, certificate_id, data=None):
|
||||
"""
|
||||
.. http:post:: /certificates/1/export
|
||||
|
||||
@ -842,21 +860,25 @@ class CertificateExport(AuthenticatedResource):
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
|
||||
"""
|
||||
self.reqparse.add_argument('export', type=dict, required=True, location='json')
|
||||
args = self.reqparse.parse_args()
|
||||
|
||||
cert = service.get(certificate_id)
|
||||
role = role_service.get_by_name(cert.owner)
|
||||
|
||||
permission = UpdateCertificatePermission(certificate_id, getattr(role, 'name', None))
|
||||
permission = CertificatePermission(cert.id, [x.name for x in cert.roles])
|
||||
|
||||
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))
|
||||
options = data['plugin']['plugin_options']
|
||||
plugin = data['plugin']['plugin_object']
|
||||
|
||||
return dict(message='You are not authorized to export this certificate'), 403
|
||||
if plugin.requires_key:
|
||||
if permission.can():
|
||||
extension, passphrase, data = plugin.export(cert.body, cert.chain, cert.private_key, options)
|
||||
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, options)
|
||||
|
||||
# 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')
|
||||
|
175
lemur/common/defaults.py
Normal file
175
lemur/common/defaults.py
Normal file
@ -0,0 +1,175 @@
|
||||
import sys
|
||||
from flask import current_app
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from lemur.constants import SAN_NAMING_TEMPLATE, DEFAULT_NAMING_TEMPLATE
|
||||
|
||||
|
||||
def parse_certificate(body):
|
||||
if sys.version_info >= (3, 0):
|
||||
return x509.load_pem_x509_certificate(body, default_backend())
|
||||
else:
|
||||
return x509.load_pem_x509_certificate(bytes(body), default_backend())
|
||||
|
||||
|
||||
def certificate_name(common_name, issuer, not_before, not_after, san):
|
||||
"""
|
||||
Create a name for our certificate. A naming standard
|
||||
is based on a series of templates. The name includes
|
||||
useful information such as Common Name, Validation dates,
|
||||
and Issuer.
|
||||
|
||||
:param san:
|
||||
:param common_name:
|
||||
:param not_after:
|
||||
:param issuer:
|
||||
:param not_before:
|
||||
:rtype : str
|
||||
:return:
|
||||
"""
|
||||
if san:
|
||||
t = SAN_NAMING_TEMPLATE
|
||||
else:
|
||||
t = DEFAULT_NAMING_TEMPLATE
|
||||
|
||||
temp = t.format(
|
||||
subject=common_name,
|
||||
issuer=issuer,
|
||||
not_before=not_before.strftime('%Y%m%d'),
|
||||
not_after=not_after.strftime('%Y%m%d')
|
||||
)
|
||||
|
||||
disallowed_chars = ''.join(c for c in map(chr, range(256)) if not c.isalnum())
|
||||
disallowed_chars = disallowed_chars.replace("-", "")
|
||||
disallowed_chars = disallowed_chars.replace(".", "")
|
||||
temp = temp.replace('*', "WILDCARD")
|
||||
|
||||
for c in disallowed_chars:
|
||||
temp = temp.replace(c, "")
|
||||
|
||||
# white space is silly too
|
||||
return temp.replace(" ", "-")
|
||||
|
||||
|
||||
def signing_algorithm(cert):
|
||||
return cert.signature_hash_algorithm.name
|
||||
|
||||
|
||||
def common_name(cert):
|
||||
"""
|
||||
Attempts to get a sane common name from a given certificate.
|
||||
|
||||
:param cert:
|
||||
:return: Common name or None
|
||||
"""
|
||||
return cert.subject.get_attributes_for_oid(
|
||||
x509.OID_COMMON_NAME
|
||||
)[0].value.strip()
|
||||
|
||||
|
||||
def domains(cert):
|
||||
"""
|
||||
Attempts to get an domains listed in a certificate.
|
||||
If 'subjectAltName' extension is not available we simply
|
||||
return the common name.
|
||||
|
||||
:param cert:
|
||||
:return: List of domains
|
||||
"""
|
||||
domains = []
|
||||
try:
|
||||
ext = cert.extensions.get_extension_for_oid(x509.OID_SUBJECT_ALTERNATIVE_NAME)
|
||||
entries = ext.value.get_values_for_type(x509.DNSName)
|
||||
for entry in entries:
|
||||
domains.append(entry)
|
||||
except Exception as e:
|
||||
current_app.logger.warning("Failed to get SubjectAltName: {0}".format(e))
|
||||
|
||||
return domains
|
||||
|
||||
|
||||
def serial(cert):
|
||||
"""
|
||||
Fetch the serial number from the certificate.
|
||||
|
||||
:param cert:
|
||||
:return: serial number
|
||||
"""
|
||||
return cert.serial
|
||||
|
||||
|
||||
def san(cert):
|
||||
"""
|
||||
Determines if a given certificate is a SAN certificate.
|
||||
SAN certificates are simply certificates that cover multiple domains.
|
||||
|
||||
:param cert:
|
||||
:return: Bool
|
||||
"""
|
||||
if len(domains(cert)) > 1:
|
||||
return True
|
||||
|
||||
|
||||
def is_wildcard(cert):
|
||||
"""
|
||||
Determines if certificate is a wildcard certificate.
|
||||
|
||||
:param cert:
|
||||
:return: Bool
|
||||
"""
|
||||
d = domains(cert)
|
||||
if len(d) == 1 and d[0][0:1] == "*":
|
||||
return True
|
||||
|
||||
if cert.subject.get_attributes_for_oid(x509.OID_COMMON_NAME)[0].value[0:1] == "*":
|
||||
return True
|
||||
|
||||
|
||||
def bitstrength(cert):
|
||||
"""
|
||||
Calculates a certificates public key bit length.
|
||||
|
||||
:param cert:
|
||||
:return: Integer
|
||||
"""
|
||||
return cert.public_key().key_size
|
||||
|
||||
|
||||
def issuer(cert):
|
||||
"""
|
||||
Gets a sane issuer from a given certificate.
|
||||
|
||||
:param cert:
|
||||
:return: Issuer
|
||||
"""
|
||||
delchars = ''.join(c for c in map(chr, range(256)) if not c.isalnum())
|
||||
try:
|
||||
issuer = str(cert.issuer.get_attributes_for_oid(x509.OID_ORGANIZATION_NAME)[0].value)
|
||||
for c in delchars:
|
||||
issuer = issuer.replace(c, "")
|
||||
return issuer
|
||||
except Exception as e:
|
||||
current_app.logger.error("Unable to get issuer! {0}".format(e))
|
||||
|
||||
|
||||
def not_before(cert):
|
||||
"""
|
||||
Gets the naive datetime of the certificates 'not_before' field.
|
||||
This field denotes the first date in time which the given certificate
|
||||
is valid.
|
||||
|
||||
:param cert:
|
||||
:return: Datetime
|
||||
"""
|
||||
return cert.not_valid_before
|
||||
|
||||
|
||||
def not_after(cert):
|
||||
"""
|
||||
Gets the naive datetime of the certificates 'not_after' field.
|
||||
This field denotes the last date in time which the given certificate
|
||||
is valid.
|
||||
|
||||
:return: Datetime
|
||||
"""
|
||||
return cert.not_valid_after
|
153
lemur/common/schema.py
Normal file
153
lemur/common/schema.py
Normal file
@ -0,0 +1,153 @@
|
||||
"""
|
||||
.. module: lemur.common.schema
|
||||
:platform: unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
|
||||
"""
|
||||
from functools import wraps
|
||||
from flask import request, current_app
|
||||
|
||||
from sqlalchemy.orm.collections import InstrumentedList
|
||||
|
||||
from marshmallow import Schema, post_dump, pre_load, pre_dump
|
||||
from inflection import camelize, underscore
|
||||
|
||||
|
||||
class LemurSchema(Schema):
|
||||
"""
|
||||
Base schema from which all grouper schema's inherit
|
||||
"""
|
||||
__envelope__ = True
|
||||
|
||||
def under(self, data, many=None):
|
||||
items = []
|
||||
if many:
|
||||
for i in data:
|
||||
items.append(
|
||||
{underscore(key): value for key, value in i.items()}
|
||||
)
|
||||
return items
|
||||
return {
|
||||
underscore(key): value
|
||||
for key, value in data.items()
|
||||
}
|
||||
|
||||
def camel(self, data, many=None):
|
||||
items = []
|
||||
if many:
|
||||
for i in data:
|
||||
items.append(
|
||||
{camelize(key, uppercase_first_letter=False): value for key, value in i.items()}
|
||||
)
|
||||
return items
|
||||
return {
|
||||
camelize(key, uppercase_first_letter=False): value
|
||||
for key, value in data.items()
|
||||
}
|
||||
|
||||
def wrap_with_envelope(self, data, many):
|
||||
if many:
|
||||
if 'total' in self.context.keys():
|
||||
return dict(total=self.context['total'], items=data)
|
||||
return data
|
||||
|
||||
|
||||
class LemurInputSchema(LemurSchema):
|
||||
@pre_load(pass_many=True)
|
||||
def preprocess(self, data, many):
|
||||
return self.under(data, many=many)
|
||||
|
||||
|
||||
class LemurOutputSchema(LemurSchema):
|
||||
@pre_load(pass_many=True)
|
||||
def preprocess(self, data, many):
|
||||
if many:
|
||||
data = self.unwrap_envelope(data, many)
|
||||
return self.under(data, many=many)
|
||||
|
||||
@pre_dump(pass_many=True)
|
||||
def unwrap_envelope(self, data, many):
|
||||
if many:
|
||||
if data:
|
||||
if isinstance(data, InstrumentedList) or isinstance(data, list):
|
||||
self.context['total'] = len(data)
|
||||
return data
|
||||
else:
|
||||
self.context['total'] = data['total']
|
||||
else:
|
||||
self.context['total'] = 0
|
||||
data = {'items': []}
|
||||
|
||||
return data['items']
|
||||
|
||||
return data
|
||||
|
||||
@post_dump(pass_many=True)
|
||||
def post_process(self, data, many):
|
||||
if data:
|
||||
data = self.camel(data, many=many)
|
||||
if self.__envelope__:
|
||||
return self.wrap_with_envelope(data, many=many)
|
||||
else:
|
||||
return data
|
||||
|
||||
|
||||
def format_errors(messages):
|
||||
errors = {}
|
||||
for k, v in messages.items():
|
||||
key = camelize(k, uppercase_first_letter=False)
|
||||
if isinstance(v, dict):
|
||||
errors[key] = format_errors(v)
|
||||
elif isinstance(v, list):
|
||||
errors[key] = v[0]
|
||||
return errors
|
||||
|
||||
|
||||
def wrap_errors(messages):
|
||||
errors = dict(message='Validation Error.')
|
||||
if messages.get('_schema'):
|
||||
errors['reasons'] = {'Schema': {'rule': messages['_schema']}}
|
||||
else:
|
||||
errors['reasons'] = format_errors(messages)
|
||||
return errors
|
||||
|
||||
|
||||
def validate_schema(input_schema, output_schema):
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if input_schema:
|
||||
if request.get_json():
|
||||
request_data = request.get_json()
|
||||
else:
|
||||
request_data = request.args
|
||||
|
||||
data, errors = input_schema.load(request_data)
|
||||
|
||||
if errors:
|
||||
return wrap_errors(errors), 400
|
||||
|
||||
kwargs['data'] = data
|
||||
|
||||
try:
|
||||
resp = f(*args, **kwargs)
|
||||
except Exception as e:
|
||||
current_app.logger.exception(e)
|
||||
return dict(message=e.message), 500
|
||||
|
||||
if isinstance(resp, tuple):
|
||||
return resp[0], resp[1]
|
||||
|
||||
if not resp:
|
||||
return dict(message="No data found"), 404
|
||||
|
||||
if output_schema:
|
||||
data = output_schema.dump(resp)
|
||||
return data.data, 200
|
||||
return resp, 200
|
||||
|
||||
return decorated_function
|
||||
return decorator
|
120
lemur/common/validators.py
Normal file
120
lemur/common/validators.py
Normal file
@ -0,0 +1,120 @@
|
||||
|
||||
import arrow
|
||||
from marshmallow.exceptions import ValidationError
|
||||
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
|
||||
from lemur.domains import service as domain_service
|
||||
from lemur.auth.permissions import SensitiveDomainPermission
|
||||
|
||||
|
||||
def public_certificate(body):
|
||||
"""
|
||||
Determines if specified string is valid public certificate.
|
||||
|
||||
:param body:
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
x509.load_pem_x509_certificate(bytes(body), default_backend())
|
||||
except Exception:
|
||||
raise ValidationError('Public certificate presented is not valid.')
|
||||
|
||||
|
||||
def private_key(key):
|
||||
"""
|
||||
User to validate that a given string is a RSA private key
|
||||
|
||||
:param key:
|
||||
:return: :raise ValueError:
|
||||
"""
|
||||
try:
|
||||
serialization.load_pem_private_key(bytes(key), None, backend=default_backend())
|
||||
except Exception:
|
||||
raise ValidationError('Private key presented is not valid.')
|
||||
|
||||
|
||||
def sensitive_domain(domain):
|
||||
"""
|
||||
Determines if domain has been marked as sensitive.
|
||||
:param domain:
|
||||
:return:
|
||||
"""
|
||||
domains = domain_service.get_by_name(domain)
|
||||
for domain in domains:
|
||||
# we only care about non-admins
|
||||
if not SensitiveDomainPermission().can():
|
||||
if domain.sensitive:
|
||||
raise ValidationError(
|
||||
'Domain {0} has been marked as sensitive, contact and administrator \
|
||||
to issue the certificate.'.format(domain))
|
||||
|
||||
|
||||
def oid_type(oid_type):
|
||||
"""
|
||||
Determines if the specified oid type is valid.
|
||||
:param oid_type:
|
||||
:return:
|
||||
"""
|
||||
valid_types = ['b64asn1', 'string', 'ia5string']
|
||||
if oid_type.lower() not in [o_type.lower() for o_type in valid_types]:
|
||||
raise ValidationError('Invalid Oid Type: {0} choose from {1}'.format(oid_type, ",".join(valid_types)))
|
||||
|
||||
|
||||
def sub_alt_type(alt_type):
|
||||
"""
|
||||
Determines if the specified subject alternate type is valid.
|
||||
:param alt_type:
|
||||
:return:
|
||||
"""
|
||||
valid_types = ['DNSName', 'IPAddress', 'uniFormResourceIdentifier', 'directoryName', 'rfc822Name', 'registrationID',
|
||||
'otherName', 'x400Address', 'EDIPartyName']
|
||||
if alt_type.lower() not in [a_type.lower() for a_type in valid_types]:
|
||||
raise ValidationError('Invalid SubAltName Type: {0} choose from {1}'.format(type, ",".join(valid_types)))
|
||||
|
||||
|
||||
def csr(data):
|
||||
"""
|
||||
Determines if the CSR is valid.
|
||||
:param data:
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
x509.load_pem_x509_csr(bytes(data), default_backend())
|
||||
except Exception:
|
||||
raise ValidationError('CSR presented is not valid.')
|
||||
|
||||
|
||||
def dates(data):
|
||||
if not data.get('validity_start') and data.get('validity_end'):
|
||||
raise ValidationError('If validity start is specified so must validity end.')
|
||||
|
||||
if not data.get('validity_end') and data.get('validity_start'):
|
||||
raise ValidationError('If validity end is specified so must validity start.')
|
||||
|
||||
if data.get('validity_end') and data.get('validity_years'):
|
||||
raise ValidationError('Cannot specify both validity end and validity years.')
|
||||
|
||||
if data.get('validity_start') and data.get('validity_end'):
|
||||
if not data['validity_start'] < data['validity_end']:
|
||||
raise ValidationError('Validity start must be before validity end.')
|
||||
|
||||
if data.get('authority'):
|
||||
if data.get('validity_start').replace(hour=0, minute=0, second=0, tzinfo=None) < data['authority'].authority_certificate.not_before.replace(hour=0, minute=0, second=0):
|
||||
raise ValidationError('Validity start must not be before {0}'.format(data['authority'].authority_certificate.not_before))
|
||||
|
||||
if data.get('validity_end').replace(hour=0, minute=0, second=0, tzinfo=None) > data['authority'].authority_certificate.not_after.replace(hour=0, minute=0, second=0):
|
||||
raise ValidationError('Validity end must not be after {0}'.format(data['authority'].authority_certificate.not_after))
|
||||
|
||||
if data.get('validity_years'):
|
||||
now = arrow.utcnow()
|
||||
end = now.replace(years=+data['validity_years'])
|
||||
|
||||
if data.get('authority'):
|
||||
if now.naive < data['authority'].authority_certificate.not_before:
|
||||
raise ValidationError('Validity start must not be before {0}'.format(data['authority'].authority_certificate.not_before))
|
||||
|
||||
if end.naive > data['authority'].authority_certificate.not_after:
|
||||
raise ValidationError('Validity end must not be after {0}'.format(data['authority'].authority_certificate.not_after))
|
@ -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
|
||||
|
||||
@ -237,9 +239,6 @@ def update_list(model, model_attr, item_model, items):
|
||||
"""
|
||||
ids = []
|
||||
|
||||
for i in items:
|
||||
ids.append(i['id'])
|
||||
|
||||
for i in getattr(model, model_attr):
|
||||
if i.id not in ids:
|
||||
getattr(model, model_attr).remove(i)
|
||||
@ -254,6 +253,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
|
||||
@ -273,4 +284,9 @@ def sort_and_page(query, model, args):
|
||||
if sort_by and sort_dir:
|
||||
query = sort(query, model, sort_by, sort_dir)
|
||||
|
||||
return paginate(query, page, count)
|
||||
total = query.count()
|
||||
|
||||
# offset calculated at zero
|
||||
page -= 1
|
||||
items = query.offset(count * page).limit(count).all()
|
||||
return dict(items=items, total=total)
|
||||
|
42
lemur/destinations/schemas.py
Normal file
42
lemur/destinations/schemas.py
Normal file
@ -0,0 +1,42 @@
|
||||
"""
|
||||
.. module: lemur.destinations.schemas
|
||||
:platform: unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
|
||||
from marshmallow import fields, post_dump
|
||||
from lemur.common.schema import LemurInputSchema, LemurOutputSchema
|
||||
from lemur.schemas import PluginInputSchema, PluginOutputSchema
|
||||
|
||||
|
||||
class DestinationInputSchema(LemurInputSchema):
|
||||
id = fields.Integer()
|
||||
label = fields.String(required=True)
|
||||
description = fields.String(required=True)
|
||||
active = fields.Boolean()
|
||||
plugin = fields.Nested(PluginInputSchema, required=True)
|
||||
|
||||
|
||||
class DestinationOutputSchema(LemurOutputSchema):
|
||||
id = fields.Integer()
|
||||
label = fields.String()
|
||||
description = fields.String()
|
||||
active = fields.Boolean()
|
||||
plugin = fields.Nested(PluginOutputSchema)
|
||||
options = fields.List(fields.Dict())
|
||||
|
||||
@post_dump
|
||||
def fill_object(self, data):
|
||||
data['plugin']['pluginOptions'] = data['options']
|
||||
return data
|
||||
|
||||
|
||||
class DestinationNestedOutputSchema(DestinationOutputSchema):
|
||||
__envelope__ = False
|
||||
|
||||
|
||||
destination_input_schema = DestinationInputSchema()
|
||||
destinations_output_schema = DestinationOutputSchema(many=True)
|
||||
destination_output_schema = DestinationOutputSchema()
|
@ -86,10 +86,6 @@ def get_all():
|
||||
|
||||
|
||||
def render(args):
|
||||
sort_by = args.pop('sort_by')
|
||||
sort_dir = args.pop('sort_dir')
|
||||
page = args.pop('page')
|
||||
count = args.pop('count')
|
||||
filt = args.pop('filter')
|
||||
certificate_id = args.pop('certificate_id', None)
|
||||
|
||||
@ -103,12 +99,7 @@ def render(args):
|
||||
terms = filt.split(';')
|
||||
query = database.filter(query, Destination, terms)
|
||||
|
||||
query = database.find_all(query, Destination, args)
|
||||
|
||||
if sort_by and sort_dir:
|
||||
query = database.sort(query, Destination, sort_by, sort_dir)
|
||||
|
||||
return database.paginate(query, page, count)
|
||||
return database.sort_and_page(query, Destination, args)
|
||||
|
||||
|
||||
def stats(**kwargs):
|
||||
|
@ -7,34 +7,28 @@
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
from flask import Blueprint
|
||||
from flask.ext.restful import Api, reqparse, fields
|
||||
from flask.ext.restful import Api, reqparse
|
||||
from lemur.destinations import service
|
||||
|
||||
from lemur.auth.service import AuthenticatedResource
|
||||
from lemur.auth.permissions import admin_permission
|
||||
from lemur.common.utils import paginated_parser, marshal_items
|
||||
from lemur.common.utils import paginated_parser
|
||||
|
||||
from lemur.common.schema import validate_schema
|
||||
from lemur.destinations.schemas import destinations_output_schema, destination_input_schema, destination_output_schema
|
||||
|
||||
|
||||
mod = Blueprint('destinations', __name__)
|
||||
api = Api(mod)
|
||||
|
||||
|
||||
FIELDS = {
|
||||
'description': fields.String,
|
||||
'destinationOptions': fields.Raw(attribute='options'),
|
||||
'pluginName': fields.String(attribute='plugin_name'),
|
||||
'label': fields.String,
|
||||
'id': fields.Integer,
|
||||
}
|
||||
|
||||
|
||||
class DestinationsList(AuthenticatedResource):
|
||||
""" Defines the 'destinations' endpoint """
|
||||
def __init__(self):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(DestinationsList, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
@validate_schema(None, destinations_output_schema)
|
||||
def get(self):
|
||||
"""
|
||||
.. http:get:: /destinations
|
||||
@ -58,32 +52,40 @@ class DestinationsList(AuthenticatedResource):
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"destinationOptions": [
|
||||
{
|
||||
"name": "accountNumber",
|
||||
"required": true,
|
||||
"value": 111111111112,
|
||||
"helpMessage": "Must be a valid AWS account number!",
|
||||
"validation": "/^[0-9]{12,12}$/",
|
||||
"type": "int"
|
||||
}
|
||||
],
|
||||
"pluginName": "aws-destination",
|
||||
"id": 3,
|
||||
"description": "test",
|
||||
"label": "test"
|
||||
}
|
||||
],
|
||||
"items": [{
|
||||
"description": "test",
|
||||
"options": [{
|
||||
"name": "accountNumber",
|
||||
"required": true,
|
||||
"value": "111111111111111",
|
||||
"helpMessage": "Must be a valid AWS account number!",
|
||||
"validation": "/^[0-9]{12,12}$/",
|
||||
"type": "str"
|
||||
}],
|
||||
"id": 4,
|
||||
"plugin": {
|
||||
"pluginOptions": [{
|
||||
"name": "accountNumber",
|
||||
"required": true,
|
||||
"value": "111111111111111",
|
||||
"helpMessage": "Must be a valid AWS account number!",
|
||||
"validation": "/^[0-9]{12,12}$/",
|
||||
"type": "str"
|
||||
}],
|
||||
"description": "Allow the uploading of certificates to AWS IAM",
|
||||
"slug": "aws-destination",
|
||||
"title": "AWS"
|
||||
},
|
||||
"label": "test546"
|
||||
}
|
||||
"total": 1
|
||||
}
|
||||
|
||||
: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 count: count number default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
"""
|
||||
@ -92,8 +94,8 @@ class DestinationsList(AuthenticatedResource):
|
||||
return service.render(args)
|
||||
|
||||
@admin_permission.require(http_exception=403)
|
||||
@marshal_items(FIELDS)
|
||||
def post(self):
|
||||
@validate_schema(destination_input_schema, destination_output_schema)
|
||||
def post(self, data=None):
|
||||
"""
|
||||
.. http:post:: /destinations
|
||||
|
||||
@ -108,20 +110,30 @@ class DestinationsList(AuthenticatedResource):
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
{
|
||||
"destinationOptions": [
|
||||
{
|
||||
"description": "test33",
|
||||
"options": [{
|
||||
"name": "accountNumber",
|
||||
"required": true,
|
||||
"value": "34324324",
|
||||
"helpMessage": "Must be a valid AWS account number!",
|
||||
"validation": "/^[0-9]{12,12}$/",
|
||||
"type": "str"
|
||||
}],
|
||||
"id": 4,
|
||||
"plugin": {
|
||||
"pluginOptions": [{
|
||||
"name": "accountNumber",
|
||||
"required": true,
|
||||
"value": 111111111112,
|
||||
"value": "34324324",
|
||||
"helpMessage": "Must be a valid AWS account number!",
|
||||
"validation": "/^[0-9]{12,12}$/",
|
||||
"type": "int"
|
||||
}
|
||||
],
|
||||
"pluginName": "aws-destination",
|
||||
"id": 3,
|
||||
"description": "test",
|
||||
"label": "test"
|
||||
"type": "str"
|
||||
}],
|
||||
"description": "Allow the uploading of certificates to AWS IAM",
|
||||
"slug": "aws-destination",
|
||||
"title": "AWS"
|
||||
},
|
||||
"label": "test546"
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
@ -133,20 +145,30 @@ class DestinationsList(AuthenticatedResource):
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"destinationOptions": [
|
||||
{
|
||||
"description": "test33",
|
||||
"options": [{
|
||||
"name": "accountNumber",
|
||||
"required": true,
|
||||
"value": "34324324",
|
||||
"helpMessage": "Must be a valid AWS account number!",
|
||||
"validation": "/^[0-9]{12,12}$/",
|
||||
"type": "str"
|
||||
}],
|
||||
"id": 4,
|
||||
"plugin": {
|
||||
"pluginOptions": [{
|
||||
"name": "accountNumber",
|
||||
"required": true,
|
||||
"value": 111111111112,
|
||||
"value": "111111111111111",
|
||||
"helpMessage": "Must be a valid AWS account number!",
|
||||
"validation": "/^[0-9]{12,12}$/",
|
||||
"type": "int"
|
||||
}
|
||||
],
|
||||
"pluginName": "aws-destination",
|
||||
"id": 3,
|
||||
"description": "test",
|
||||
"label": "test"
|
||||
"type": "str"
|
||||
}],
|
||||
"description": "Allow the uploading of certificates to AWS IAM",
|
||||
"slug": "aws-destination",
|
||||
"title": "AWS"
|
||||
},
|
||||
"label": "test546"
|
||||
}
|
||||
|
||||
:arg label: human readable account label
|
||||
@ -154,12 +176,7 @@ class DestinationsList(AuthenticatedResource):
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
"""
|
||||
self.reqparse.add_argument('label', type=str, location='json', required=True)
|
||||
self.reqparse.add_argument('plugin', type=dict, location='json', required=True)
|
||||
self.reqparse.add_argument('description', type=str, location='json')
|
||||
|
||||
args = self.reqparse.parse_args()
|
||||
return service.create(args['label'], args['plugin']['slug'], args['plugin']['pluginOptions'], args['description'])
|
||||
return service.create(data['label'], data['plugin']['slug'], data['plugin']['plugin_options'], data['description'])
|
||||
|
||||
|
||||
class Destinations(AuthenticatedResource):
|
||||
@ -167,7 +184,7 @@ class Destinations(AuthenticatedResource):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(Destinations, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
@validate_schema(None, destination_output_schema)
|
||||
def get(self, destination_id):
|
||||
"""
|
||||
.. http:get:: /destinations/1
|
||||
@ -191,20 +208,30 @@ class Destinations(AuthenticatedResource):
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"destinationOptions": [
|
||||
{
|
||||
"description": "test",
|
||||
"options": [{
|
||||
"name": "accountNumber",
|
||||
"required": true,
|
||||
"value": "111111111111111",
|
||||
"helpMessage": "Must be a valid AWS account number!",
|
||||
"validation": "/^[0-9]{12,12}$/",
|
||||
"type": "str"
|
||||
}],
|
||||
"id": 4,
|
||||
"plugin": {
|
||||
"pluginOptions": [{
|
||||
"name": "accountNumber",
|
||||
"required": true,
|
||||
"value": 111111111112,
|
||||
"value": "111111111111111",
|
||||
"helpMessage": "Must be a valid AWS account number!",
|
||||
"validation": "/^[0-9]{12,12}$/",
|
||||
"type": "int"
|
||||
}
|
||||
],
|
||||
"pluginName": "aws-destination",
|
||||
"id": 3,
|
||||
"description": "test",
|
||||
"label": "test"
|
||||
"type": "str"
|
||||
}],
|
||||
"description": "Allow the uploading of certificates to AWS IAM",
|
||||
"slug": "aws-destination",
|
||||
"title": "AWS"
|
||||
},
|
||||
"label": "test546"
|
||||
}
|
||||
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
@ -213,8 +240,8 @@ class Destinations(AuthenticatedResource):
|
||||
return service.get(destination_id)
|
||||
|
||||
@admin_permission.require(http_exception=403)
|
||||
@marshal_items(FIELDS)
|
||||
def put(self, destination_id):
|
||||
@validate_schema(destination_input_schema, destination_output_schema)
|
||||
def put(self, destination_id, data=None):
|
||||
"""
|
||||
.. http:put:: /destinations/1
|
||||
|
||||
@ -228,23 +255,35 @@ class Destinations(AuthenticatedResource):
|
||||
Host: example.com
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
|
||||
{
|
||||
"destinationOptions": [
|
||||
{
|
||||
"description": "test33",
|
||||
"options": [{
|
||||
"name": "accountNumber",
|
||||
"required": true,
|
||||
"value": "34324324",
|
||||
"helpMessage": "Must be a valid AWS account number!",
|
||||
"validation": "/^[0-9]{12,12}$/",
|
||||
"type": "str"
|
||||
}],
|
||||
"id": 4,
|
||||
"plugin": {
|
||||
"pluginOptions": [{
|
||||
"name": "accountNumber",
|
||||
"required": true,
|
||||
"value": 111111111112,
|
||||
"value": "34324324",
|
||||
"helpMessage": "Must be a valid AWS account number!",
|
||||
"validation": "/^[0-9]{12,12}$/",
|
||||
"type": "int"
|
||||
}
|
||||
],
|
||||
"pluginName": "aws-destination",
|
||||
"id": 3,
|
||||
"description": "test",
|
||||
"label": "test"
|
||||
"type": "str"
|
||||
}],
|
||||
"description": "Allow the uploading of certificates to AWS IAM",
|
||||
"slug": "aws-destination",
|
||||
"title": "AWS"
|
||||
},
|
||||
"label": "test546"
|
||||
}
|
||||
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
@ -254,20 +293,30 @@ class Destinations(AuthenticatedResource):
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"destinationOptions": [
|
||||
{
|
||||
"description": "test",
|
||||
"options": [{
|
||||
"name": "accountNumber",
|
||||
"required": true,
|
||||
"value": "111111111111111",
|
||||
"helpMessage": "Must be a valid AWS account number!",
|
||||
"validation": "/^[0-9]{12,12}$/",
|
||||
"type": "str"
|
||||
}],
|
||||
"id": 4,
|
||||
"plugin": {
|
||||
"pluginOptions": [{
|
||||
"name": "accountNumber",
|
||||
"required": true,
|
||||
"value": 111111111112,
|
||||
"value": "111111111111111",
|
||||
"helpMessage": "Must be a valid AWS account number!",
|
||||
"validation": "/^[0-9]{12,12}$/",
|
||||
"type": "int"
|
||||
}
|
||||
],
|
||||
"pluginName": "aws-destination",
|
||||
"id": 3,
|
||||
"description": "test",
|
||||
"label": "test"
|
||||
"type": "str"
|
||||
}],
|
||||
"description": "Allow the uploading of certificates to AWS IAM",
|
||||
"slug": "aws-destination",
|
||||
"title": "AWS"
|
||||
},
|
||||
"label": "test546"
|
||||
}
|
||||
|
||||
:arg accountNumber: aws account number
|
||||
@ -276,12 +325,7 @@ class Destinations(AuthenticatedResource):
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
"""
|
||||
self.reqparse.add_argument('label', type=str, location='json', required=True)
|
||||
self.reqparse.add_argument('plugin', type=dict, location='json', required=True)
|
||||
self.reqparse.add_argument('description', type=str, location='json')
|
||||
|
||||
args = self.reqparse.parse_args()
|
||||
return service.update(destination_id, args['label'], args['plugin']['pluginOptions'], args['description'])
|
||||
return service.update(destination_id, data['label'], data['plugin']['plugin_options'], data['description'])
|
||||
|
||||
@admin_permission.require(http_exception=403)
|
||||
def delete(self, destination_id):
|
||||
@ -294,7 +338,7 @@ class CertificateDestinations(AuthenticatedResource):
|
||||
def __init__(self):
|
||||
super(CertificateDestinations, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
@validate_schema(None, destination_output_schema)
|
||||
def get(self, certificate_id):
|
||||
"""
|
||||
.. http:get:: /certificates/1/destinations
|
||||
@ -318,32 +362,40 @@ class CertificateDestinations(AuthenticatedResource):
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"destinationOptions": [
|
||||
{
|
||||
"name": "accountNumber",
|
||||
"required": true,
|
||||
"value": 111111111112,
|
||||
"helpMessage": "Must be a valid AWS account number!",
|
||||
"validation": "/^[0-9]{12,12}$/",
|
||||
"type": "int"
|
||||
}
|
||||
],
|
||||
"pluginName": "aws-destination",
|
||||
"id": 3,
|
||||
"description": "test",
|
||||
"label": "test"
|
||||
}
|
||||
],
|
||||
"items": [{
|
||||
"description": "test",
|
||||
"options": [{
|
||||
"name": "accountNumber",
|
||||
"required": true,
|
||||
"value": "111111111111111",
|
||||
"helpMessage": "Must be a valid AWS account number!",
|
||||
"validation": "/^[0-9]{12,12}$/",
|
||||
"type": "str"
|
||||
}],
|
||||
"id": 4,
|
||||
"plugin": {
|
||||
"pluginOptions": [{
|
||||
"name": "accountNumber",
|
||||
"required": true,
|
||||
"value": "111111111111111",
|
||||
"helpMessage": "Must be a valid AWS account number!",
|
||||
"validation": "/^[0-9]{12,12}$/",
|
||||
"type": "str"
|
||||
}],
|
||||
"description": "Allow the uploading of certificates to AWS IAM",
|
||||
"slug": "aws-destination",
|
||||
"title": "AWS"
|
||||
},
|
||||
"label": "test546"
|
||||
}
|
||||
"total": 1
|
||||
}
|
||||
|
||||
: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 count: count number default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
"""
|
||||
|
@ -7,7 +7,7 @@
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
|
||||
"""
|
||||
from sqlalchemy import Column, Integer, String
|
||||
from sqlalchemy import Column, Integer, String, Boolean
|
||||
|
||||
from lemur.database import db
|
||||
|
||||
@ -16,11 +16,4 @@ class Domain(db.Model):
|
||||
__tablename__ = 'domains'
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String(256))
|
||||
|
||||
def as_dict(self):
|
||||
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
|
||||
|
||||
def serialize(self):
|
||||
blob = self.as_dict()
|
||||
blob['certificates'] = [x.id for x in self.certificate]
|
||||
return blob
|
||||
sensitive = Column(Boolean, default=False)
|
||||
|
35
lemur/domains/schemas.py
Normal file
35
lemur/domains/schemas.py
Normal file
@ -0,0 +1,35 @@
|
||||
"""
|
||||
.. module: lemur.domains.schemas
|
||||
:platform: unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
from marshmallow import fields
|
||||
from lemur.common.schema import LemurInputSchema, LemurOutputSchema
|
||||
from lemur.schemas import AssociatedCertificateSchema
|
||||
|
||||
# from lemur.certificates.schemas import CertificateNestedOutputSchema
|
||||
|
||||
|
||||
class DomainInputSchema(LemurInputSchema):
|
||||
id = fields.Integer()
|
||||
name = fields.String(required=True)
|
||||
sensitive = fields.Boolean()
|
||||
certificates = fields.Nested(AssociatedCertificateSchema, many=True, missing=[])
|
||||
|
||||
|
||||
class DomainOutputSchema(LemurOutputSchema):
|
||||
id = fields.Integer()
|
||||
name = fields.String()
|
||||
sensitive = fields.Boolean()
|
||||
# certificates = fields.Nested(CertificateNestedOutputSchema, many=True, missing=[])
|
||||
|
||||
|
||||
class DomainNestedOutputSchema(DomainOutputSchema):
|
||||
__envelope__ = False
|
||||
|
||||
|
||||
domain_input_schema = DomainInputSchema()
|
||||
domain_output_schema = DomainOutputSchema()
|
||||
domains_output_schema = DomainOutputSchema(many=True)
|
@ -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
|
||||
@ -40,11 +77,6 @@ def render(args):
|
||||
:return:
|
||||
"""
|
||||
query = database.session_query(Domain).join(Certificate, Domain.certificate)
|
||||
|
||||
sort_by = args.pop('sort_by')
|
||||
sort_dir = args.pop('sort_dir')
|
||||
page = args.pop('page')
|
||||
count = args.pop('count')
|
||||
filt = args.pop('filter')
|
||||
certificate_id = args.pop('certificate_id', None)
|
||||
|
||||
@ -55,9 +87,4 @@ def render(args):
|
||||
if certificate_id:
|
||||
query = query.filter(Certificate.id == certificate_id)
|
||||
|
||||
query = database.find_all(query, Domain, args)
|
||||
|
||||
if sort_by and sort_dir:
|
||||
query = database.sort(query, Domain, sort_by, sort_dir)
|
||||
|
||||
return database.paginate(query, page, count)
|
||||
return database.sort_and_page(query, Domain, args)
|
||||
|
@ -8,17 +8,16 @@
|
||||
|
||||
"""
|
||||
from flask import Blueprint
|
||||
from flask.ext.restful import reqparse, Api, fields
|
||||
from flask.ext.restful import reqparse, Api
|
||||
|
||||
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
|
||||
from lemur.common.schema import validate_schema
|
||||
from lemur.common.utils import paginated_parser
|
||||
|
||||
FIELDS = {
|
||||
'id': fields.Integer,
|
||||
'name': fields.String
|
||||
}
|
||||
from lemur.domains.schemas import domain_input_schema, domain_output_schema, domains_output_schema
|
||||
|
||||
mod = Blueprint('domains', __name__)
|
||||
api = Api(mod)
|
||||
@ -29,7 +28,7 @@ class DomainsList(AuthenticatedResource):
|
||||
def __init__(self):
|
||||
super(DomainsList, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
@validate_schema(None, domains_output_schema)
|
||||
def get(self):
|
||||
"""
|
||||
.. http:get:: /domains
|
||||
@ -57,10 +56,12 @@ class DomainsList(AuthenticatedResource):
|
||||
{
|
||||
"id": 1,
|
||||
"name": "www.example.com",
|
||||
"sensitive": false
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "www.example2.com",
|
||||
"sensitive": false
|
||||
}
|
||||
]
|
||||
"total": 2
|
||||
@ -68,9 +69,9 @@ 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 limit: limit number. default is 10
|
||||
:query page: int default is 1
|
||||
:query filter: key value pair format is k;v
|
||||
:query count: count number. default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
@ -79,13 +80,58 @@ class DomainsList(AuthenticatedResource):
|
||||
args = parser.parse_args()
|
||||
return service.render(args)
|
||||
|
||||
@validate_schema(domain_input_schema, domain_output_schema)
|
||||
def post(self, data=None):
|
||||
"""
|
||||
.. 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 count: count number default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
"""
|
||||
return service.create(data['name'], data['sensitive'])
|
||||
|
||||
|
||||
class Domains(AuthenticatedResource):
|
||||
def __init__(self):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(Domains, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
@validate_schema(None, domain_output_schema)
|
||||
def get(self, domain_id):
|
||||
"""
|
||||
.. http:get:: /domains/1
|
||||
@ -111,6 +157,7 @@ class Domains(AuthenticatedResource):
|
||||
{
|
||||
"id": 1,
|
||||
"name": "www.example.com",
|
||||
"sensitive": false
|
||||
}
|
||||
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
@ -119,13 +166,56 @@ class Domains(AuthenticatedResource):
|
||||
"""
|
||||
return service.get(domain_id)
|
||||
|
||||
@validate_schema(domain_input_schema, domain_output_schema)
|
||||
def put(self, domain_id, data=None):
|
||||
"""
|
||||
.. 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
|
||||
"""
|
||||
if SensitiveDomainPermission().can():
|
||||
return service.update(domain_id, data['name'], data['sensitive'])
|
||||
|
||||
return dict(message='You are not authorized to modify this domain'), 403
|
||||
|
||||
|
||||
class CertificateDomains(AuthenticatedResource):
|
||||
""" Defines the 'domains' endpoint """
|
||||
def __init__(self):
|
||||
super(CertificateDomains, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
@validate_schema(None, domains_output_schema)
|
||||
def get(self, certificate_id):
|
||||
"""
|
||||
.. http:get:: /certificates/1/domains
|
||||
@ -153,10 +243,12 @@ class CertificateDomains(AuthenticatedResource):
|
||||
{
|
||||
"id": 1,
|
||||
"name": "www.example.com",
|
||||
"sensitive": false
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "www.example2.com",
|
||||
"sensitive": false
|
||||
}
|
||||
]
|
||||
"total": 2
|
||||
@ -164,9 +256,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 count: count number default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
|
@ -3,17 +3,20 @@
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
"""
|
||||
from flask.ext.sqlalchemy import SQLAlchemy
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
db = SQLAlchemy()
|
||||
|
||||
from flask.ext.migrate import Migrate
|
||||
from flask_migrate import Migrate
|
||||
migrate = Migrate()
|
||||
|
||||
from flask.ext.bcrypt import Bcrypt
|
||||
from flask_bcrypt import Bcrypt
|
||||
bcrypt = Bcrypt()
|
||||
|
||||
from flask.ext.principal import Principal
|
||||
from flask_principal import Principal
|
||||
principal = Principal()
|
||||
|
||||
from flask_mail import Mail
|
||||
smtp_mail = Mail()
|
||||
|
||||
from lemur.metrics import Metrics
|
||||
metrics = Metrics()
|
||||
|
@ -19,7 +19,7 @@ from logging.handlers import RotatingFileHandler
|
||||
|
||||
from flask import Flask
|
||||
from lemur.common.health import mod as health
|
||||
from lemur.extensions import db, migrate, principal, smtp_mail
|
||||
from lemur.extensions import db, migrate, principal, smtp_mail, metrics
|
||||
|
||||
|
||||
DEFAULT_BLUEPRINTS = (
|
||||
@ -112,6 +112,7 @@ def configure_extensions(app):
|
||||
migrate.init_app(app, db)
|
||||
principal.init_app(app)
|
||||
smtp_mail.init_app(app)
|
||||
metrics.init_app(app)
|
||||
|
||||
|
||||
def configure_blueprints(app, blueprints):
|
||||
|
174
lemur/manage.py
174
lemur/manage.py
@ -4,6 +4,7 @@ import os
|
||||
import sys
|
||||
import base64
|
||||
import time
|
||||
import arrow
|
||||
import requests
|
||||
import json
|
||||
from gunicorn.config import make_settings
|
||||
@ -24,8 +25,12 @@ 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.service import get_name_from_arn
|
||||
from lemur.certificates.verify import verify_string
|
||||
from lemur.sources.service import sync
|
||||
|
||||
from lemur.plugins.lemur_aws import elb
|
||||
|
||||
from lemur.sources.service import sync as source_sync
|
||||
|
||||
from lemur import create_app
|
||||
|
||||
@ -184,7 +189,7 @@ def generate_settings():
|
||||
|
||||
|
||||
@manager.option('-s', '--sources', dest='labels')
|
||||
def sync_sources(labels):
|
||||
def sync(labels):
|
||||
"""
|
||||
Attempts to run several methods Certificate discovery. This is
|
||||
run on a periodic basis and updates the Lemur datastore with the
|
||||
@ -213,9 +218,9 @@ def sync_sources(labels):
|
||||
labels = labels.split(",")
|
||||
|
||||
if labels[0] == 'all':
|
||||
sync()
|
||||
source_sync()
|
||||
else:
|
||||
sync(labels=labels)
|
||||
source_sync(labels=labels)
|
||||
|
||||
sys.stdout.write(
|
||||
"[+] Finished syncing sources. Run Time: {time}\n".format(
|
||||
@ -312,7 +317,7 @@ class InitializeApp(Command):
|
||||
|
||||
class CreateUser(Command):
|
||||
"""
|
||||
This command allows for the creation of a new user within Lemur
|
||||
This command allows for the creation of a new user within Lemur.
|
||||
"""
|
||||
option_list = (
|
||||
Option('-u', '--username', dest='username', required=True),
|
||||
@ -328,18 +333,46 @@ class CreateUser(Command):
|
||||
if role_obj:
|
||||
role_objs.append(role_obj)
|
||||
else:
|
||||
sys.stderr.write("[!] Cannot find role {0}".format(r))
|
||||
sys.stderr.write("[!] Cannot find role {0}\n".format(r))
|
||||
sys.exit(1)
|
||||
|
||||
password1 = prompt_pass("Password")
|
||||
password2 = prompt_pass("Confirm Password")
|
||||
|
||||
if password1 != password2:
|
||||
sys.stderr.write("[!] Passwords do not match")
|
||||
sys.stderr.write("[!] Passwords do not match!\n")
|
||||
sys.exit(1)
|
||||
|
||||
user_service.create(username, password1, email, active, None, role_objs)
|
||||
sys.stdout.write("[+] Created new user: {0}".format(username))
|
||||
sys.stdout.write("[+] Created new user: {0}\n".format(username))
|
||||
|
||||
|
||||
class ResetPassword(Command):
|
||||
"""
|
||||
This command allows you to reset a user's password.
|
||||
"""
|
||||
option_list = (
|
||||
Option('-u', '--username', dest='username', required=True),
|
||||
)
|
||||
|
||||
def run(self, username):
|
||||
user = user_service.get_by_username(username)
|
||||
|
||||
if not user:
|
||||
sys.stderr.write("[!] No user found for username: {0}\n".format(username))
|
||||
sys.exit(1)
|
||||
|
||||
sys.stderr.write("[+] Resetting password for {0}\n".format(username))
|
||||
password1 = prompt_pass("Password")
|
||||
password2 = prompt_pass("Confirm Password")
|
||||
|
||||
if password1 != password2:
|
||||
sys.stderr.write("[!] Passwords do not match\n")
|
||||
sys.exit(1)
|
||||
|
||||
user.password = password1
|
||||
user.hash_password()
|
||||
database.commit()
|
||||
|
||||
|
||||
class CreateRole(Command):
|
||||
@ -386,7 +419,7 @@ class LemurServer(Command):
|
||||
settings = make_settings()
|
||||
options = (
|
||||
Option(*klass.cli, action=klass.action)
|
||||
for setting, klass in settings.iteritems() if klass.cli
|
||||
for setting, klass in settings.items() if klass.cli
|
||||
)
|
||||
|
||||
return options
|
||||
@ -510,23 +543,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 +819,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():
|
||||
@ -769,9 +869,11 @@ def main():
|
||||
manager.add_command("db", MigrateCommand)
|
||||
manager.add_command("init", InitializeApp())
|
||||
manager.add_command("create_user", CreateUser())
|
||||
manager.add_command("reset_password", ResetPassword())
|
||||
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__":
|
||||
|
32
lemur/metrics.py
Normal file
32
lemur/metrics.py
Normal file
@ -0,0 +1,32 @@
|
||||
"""
|
||||
.. module: lemur.metrics
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
"""
|
||||
from flask import current_app
|
||||
from lemur.plugins.base import plugins
|
||||
|
||||
|
||||
class Metrics(object):
|
||||
"""
|
||||
:param app: The Flask application object. Defaults to None.
|
||||
"""
|
||||
_providers = []
|
||||
|
||||
def __init__(self, app=None):
|
||||
if app is not None:
|
||||
self.init_app(app)
|
||||
|
||||
def init_app(self, app):
|
||||
"""Initializes the application with the extension.
|
||||
|
||||
:param app: The Flask application object.
|
||||
"""
|
||||
self._providers = app.config.get('METRIC_PROVIDERS', [])
|
||||
|
||||
def send(self, metric_name, metric_type, metric_value, *args, **kwargs):
|
||||
for provider in self._providers:
|
||||
current_app.logger.debug(
|
||||
"Sending metric '{metric}' to the {provider} provider.".format(metric=metric_name, provider=provider))
|
||||
p = plugins.get(provider)
|
||||
p.submit(metric_name, metric_type, metric_value, *args, **kwargs)
|
@ -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 ###
|
131
lemur/migrations/versions/3307381f3b88_.py
Normal file
131
lemur/migrations/versions/3307381f3b88_.py
Normal file
@ -0,0 +1,131 @@
|
||||
"""
|
||||
Refactor authority columns and associates an authorities root certificate with a certificate stored in the
|
||||
certificate tables.
|
||||
|
||||
Migrates existing authority owners to associated roles.
|
||||
Migrates existing certificate owners to associated role.
|
||||
|
||||
Revision ID: 3307381f3b88
|
||||
Revises: 412b22cb656a
|
||||
Create Date: 2016-05-20 17:33:04.360687
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '3307381f3b88'
|
||||
down_revision = '412b22cb656a'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.sql import text
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
|
||||
def upgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.alter_column('authorities', 'owner',
|
||||
existing_type=sa.VARCHAR(length=128),
|
||||
nullable=True)
|
||||
op.drop_column('authorities', 'not_after')
|
||||
op.drop_column('authorities', 'bits')
|
||||
op.drop_column('authorities', 'cn')
|
||||
op.drop_column('authorities', 'not_before')
|
||||
op.add_column('certificates', sa.Column('root_authority_id', sa.Integer(), nullable=True))
|
||||
op.alter_column('certificates', 'body',
|
||||
existing_type=sa.TEXT(),
|
||||
nullable=False)
|
||||
op.alter_column('certificates', 'owner',
|
||||
existing_type=sa.VARCHAR(length=128),
|
||||
nullable=True)
|
||||
op.drop_constraint(u'certificates_authority_id_fkey', 'certificates', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'certificates', 'authorities', ['authority_id'], ['id'], ondelete='CASCADE')
|
||||
op.create_foreign_key(None, 'certificates', 'authorities', ['root_authority_id'], ['id'], ondelete='CASCADE')
|
||||
### end Alembic commands ###
|
||||
|
||||
# link existing certificate to their authority certificates
|
||||
conn = op.get_bind()
|
||||
for id, body, owner in conn.execute(text('select id, body, owner from authorities')):
|
||||
if not owner:
|
||||
owner = "lemur@nobody"
|
||||
|
||||
# look up certificate by body, if duplications are found, pick one
|
||||
stmt = text('select id from certificates where body=:body')
|
||||
stmt = stmt.bindparams(body=body)
|
||||
root_certificate = conn.execute(stmt).fetchone()
|
||||
if root_certificate:
|
||||
stmt = text('update certificates set root_authority_id=:root_authority_id where id=:id')
|
||||
stmt = stmt.bindparams(root_authority_id=id, id=root_certificate[0])
|
||||
op.execute(stmt)
|
||||
|
||||
# link owner roles to their authorities
|
||||
stmt = text('select id from roles where name=:name')
|
||||
stmt = stmt.bindparams(name=owner)
|
||||
owner_role = conn.execute(stmt).fetchone()
|
||||
|
||||
if not owner_role:
|
||||
stmt = text('insert into roles (name, description) values (:name, :description)')
|
||||
stmt = stmt.bindparams(name=owner, description='Lemur generated role or existing owner.')
|
||||
op.execute(stmt)
|
||||
|
||||
stmt = text('select id from roles where name=:name')
|
||||
stmt = stmt.bindparams(name=owner)
|
||||
owner_role = conn.execute(stmt).fetchone()
|
||||
|
||||
stmt = text('select * from roles_authorities where role_id=:role_id and authority_id=:authority_id')
|
||||
stmt = stmt.bindparams(role_id=owner_role[0], authority_id=id)
|
||||
exists = conn.execute(stmt).fetchone()
|
||||
|
||||
if not exists:
|
||||
stmt = text('insert into roles_authorities (role_id, authority_id) values (:role_id, :authority_id)')
|
||||
stmt = stmt.bindparams(role_id=owner_role[0], authority_id=id)
|
||||
op.execute(stmt)
|
||||
|
||||
# link owner roles to their certificates
|
||||
for id, owner in conn.execute(text('select id, owner from certificates')):
|
||||
if not owner:
|
||||
owner = "lemur@nobody"
|
||||
|
||||
stmt = text('select id from roles where name=:name')
|
||||
stmt = stmt.bindparams(name=owner)
|
||||
owner_role = conn.execute(stmt).fetchone()
|
||||
|
||||
if not owner_role:
|
||||
stmt = text('insert into roles (name, description) values (:name, :description)')
|
||||
stmt = stmt.bindparams(name=owner, description='Lemur generated role or existing owner.')
|
||||
op.execute(stmt)
|
||||
|
||||
# link owner roles to their authorities
|
||||
stmt = text('select id from roles where name=:name')
|
||||
stmt = stmt.bindparams(name=owner)
|
||||
owner_role = conn.execute(stmt).fetchone()
|
||||
|
||||
stmt = text('select * from roles_certificates where role_id=:role_id and certificate_id=:certificate_id')
|
||||
stmt = stmt.bindparams(role_id=owner_role[0], certificate_id=id)
|
||||
exists = conn.execute(stmt).fetchone()
|
||||
|
||||
if not exists:
|
||||
stmt = text('insert into roles_certificates (role_id, certificate_id) values (:role_id, :certificate_id)')
|
||||
stmt = stmt.bindparams(role_id=owner_role[0], certificate_id=id)
|
||||
op.execute(stmt)
|
||||
|
||||
|
||||
def downgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_constraint(None, 'certificates', type_='foreignkey')
|
||||
op.drop_constraint(None, 'certificates', type_='foreignkey')
|
||||
op.create_foreign_key(u'certificates_authority_id_fkey', 'certificates', 'authorities', ['authority_id'], ['id'])
|
||||
op.alter_column('certificates', 'owner',
|
||||
existing_type=sa.VARCHAR(length=128),
|
||||
nullable=True)
|
||||
op.alter_column('certificates', 'body',
|
||||
existing_type=sa.TEXT(),
|
||||
nullable=True)
|
||||
op.drop_column('certificates', 'root_authority_id')
|
||||
op.add_column('authorities', sa.Column('not_before', postgresql.TIMESTAMP(), autoincrement=False, nullable=True))
|
||||
op.add_column('authorities', sa.Column('cn', sa.VARCHAR(length=128), autoincrement=False, nullable=True))
|
||||
op.add_column('authorities', sa.Column('bits', sa.INTEGER(), autoincrement=False, nullable=True))
|
||||
op.add_column('authorities', sa.Column('not_after', postgresql.TIMESTAMP(), autoincrement=False, nullable=True))
|
||||
op.alter_column('authorities', 'owner',
|
||||
existing_type=sa.VARCHAR(length=128),
|
||||
nullable=True)
|
||||
### end Alembic commands ###
|
@ -8,7 +8,7 @@ Create Date: 2015-11-30 15:40:19.827272
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '33de094da890'
|
||||
down_revision = 'ed422fc58ba'
|
||||
down_revision = None
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
@ -1,49 +0,0 @@
|
||||
"""Refactors Accounts to Destinations
|
||||
|
||||
Revision ID: 3b718f59b8ce
|
||||
Revises: None
|
||||
Create Date: 2015-07-09 17:44:55.626221
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '3b718f59b8ce'
|
||||
down_revision = None
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('certificate_account_associations')
|
||||
op.drop_table('accounts')
|
||||
op.add_column('destinations', sa.Column('plugin_name', sa.String(length=32), nullable=True))
|
||||
op.drop_index('ix_elbs_account_id', table_name='elbs')
|
||||
op.drop_constraint(u'elbs_account_id_fkey', 'elbs', type_='foreignkey')
|
||||
op.drop_column('elbs', 'account_id')
|
||||
### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('elbs', sa.Column('account_id', sa.BIGINT(), autoincrement=False, nullable=True))
|
||||
op.create_foreign_key(u'elbs_account_id_fkey', 'elbs', 'accounts', ['account_id'], ['id'])
|
||||
op.create_index('ix_elbs_account_id', 'elbs', ['account_id'], unique=False)
|
||||
op.drop_column('destinations', 'plugin_name')
|
||||
op.create_table('accounts',
|
||||
sa.Column('id', sa.INTEGER(), server_default=sa.text(u"nextval('accounts_id_seq'::regclass)"), nullable=False),
|
||||
sa.Column('account_number', sa.VARCHAR(length=32), autoincrement=False, nullable=True),
|
||||
sa.Column('label', sa.VARCHAR(length=32), autoincrement=False, nullable=True),
|
||||
sa.Column('notes', sa.TEXT(), autoincrement=False, nullable=True),
|
||||
sa.PrimaryKeyConstraint('id', name=u'accounts_pkey'),
|
||||
sa.UniqueConstraint('account_number', name=u'accounts_account_number_key'),
|
||||
postgresql_ignore_search_path=False
|
||||
)
|
||||
op.create_table('certificate_account_associations',
|
||||
sa.Column('account_id', sa.INTEGER(), autoincrement=False, nullable=True),
|
||||
sa.Column('certificate_id', sa.INTEGER(), autoincrement=False, nullable=True),
|
||||
sa.ForeignKeyConstraint(['account_id'], [u'accounts.id'], name=u'certificate_account_associations_account_id_fkey', ondelete=u'CASCADE'),
|
||||
sa.ForeignKeyConstraint(['certificate_id'], [u'certificates.id'], name=u'certificate_account_associations_certificate_id_fkey', ondelete=u'CASCADE')
|
||||
)
|
||||
### end Alembic commands ###
|
63
lemur/migrations/versions/412b22cb656a_.py
Normal file
63
lemur/migrations/versions/412b22cb656a_.py
Normal file
@ -0,0 +1,63 @@
|
||||
"""
|
||||
|
||||
Revision ID: 412b22cb656a
|
||||
Revises: 4c50b903d1ae
|
||||
Create Date: 2016-05-17 17:37:41.210232
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '412b22cb656a'
|
||||
down_revision = '4c50b903d1ae'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.sql import text
|
||||
|
||||
|
||||
def upgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('roles_authorities',
|
||||
sa.Column('authority_id', sa.Integer(), nullable=True),
|
||||
sa.Column('role_id', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['authority_id'], ['authorities.id'], ),
|
||||
sa.ForeignKeyConstraint(['role_id'], ['roles.id'], )
|
||||
)
|
||||
op.create_index('roles_authorities_ix', 'roles_authorities', ['authority_id', 'role_id'], unique=True)
|
||||
op.create_table('roles_certificates',
|
||||
sa.Column('certificate_id', sa.Integer(), nullable=True),
|
||||
sa.Column('role_id', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['certificate_id'], ['certificates.id'], ),
|
||||
sa.ForeignKeyConstraint(['role_id'], ['roles.id'], )
|
||||
)
|
||||
op.create_index('roles_certificates_ix', 'roles_certificates', ['certificate_id', 'role_id'], unique=True)
|
||||
op.create_index('certificate_associations_ix', 'certificate_associations', ['domain_id', 'certificate_id'], unique=True)
|
||||
op.create_index('certificate_destination_associations_ix', 'certificate_destination_associations', ['destination_id', 'certificate_id'], unique=True)
|
||||
op.create_index('certificate_notification_associations_ix', 'certificate_notification_associations', ['notification_id', 'certificate_id'], unique=True)
|
||||
op.create_index('certificate_replacement_associations_ix', 'certificate_replacement_associations', ['certificate_id', 'certificate_id'], unique=True)
|
||||
op.create_index('certificate_source_associations_ix', 'certificate_source_associations', ['source_id', 'certificate_id'], unique=True)
|
||||
op.create_index('roles_users_ix', 'roles_users', ['user_id', 'role_id'], unique=True)
|
||||
|
||||
### end Alembic commands ###
|
||||
|
||||
# migrate existing authority_id relationship to many_to_many
|
||||
conn = op.get_bind()
|
||||
for id, authority_id in conn.execute(text('select id, authority_id from roles where authority_id is not null')):
|
||||
stmt = text('insert into roles_authorities (role_id, authority_id) values (:role_id, :authority_id)')
|
||||
stmt = stmt.bindparams(role_id=id, authority_id=authority_id)
|
||||
op.execute(stmt)
|
||||
|
||||
|
||||
def downgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index('roles_users_ix', table_name='roles_users')
|
||||
op.drop_index('certificate_source_associations_ix', table_name='certificate_source_associations')
|
||||
op.drop_index('certificate_replacement_associations_ix', table_name='certificate_replacement_associations')
|
||||
op.drop_index('certificate_notification_associations_ix', table_name='certificate_notification_associations')
|
||||
op.drop_index('certificate_destination_associations_ix', table_name='certificate_destination_associations')
|
||||
op.drop_index('certificate_associations_ix', table_name='certificate_associations')
|
||||
op.drop_index('roles_certificates_ix', table_name='roles_certificates')
|
||||
op.drop_table('roles_certificates')
|
||||
op.drop_index('roles_authorities_ix', table_name='roles_authorities')
|
||||
op.drop_table('roles_authorities')
|
||||
### end Alembic commands ###
|
@ -1,26 +0,0 @@
|
||||
"""Adding certificate signing algorithm
|
||||
|
||||
Revision ID: 4bcfa2c36623
|
||||
Revises: 1ff763f5b80b
|
||||
Create Date: 2015-10-06 10:03:47.993204
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '4bcfa2c36623'
|
||||
down_revision = '1ff763f5b80b'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('certificates', sa.Column('signing_algorithm', sa.String(length=128), nullable=True))
|
||||
### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('certificates', 'signing_algorithm')
|
||||
### end Alembic commands ###
|
26
lemur/migrations/versions/4c50b903d1ae_.py
Normal file
26
lemur/migrations/versions/4c50b903d1ae_.py
Normal file
@ -0,0 +1,26 @@
|
||||
"""Adding ability to mark domains as 'sensitive'
|
||||
|
||||
Revision ID: 4c50b903d1ae
|
||||
Revises: 33de094da890
|
||||
Create Date: 2015-12-30 10:19:30.057791
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '4c50b903d1ae'
|
||||
down_revision = '33de094da890'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
def upgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('domains', sa.Column('sensitive', sa.Boolean(), nullable=True))
|
||||
### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('domains', 'sensitive')
|
||||
### end Alembic commands ###
|
@ -1,41 +0,0 @@
|
||||
"""Adding notifications
|
||||
|
||||
Revision ID: 4c8915e461b3
|
||||
Revises: 3b718f59b8ce
|
||||
Create Date: 2015-07-24 14:34:57.316273
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '4c8915e461b3'
|
||||
down_revision = '3b718f59b8ce'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
import sqlalchemy_utils
|
||||
|
||||
|
||||
def upgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('notifications',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('label', sa.String(length=128), nullable=True),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('options', sqlalchemy_utils.types.json.JSONType(), nullable=True),
|
||||
sa.Column('active', sa.Boolean(), nullable=True),
|
||||
sa.Column('plugin_name', sa.String(length=32), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.drop_column(u'certificates', 'challenge')
|
||||
op.drop_column(u'certificates', 'csr_config')
|
||||
### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column(u'certificates', sa.Column('csr_config', sa.TEXT(), autoincrement=False, nullable=True))
|
||||
op.add_column(u'certificates', sa.Column('challenge', postgresql.BYTEA(), autoincrement=False, nullable=True))
|
||||
op.drop_table('notifications')
|
||||
### end Alembic commands ###
|
@ -1,31 +0,0 @@
|
||||
"""Creating a one-to-many relationship for notifications
|
||||
|
||||
Revision ID: 4dc5ddd111b8
|
||||
Revises: 4c8915e461b3
|
||||
Create Date: 2015-07-24 15:02:04.398262
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '4dc5ddd111b8'
|
||||
down_revision = '4c8915e461b3'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('certificate_notification_associations',
|
||||
sa.Column('notification_id', sa.Integer(), nullable=True),
|
||||
sa.Column('certificate_id', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['certificate_id'], ['certificates.id'], ondelete='cascade'),
|
||||
sa.ForeignKeyConstraint(['notification_id'], ['notifications.id'], ondelete='cascade')
|
||||
)
|
||||
### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('certificate_notification_associations')
|
||||
### end Alembic commands ###
|
@ -1,255 +0,0 @@
|
||||
"""Migrates the private key encrypted column from AES to fernet encryption scheme.
|
||||
|
||||
Revision ID: ed422fc58ba
|
||||
Revises: 4bcfa2c36623
|
||||
Create Date: 2015-10-23 09:19:28.654126
|
||||
|
||||
"""
|
||||
import base64
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'ed422fc58ba'
|
||||
down_revision = '4bcfa2c36623'
|
||||
import six
|
||||
|
||||
from StringIO import StringIO
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.sql import text
|
||||
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||
from cryptography.fernet import Fernet, MultiFernet
|
||||
|
||||
from flask import current_app
|
||||
from lemur.common.utils import get_psuedo_random_string
|
||||
|
||||
conn = op.get_bind()
|
||||
|
||||
op.drop_table('encrypted_keys')
|
||||
op.drop_table('encrypted_passwords')
|
||||
|
||||
# helper tables to migrate data
|
||||
temp_key_table = op.create_table('encrypted_keys',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('aes', sa.Binary()),
|
||||
sa.Column('fernet', sa.Binary()),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
|
||||
# helper table to migrate data
|
||||
temp_password_table = op.create_table('encrypted_passwords',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('aes', sa.Binary()),
|
||||
sa.Column('fernet', sa.Binary()),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
|
||||
|
||||
# From http://sqlalchemy-utils.readthedocs.org/en/latest/_modules/sqlalchemy_utils/types/encrypted.html#EncryptedType
|
||||
# for migration purposes only
|
||||
class EncryptionDecryptionBaseEngine(object):
|
||||
"""A base encryption and decryption engine.
|
||||
|
||||
This class must be sub-classed in order to create
|
||||
new engines.
|
||||
"""
|
||||
|
||||
def _update_key(self, key):
|
||||
if isinstance(key, six.string_types):
|
||||
key = key.encode()
|
||||
digest = hashes.Hash(hashes.SHA256(), backend=default_backend())
|
||||
digest.update(key)
|
||||
engine_key = digest.finalize()
|
||||
|
||||
self._initialize_engine(engine_key)
|
||||
|
||||
def encrypt(self, value):
|
||||
raise NotImplementedError('Subclasses must implement this!')
|
||||
|
||||
def decrypt(self, value):
|
||||
raise NotImplementedError('Subclasses must implement this!')
|
||||
|
||||
|
||||
class AesEngine(EncryptionDecryptionBaseEngine):
|
||||
"""Provide AES encryption and decryption methods."""
|
||||
|
||||
BLOCK_SIZE = 16
|
||||
PADDING = six.b('*')
|
||||
|
||||
def _initialize_engine(self, parent_class_key):
|
||||
self.secret_key = parent_class_key
|
||||
self.iv = self.secret_key[:16]
|
||||
self.cipher = Cipher(
|
||||
algorithms.AES(self.secret_key),
|
||||
modes.CBC(self.iv),
|
||||
backend=default_backend()
|
||||
)
|
||||
|
||||
def _pad(self, value):
|
||||
"""Pad the message to be encrypted, if needed."""
|
||||
BS = self.BLOCK_SIZE
|
||||
P = self.PADDING
|
||||
padded = (value + (BS - len(value) % BS) * P)
|
||||
return padded
|
||||
|
||||
def encrypt(self, value):
|
||||
if not isinstance(value, six.string_types):
|
||||
value = repr(value)
|
||||
if isinstance(value, six.text_type):
|
||||
value = str(value)
|
||||
value = value.encode()
|
||||
value = self._pad(value)
|
||||
encryptor = self.cipher.encryptor()
|
||||
encrypted = encryptor.update(value) + encryptor.finalize()
|
||||
encrypted = base64.b64encode(encrypted)
|
||||
return encrypted
|
||||
|
||||
def decrypt(self, value):
|
||||
if isinstance(value, six.text_type):
|
||||
value = str(value)
|
||||
decryptor = self.cipher.decryptor()
|
||||
decrypted = base64.b64decode(value)
|
||||
decrypted = decryptor.update(decrypted) + decryptor.finalize()
|
||||
decrypted = decrypted.rstrip(self.PADDING)
|
||||
if not isinstance(decrypted, six.string_types):
|
||||
decrypted = decrypted.decode('utf-8')
|
||||
return decrypted
|
||||
|
||||
|
||||
def migrate_to_fernet(aes_encrypted, old_key, new_key):
|
||||
"""
|
||||
Will attempt to migrate an aes encrypted to fernet encryption
|
||||
:param aes_encrypted:
|
||||
:return: fernet encrypted value
|
||||
"""
|
||||
engine = AesEngine()
|
||||
engine._update_key(old_key)
|
||||
|
||||
if not isinstance(aes_encrypted, six.string_types):
|
||||
return
|
||||
|
||||
aes_decrypted = engine.decrypt(aes_encrypted)
|
||||
fernet_encrypted = MultiFernet([Fernet(k) for k in new_key]).encrypt(bytes(aes_decrypted))
|
||||
|
||||
# sanity check
|
||||
fernet_decrypted = MultiFernet([Fernet(k) for k in new_key]).decrypt(fernet_encrypted)
|
||||
if fernet_decrypted != aes_decrypted:
|
||||
raise Exception("WARNING: Decrypted values do not match!")
|
||||
|
||||
return fernet_encrypted
|
||||
|
||||
|
||||
def migrate_from_fernet(fernet_encrypted, old_key, new_key):
|
||||
"""
|
||||
Will attempt to migrate from a fernet encryption to aes
|
||||
:param fernet_encrypted:
|
||||
:return:
|
||||
"""
|
||||
engine = AesEngine()
|
||||
engine._update_key(new_key)
|
||||
|
||||
fernet_decrypted = MultiFernet([Fernet(k) for k in old_key]).decrypt(fernet_encrypted)
|
||||
aes_encrypted = engine.encrypt(fernet_decrypted)
|
||||
|
||||
# sanity check
|
||||
aes_decrypted = engine.decrypt(aes_encrypted)
|
||||
if fernet_decrypted != aes_decrypted:
|
||||
raise Exception("WARNING: Decrypted values do not match!")
|
||||
|
||||
return aes_encrypted
|
||||
|
||||
|
||||
def upgrade():
|
||||
old_key = current_app.config.get('LEMUR_ENCRYPTION_KEY')
|
||||
print "Using: {0} as decryption key".format(old_key)
|
||||
# generate a new fernet token
|
||||
|
||||
if current_app.config.get('LEMUR_ENCRYPTION_KEYS'):
|
||||
new_key = current_app.config.get('LEMUR_ENCRYPTION_KEYS')
|
||||
else:
|
||||
new_key = [Fernet.generate_key()]
|
||||
|
||||
print "Using: {0} as new encryption key, save this and place it in your configuration!".format(new_key)
|
||||
|
||||
# migrate private_keys
|
||||
temp_keys = []
|
||||
for id, private_key in conn.execute(text('select id, private_key from certificates where private_key is not null')):
|
||||
aes_encrypted = StringIO(private_key).read()
|
||||
fernet_encrypted = migrate_to_fernet(aes_encrypted, old_key, new_key)
|
||||
temp_keys.append({'id': id, 'aes': aes_encrypted, 'fernet': fernet_encrypted})
|
||||
|
||||
op.bulk_insert(temp_key_table, temp_keys)
|
||||
|
||||
for id, fernet in conn.execute(text('select id, fernet from encrypted_keys')):
|
||||
stmt = text("update certificates set private_key=:key where id=:id")
|
||||
stmt = stmt.bindparams(key=fernet, id=id)
|
||||
op.execute(stmt)
|
||||
print "Certificate {0} has been migrated".format(id)
|
||||
|
||||
# migrate role_passwords
|
||||
temp_passwords = []
|
||||
for id, password in conn.execute(text('select id, password from roles where password is not null')):
|
||||
aes_encrypted = StringIO(password).read()
|
||||
fernet_encrypted = migrate_to_fernet(aes_encrypted, old_key, new_key)
|
||||
temp_passwords.append({'id': id, 'aes': aes_encrypted, 'fernet': fernet_encrypted})
|
||||
|
||||
op.bulk_insert(temp_password_table, temp_passwords)
|
||||
|
||||
for id, fernet in conn.execute(text('select id, fernet from encrypted_passwords')):
|
||||
stmt = text("update roles set password=:password where id=:id")
|
||||
stmt = stmt.bindparams(password=fernet, id=id)
|
||||
print stmt
|
||||
op.execute(stmt)
|
||||
print "Password {0} has been migrated".format(id)
|
||||
|
||||
op.drop_table('encrypted_keys')
|
||||
op.drop_table('encrypted_passwords')
|
||||
|
||||
|
||||
def downgrade():
|
||||
old_key = current_app.config.get('LEMUR_ENCRYPTION_KEYS')
|
||||
print "Using: {0} as decryption key(s)".format(old_key)
|
||||
|
||||
# generate aes valid key
|
||||
if current_app.config.get('LEMUR_ENCRYPTION_KEY'):
|
||||
new_key = current_app.config.get('LEMUR_ENCRYPTION_KEY')
|
||||
else:
|
||||
new_key = get_psuedo_random_string()
|
||||
print "Using: {0} as the encryption key, save this and place it in your configuration!".format(new_key)
|
||||
|
||||
# migrate keys
|
||||
temp_keys = []
|
||||
for id, private_key in conn.execute(text('select id, private_key from certificates where private_key is not null')):
|
||||
fernet_encrypted = StringIO(private_key).read()
|
||||
aes_encrypted = migrate_from_fernet(fernet_encrypted, old_key, new_key)
|
||||
temp_keys.append({'id': id, 'aes': aes_encrypted, 'fernet': fernet_encrypted})
|
||||
|
||||
op.bulk_insert(temp_key_table, temp_keys)
|
||||
|
||||
for id, aes in conn.execute(text('select id, aes from encrypted_keys')):
|
||||
stmt = text("update certificates set private_key=:key where id=:id")
|
||||
stmt = stmt.bindparams(key=aes, id=id)
|
||||
print stmt
|
||||
op.execute(stmt)
|
||||
print "Certificate {0} has been migrated".format(id)
|
||||
|
||||
# migrate role_passwords
|
||||
temp_passwords = []
|
||||
for id, password in conn.execute(text('select id, password from roles where password is not null')):
|
||||
fernet_encrypted = StringIO(password).read()
|
||||
aes_encrypted = migrate_from_fernet(fernet_encrypted, old_key, new_key)
|
||||
temp_passwords.append({'id': id, 'aes': aes_encrypted, 'fernet': fernet_encrypted})
|
||||
|
||||
op.bulk_insert(temp_password_table, temp_passwords)
|
||||
|
||||
for id, aes in conn.execute(text('select id, aes from encrypted_passwords')):
|
||||
stmt = text("update roles set password=:password where id=:id")
|
||||
stmt = stmt.bindparams(password=aes, id=id)
|
||||
op.execute(stmt)
|
||||
print "Password {0} has been migrated".format(id)
|
||||
|
||||
op.drop_table('encrypted_keys')
|
||||
op.drop_table('encrypted_passwords')
|
@ -8,7 +8,8 @@
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
from sqlalchemy import Column, Integer, ForeignKey
|
||||
from sqlalchemy import Column, Integer, ForeignKey, Index
|
||||
|
||||
from lemur.database import db
|
||||
|
||||
certificate_associations = db.Table('certificate_associations',
|
||||
@ -16,6 +17,8 @@ certificate_associations = db.Table('certificate_associations',
|
||||
Column('certificate_id', Integer, ForeignKey('certificates.id'))
|
||||
)
|
||||
|
||||
Index('certificate_associations_ix', certificate_associations.c.domain_id, certificate_associations.c.certificate_id)
|
||||
|
||||
certificate_destination_associations = db.Table('certificate_destination_associations',
|
||||
Column('destination_id', Integer,
|
||||
ForeignKey('destinations.id', ondelete='cascade')),
|
||||
@ -23,6 +26,8 @@ certificate_destination_associations = db.Table('certificate_destination_associa
|
||||
ForeignKey('certificates.id', ondelete='cascade'))
|
||||
)
|
||||
|
||||
Index('certificate_destination_associations_ix', certificate_destination_associations.c.destination_id, certificate_destination_associations.c.certificate_id)
|
||||
|
||||
certificate_source_associations = db.Table('certificate_source_associations',
|
||||
Column('source_id', Integer,
|
||||
ForeignKey('sources.id', ondelete='cascade')),
|
||||
@ -30,6 +35,8 @@ certificate_source_associations = db.Table('certificate_source_associations',
|
||||
ForeignKey('certificates.id', ondelete='cascade'))
|
||||
)
|
||||
|
||||
Index('certificate_source_associations_ix', certificate_source_associations.c.source_id, certificate_source_associations.c.certificate_id)
|
||||
|
||||
certificate_notification_associations = db.Table('certificate_notification_associations',
|
||||
Column('notification_id', Integer,
|
||||
ForeignKey('notifications.id', ondelete='cascade')),
|
||||
@ -37,6 +44,8 @@ certificate_notification_associations = db.Table('certificate_notification_assoc
|
||||
ForeignKey('certificates.id', ondelete='cascade'))
|
||||
)
|
||||
|
||||
Index('certificate_notification_associations_ix', certificate_notification_associations.c.notification_id, certificate_notification_associations.c.certificate_id)
|
||||
|
||||
certificate_replacement_associations = db.Table('certificate_replacement_associations',
|
||||
Column('replaced_certificate_id', Integer,
|
||||
ForeignKey('certificates.id', ondelete='cascade')),
|
||||
@ -44,7 +53,26 @@ certificate_replacement_associations = db.Table('certificate_replacement_associa
|
||||
ForeignKey('certificates.id', ondelete='cascade'))
|
||||
)
|
||||
|
||||
Index('certificate_replacement_associations_ix', certificate_replacement_associations.c.certificate_id, certificate_replacement_associations.c.certificate_id)
|
||||
|
||||
roles_authorities = db.Table('roles_authorities',
|
||||
Column('authority_id', Integer, ForeignKey('authorities.id')),
|
||||
Column('role_id', Integer, ForeignKey('roles.id'))
|
||||
)
|
||||
|
||||
Index('roles_authorities_ix', roles_authorities.c.authority_id, roles_authorities.c.role_id)
|
||||
|
||||
roles_certificates = db.Table('roles_certificates',
|
||||
Column('certificate_id', Integer, ForeignKey('certificates.id')),
|
||||
Column('role_id', Integer, ForeignKey('roles.id'))
|
||||
)
|
||||
|
||||
Index('roles_certificates_ix', roles_certificates.c.certificate_id, roles_certificates.c.role_id)
|
||||
|
||||
|
||||
roles_users = db.Table('roles_users',
|
||||
Column('user_id', Integer, ForeignKey('users.id')),
|
||||
Column('role_id', Integer, ForeignKey('roles.id'))
|
||||
)
|
||||
|
||||
Index('roles_users_ix', roles_users.c.user_id, roles_users.c.role_id)
|
||||
|
49
lemur/notifications/schemas.py
Normal file
49
lemur/notifications/schemas.py
Normal file
@ -0,0 +1,49 @@
|
||||
"""
|
||||
.. module: lemur.notifications.schemas
|
||||
:platform: unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
from marshmallow import fields, post_dump
|
||||
from lemur.common.schema import LemurInputSchema, LemurOutputSchema
|
||||
from lemur.schemas import PluginInputSchema, PluginOutputSchema, AssociatedCertificateSchema
|
||||
|
||||
|
||||
class NotificationInputSchema(LemurInputSchema):
|
||||
id = fields.Integer()
|
||||
label = fields.String(required=True)
|
||||
description = fields.String()
|
||||
active = fields.Boolean()
|
||||
plugin = fields.Nested(PluginInputSchema, required=True)
|
||||
certificates = fields.Nested(AssociatedCertificateSchema, many=True, missing=[])
|
||||
|
||||
|
||||
class NotificationOutputSchema(LemurOutputSchema):
|
||||
id = fields.Integer()
|
||||
label = fields.String()
|
||||
description = fields.String()
|
||||
active = fields.Boolean()
|
||||
options = fields.List(fields.Dict())
|
||||
plugin = fields.Nested(PluginOutputSchema)
|
||||
certificates = fields.Nested(AssociatedCertificateSchema, many=True, missing=[])
|
||||
|
||||
@post_dump
|
||||
def fill_object(self, data):
|
||||
data['plugin']['pluginOptions'] = data['options']
|
||||
return data
|
||||
|
||||
|
||||
class NotificationNestedOutputSchema(LemurOutputSchema):
|
||||
__envelope__ = False
|
||||
id = fields.Integer()
|
||||
label = fields.String()
|
||||
description = fields.String()
|
||||
active = fields.Boolean()
|
||||
options = fields.List(fields.Dict())
|
||||
plugin = fields.Nested(PluginOutputSchema)
|
||||
|
||||
|
||||
notification_input_schema = NotificationInputSchema()
|
||||
notification_output_schema = NotificationOutputSchema()
|
||||
notifications_output_schema = NotificationOutputSchema(many=True)
|
@ -273,7 +273,7 @@ def update(notification_id, label, options, description, active, certificates):
|
||||
notification.options = options
|
||||
notification.description = description
|
||||
notification.active = active
|
||||
notification = database.update_list(notification, 'certificates', Certificate, certificates)
|
||||
notification.certificates = certificates
|
||||
|
||||
return database.update(notification)
|
||||
|
||||
@ -319,10 +319,6 @@ def get_all():
|
||||
|
||||
|
||||
def render(args):
|
||||
sort_by = args.pop('sort_by')
|
||||
sort_dir = args.pop('sort_dir')
|
||||
page = args.pop('page')
|
||||
count = args.pop('count')
|
||||
filt = args.pop('filter')
|
||||
certificate_id = args.pop('certificate_id', None)
|
||||
|
||||
@ -341,9 +337,4 @@ def render(args):
|
||||
else:
|
||||
query = database.filter(query, Notification, terms)
|
||||
|
||||
query = database.find_all(query, Notification, args)
|
||||
|
||||
if sort_by and sort_dir:
|
||||
query = database.sort(query, Notification, sort_by, sort_dir)
|
||||
|
||||
return database.paginate(query, page, count)
|
||||
return database.sort_and_page(query, Notification, args)
|
||||
|
@ -7,64 +7,27 @@
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
from flask import Blueprint
|
||||
from flask.ext.restful import Api, reqparse, fields
|
||||
from flask.ext.restful import Api, reqparse
|
||||
from lemur.notifications import service
|
||||
from lemur.notifications.schemas import notification_input_schema, notification_output_schema, notifications_output_schema
|
||||
|
||||
from lemur.auth.service import AuthenticatedResource
|
||||
from lemur.common.utils import paginated_parser, marshal_items
|
||||
from lemur.common.utils import paginated_parser
|
||||
|
||||
from lemur.common.schema import validate_schema
|
||||
|
||||
|
||||
mod = Blueprint('notifications', __name__)
|
||||
api = Api(mod)
|
||||
|
||||
|
||||
FIELDS = {
|
||||
'description': fields.String,
|
||||
'notificationOptions': fields.Raw(attribute='options'),
|
||||
'pluginName': fields.String(attribute='plugin_name'),
|
||||
'label': fields.String,
|
||||
'active': fields.Boolean,
|
||||
'id': fields.Integer,
|
||||
}
|
||||
|
||||
|
||||
def notification(value, name):
|
||||
"""
|
||||
Validates a given notification exits
|
||||
:param value:
|
||||
:param name:
|
||||
:return:
|
||||
"""
|
||||
n = service.get(value)
|
||||
if not n:
|
||||
raise ValueError("Unable to find notification specified")
|
||||
return n
|
||||
|
||||
|
||||
def notification_list(value, name):
|
||||
"""
|
||||
Validates a given notification exists and returns a list
|
||||
:param value:
|
||||
:param name:
|
||||
:return:
|
||||
"""
|
||||
notifications = []
|
||||
for v in value:
|
||||
try:
|
||||
notifications.append(notification(v['id'], 'id'))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return notifications
|
||||
|
||||
|
||||
class NotificationsList(AuthenticatedResource):
|
||||
""" Defines the 'notifications' endpoint """
|
||||
def __init__(self):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(NotificationsList, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
@validate_schema(None, notifications_output_schema)
|
||||
def get(self):
|
||||
"""
|
||||
.. http:get:: /notifications
|
||||
@ -91,7 +54,7 @@ class NotificationsList(AuthenticatedResource):
|
||||
"items": [
|
||||
{
|
||||
"description": "An example",
|
||||
"notificationOptions": [
|
||||
"options": [
|
||||
{
|
||||
"name": "interval",
|
||||
"required": true,
|
||||
@ -133,9 +96,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 count: count number default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
"""
|
||||
@ -144,8 +107,8 @@ class NotificationsList(AuthenticatedResource):
|
||||
args = parser.parse_args()
|
||||
return service.render(args)
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
def post(self):
|
||||
@validate_schema(notification_input_schema, notification_output_schema)
|
||||
def post(self, data=None):
|
||||
"""
|
||||
.. http:post:: /notifications
|
||||
|
||||
@ -161,7 +124,7 @@ class NotificationsList(AuthenticatedResource):
|
||||
|
||||
{
|
||||
"description": "a test",
|
||||
"notificationOptions": [
|
||||
"options": [
|
||||
{
|
||||
"name": "interval",
|
||||
"required": true,
|
||||
@ -208,7 +171,7 @@ class NotificationsList(AuthenticatedResource):
|
||||
|
||||
{
|
||||
"description": "a test",
|
||||
"notificationOptions": [
|
||||
"options": [
|
||||
{
|
||||
"name": "interval",
|
||||
"required": true,
|
||||
@ -251,18 +214,12 @@ class NotificationsList(AuthenticatedResource):
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
"""
|
||||
self.reqparse.add_argument('label', type=str, location='json', required=True)
|
||||
self.reqparse.add_argument('plugin', type=dict, location='json', required=True)
|
||||
self.reqparse.add_argument('description', type=str, location='json')
|
||||
self.reqparse.add_argument('certificates', type=list, default=[], location='json')
|
||||
|
||||
args = self.reqparse.parse_args()
|
||||
return service.create(
|
||||
args['label'],
|
||||
args['plugin']['slug'],
|
||||
args['plugin']['pluginOptions'],
|
||||
args['description'],
|
||||
args['certificates']
|
||||
data['label'],
|
||||
data['plugin']['slug'],
|
||||
data['plugin']['plugin_options'],
|
||||
data['description'],
|
||||
data['certificates']
|
||||
)
|
||||
|
||||
|
||||
@ -271,7 +228,7 @@ class Notifications(AuthenticatedResource):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(Notifications, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
@validate_schema(None, notification_output_schema)
|
||||
def get(self, notification_id):
|
||||
"""
|
||||
.. http:get:: /notifications/1
|
||||
@ -296,7 +253,7 @@ class Notifications(AuthenticatedResource):
|
||||
|
||||
{
|
||||
"description": "a test",
|
||||
"notificationOptions": [
|
||||
"options": [
|
||||
{
|
||||
"name": "interval",
|
||||
"required": true,
|
||||
@ -338,8 +295,8 @@ class Notifications(AuthenticatedResource):
|
||||
"""
|
||||
return service.get(notification_id)
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
def put(self, notification_id):
|
||||
@validate_schema(notification_input_schema, notification_output_schema)
|
||||
def put(self, notification_id, data=None):
|
||||
"""
|
||||
.. http:put:: /notifications/1
|
||||
|
||||
@ -375,20 +332,13 @@ class Notifications(AuthenticatedResource):
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
"""
|
||||
self.reqparse.add_argument('label', type=str, location='json', required=True)
|
||||
self.reqparse.add_argument('plugin', type=dict, location='json', required=True)
|
||||
self.reqparse.add_argument('active', type=bool, location='json')
|
||||
self.reqparse.add_argument('certificates', type=list, default=[], location='json')
|
||||
self.reqparse.add_argument('description', type=str, location='json')
|
||||
|
||||
args = self.reqparse.parse_args()
|
||||
return service.update(
|
||||
notification_id,
|
||||
args['label'],
|
||||
args['plugin']['pluginOptions'],
|
||||
args['description'],
|
||||
args['active'],
|
||||
args['certificates']
|
||||
data['label'],
|
||||
data['plugin']['plugin_options'],
|
||||
data['description'],
|
||||
data['active'],
|
||||
data['certificates']
|
||||
)
|
||||
|
||||
def delete(self, notification_id):
|
||||
@ -401,7 +351,7 @@ class CertificateNotifications(AuthenticatedResource):
|
||||
def __init__(self):
|
||||
super(CertificateNotifications, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
@validate_schema(None, notifications_output_schema)
|
||||
def get(self, certificate_id):
|
||||
"""
|
||||
.. http:get:: /certificates/1/notifications
|
||||
@ -428,7 +378,7 @@ class CertificateNotifications(AuthenticatedResource):
|
||||
"items": [
|
||||
{
|
||||
"description": "An example",
|
||||
"notificationOptions": [
|
||||
"options": [
|
||||
{
|
||||
"name": "interval",
|
||||
"required": true,
|
||||
@ -470,17 +420,13 @@ 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 count: count number default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
"""
|
||||
parser = paginated_parser.copy()
|
||||
parser.add_argument('active', type=bool, location='args')
|
||||
args = parser.parse_args()
|
||||
args['certificate_id'] = certificate_id
|
||||
return service.render(args)
|
||||
return service.render({'certificate_id': certificate_id})
|
||||
|
||||
|
||||
api.add_resource(NotificationsList, '/notifications', endpoint='notifications')
|
||||
|
@ -112,7 +112,7 @@ class IPlugin(local):
|
||||
def get_option(name, options):
|
||||
for o in options:
|
||||
if o.get('name') == name:
|
||||
return o.get('value')
|
||||
return o.get('value', o.get('default'))
|
||||
|
||||
|
||||
class Plugin(IPlugin):
|
||||
|
@ -15,6 +15,7 @@ class ExportPlugin(Plugin):
|
||||
exporters will inherit from.
|
||||
"""
|
||||
type = 'export'
|
||||
requires_key = True
|
||||
|
||||
def export(self):
|
||||
raise NotImplemented
|
||||
|
16
lemur/plugins/bases/metric.py
Normal file
16
lemur/plugins/bases/metric.py
Normal file
@ -0,0 +1,16 @@
|
||||
"""
|
||||
.. module: lemur.bases.metric
|
||||
:platform: Unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
from lemur.plugins.base import Plugin
|
||||
|
||||
|
||||
class MetricPlugin(Plugin):
|
||||
type = 'metric'
|
||||
|
||||
def submit(self, *args, **kwargs):
|
||||
raise NotImplemented
|
@ -45,7 +45,7 @@ class ExpirationNotificationPlugin(NotificationPlugin):
|
||||
]
|
||||
|
||||
@property
|
||||
def options(self):
|
||||
def plugin_options(self):
|
||||
return list(self.default_options) + self.additional_options
|
||||
|
||||
def send(self):
|
||||
|
5
lemur/plugins/lemur_atlas/__init__.py
Normal file
5
lemur/plugins/lemur_atlas/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
try:
|
||||
VERSION = __import__('pkg_resources') \
|
||||
.get_distribution(__name__).version
|
||||
except Exception as e:
|
||||
VERSION = 'unknown'
|
107
lemur/plugins/lemur_atlas/plugin.py
Normal file
107
lemur/plugins/lemur_atlas/plugin.py
Normal file
@ -0,0 +1,107 @@
|
||||
"""
|
||||
.. module: lemur.plugins.lemur_atlas.plugin
|
||||
:platform: Unix
|
||||
:copyright: (c) 2016 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
import json
|
||||
import requests
|
||||
from requests.exceptions import ConnectionError
|
||||
from datetime import datetime
|
||||
|
||||
from flask import current_app
|
||||
from lemur.plugins import lemur_atlas as atlas
|
||||
from lemur.plugins.bases.metric import MetricPlugin
|
||||
|
||||
|
||||
def millis_since_epoch():
|
||||
"""
|
||||
current time since epoch in milliseconds
|
||||
"""
|
||||
epoch = datetime.utcfromtimestamp(0)
|
||||
delta = datetime.now() - epoch
|
||||
return int(delta.total_seconds() * 1000.0)
|
||||
|
||||
|
||||
class AtlasMetricPlugin(MetricPlugin):
|
||||
title = 'Atlas'
|
||||
slug = 'atlas-metric'
|
||||
description = 'Adds support for sending key metrics to Atlas'
|
||||
version = atlas.VERSION
|
||||
|
||||
author = 'Kevin Glisson'
|
||||
author_url = 'https://github.com/netflix/lemur'
|
||||
|
||||
options = [
|
||||
{
|
||||
'name': 'sidecar_host',
|
||||
'type': 'str',
|
||||
'required': False,
|
||||
'help_message': 'If no host is provided localhost is assumed',
|
||||
'default': 'localhost'
|
||||
},
|
||||
{
|
||||
'name': 'sidecar_port',
|
||||
'type': 'int',
|
||||
'required': False,
|
||||
'default': 8078
|
||||
}
|
||||
]
|
||||
|
||||
metric_data = {}
|
||||
sidecar_host = None
|
||||
sidecar_port = None
|
||||
|
||||
def submit(self, metric_name, metric_type, metric_value, metric_tags=None, options=None):
|
||||
if not options:
|
||||
options = self.options
|
||||
|
||||
# TODO marshmallow schema?
|
||||
valid_types = ['COUNTER', 'GAUGE', 'TIMER']
|
||||
if metric_type.upper() not in valid_types:
|
||||
raise Exception(
|
||||
"Invalid Metric Type for Atlas: '{metric}' choose from: {options}".format(
|
||||
metric=metric_type, options=','.join(valid_types)
|
||||
)
|
||||
)
|
||||
|
||||
if metric_tags:
|
||||
if not isinstance(metric_tags, dict):
|
||||
raise Exception(
|
||||
"Invalid Metric Tags for Atlas: Tags must be in dict format"
|
||||
)
|
||||
|
||||
if metric_value == "NaN" or isinstance(metric_value, int) or isinstance(metric_value, float):
|
||||
self.metric_data['value'] = metric_value
|
||||
else:
|
||||
raise Exception(
|
||||
"Invalid Metric Value for Atlas: Metric must be a number"
|
||||
)
|
||||
|
||||
self.metric_data['type'] = metric_type.upper()
|
||||
self.metric_data['name'] = str(metric_name)
|
||||
self.metric_data['tags'] = metric_tags
|
||||
self.metric_data['timestamp'] = millis_since_epoch()
|
||||
|
||||
self.sidecar_host = self.get_option('sidecar_host', options)
|
||||
self.sidecar_port = self.get_option('sidecar_port', options)
|
||||
|
||||
try:
|
||||
res = requests.post(
|
||||
'http://{host}:{port}/metrics'.format(
|
||||
host=self.sidecar_host,
|
||||
port=self.sidecar_port),
|
||||
data=json.dumps([self.metric_data])
|
||||
)
|
||||
|
||||
if res.status_code != 200:
|
||||
current_app.logger.warning("Failed to publish altas metric. {0}".format(res.content))
|
||||
|
||||
except ConnectionError:
|
||||
current_app.logger.warning(
|
||||
"AtlasMetrics: could not connect to sidecar at {host}:{port}".format(
|
||||
host=self.sidecar_host, port=self.sidecar_port
|
||||
)
|
||||
)
|
@ -1,5 +1,5 @@
|
||||
"""
|
||||
.. module: elb
|
||||
.. module: lemur.plugins.lemur_aws.elb
|
||||
:synopsis: Module contains some often used and helpful classes that
|
||||
are used to deal with ELBs
|
||||
|
||||
@ -28,7 +28,6 @@ def is_valid(listener_tuple):
|
||||
|
||||
:param listener_tuple:
|
||||
"""
|
||||
|
||||
current_app.logger.debug(listener_tuple)
|
||||
lb_port, i_port, lb_protocol, arn = listener_tuple
|
||||
current_app.logger.debug(lb_protocol)
|
||||
|
@ -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):
|
||||
@ -80,8 +83,8 @@ class AWSSourcePlugin(SourcePlugin):
|
||||
cert_body, cert_chain = iam.get_cert_from_arn(arn)
|
||||
cert_name = iam.get_name_from_arn(arn)
|
||||
cert = dict(
|
||||
public_certificate=cert_body,
|
||||
intermediate_certificate=cert_chain,
|
||||
body=cert_body,
|
||||
chain=cert_chain,
|
||||
name=cert_name
|
||||
)
|
||||
certs.append(cert)
|
||||
|
@ -2,7 +2,7 @@ from moto import mock_iam, mock_sts
|
||||
|
||||
from lemur.certificates.models import Certificate
|
||||
|
||||
from lemur.tests.certs import EXTERNAL_VALID_STR, PRIVATE_KEY_STR
|
||||
from lemur.tests.vectors import EXTERNAL_VALID_STR, PRIVATE_KEY_STR
|
||||
|
||||
|
||||
def test_get_name_from_arn():
|
||||
|
0
lemur/plugins/lemur_email/tests/email.html
Normal file
0
lemur/plugins/lemur_email/tests/email.html
Normal file
@ -10,10 +10,11 @@ import subprocess
|
||||
|
||||
from flask import current_app
|
||||
|
||||
from cryptography.fernet import Fernet
|
||||
|
||||
from lemur.utils import mktempfile, mktemppath
|
||||
from lemur.plugins.bases import ExportPlugin
|
||||
from lemur.plugins import lemur_java as java
|
||||
from lemur.common.utils import get_psuedo_random_string
|
||||
|
||||
|
||||
def run_process(command):
|
||||
@ -29,6 +30,7 @@ def run_process(command):
|
||||
if p.returncode != 0:
|
||||
current_app.logger.debug(" ".join(command))
|
||||
current_app.logger.error(stderr)
|
||||
current_app.logger.error(stdout)
|
||||
raise Exception(stderr)
|
||||
|
||||
|
||||
@ -85,45 +87,43 @@ def create_truststore(cert, chain, jks_tmp, alias, passphrase):
|
||||
])
|
||||
|
||||
|
||||
def create_keystore(cert, jks_tmp, key, alias, passphrase):
|
||||
with mktempfile() as key_tmp:
|
||||
with open(key_tmp, 'w') as f:
|
||||
f.write(key)
|
||||
def create_keystore(cert, chain, jks_tmp, key, alias, passphrase):
|
||||
# Create PKCS12 keystore from private key and public certificate
|
||||
with mktempfile() as cert_tmp:
|
||||
with open(cert_tmp, 'w') as f:
|
||||
f.writelines([key + "\n", cert + "\n", chain + "\n"])
|
||||
|
||||
# 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",
|
||||
"-nodes",
|
||||
"-name", alias,
|
||||
"-in", cert_tmp,
|
||||
"-out", p12_tmp,
|
||||
"-password", "pass:{}".format(passphrase)
|
||||
])
|
||||
|
||||
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
|
||||
])
|
||||
# Convert PKCS12 keystore into a JKS keystore
|
||||
run_process([
|
||||
"keytool",
|
||||
"-importkeystore",
|
||||
"-destkeystore", jks_tmp,
|
||||
"-srckeystore", p12_tmp,
|
||||
"-srcstoretype", "pkcs12",
|
||||
"-deststoretype", "JKS",
|
||||
"-alias", alias,
|
||||
"-srcstorepass", passphrase,
|
||||
"-deststorepass", 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 +131,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 = Fernet.generate_key()
|
||||
|
||||
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 +202,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:
|
||||
@ -166,28 +214,18 @@ class JavaExportPlugin(ExportPlugin):
|
||||
if self.get_option('passphrase', options):
|
||||
passphrase = self.get_option('passphrase', options)
|
||||
else:
|
||||
passphrase = get_psuedo_random_string()
|
||||
passphrase = Fernet.generate_key()
|
||||
|
||||
if self.get_option('alias', options):
|
||||
alias = self.get_option('alias', options)
|
||||
else:
|
||||
alias = "blah"
|
||||
|
||||
type = self.get_option('type', options)
|
||||
|
||||
with mktemppath() as jks_tmp:
|
||||
if type == 'Truststore (JKS)':
|
||||
create_truststore(body, chain, jks_tmp, alias, passphrase)
|
||||
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_keystore(body, chain, jks_tmp, key, alias, passphrase)
|
||||
|
||||
with open(jks_tmp, 'rb') as f:
|
||||
raw = f.read()
|
||||
|
@ -33,11 +33,12 @@ def run_process(command):
|
||||
raise Exception(stderr)
|
||||
|
||||
|
||||
def create_pkcs12(cert, p12_tmp, key, alias, passphrase):
|
||||
def create_pkcs12(cert, chain, p12_tmp, key, alias, passphrase):
|
||||
"""
|
||||
Creates a pkcs12 formated file.
|
||||
:param cert:
|
||||
:param jks_tmp:
|
||||
:param chain:
|
||||
:param p12_tmp:
|
||||
:param key:
|
||||
:param alias:
|
||||
:param passphrase:
|
||||
@ -49,7 +50,7 @@ def create_pkcs12(cert, p12_tmp, key, alias, passphrase):
|
||||
# Create PKCS12 keystore from private key and public certificate
|
||||
with mktempfile() as cert_tmp:
|
||||
with open(cert_tmp, 'w') as f:
|
||||
f.write(cert)
|
||||
f.writelines([cert + "\n", chain + "\n"])
|
||||
|
||||
run_process([
|
||||
"openssl",
|
||||
@ -85,7 +86,7 @@ class OpenSSLExportPlugin(ExportPlugin):
|
||||
'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': ''
|
||||
},
|
||||
{
|
||||
'name': 'alias',
|
||||
@ -119,7 +120,7 @@ class OpenSSLExportPlugin(ExportPlugin):
|
||||
|
||||
with mktemppath() as output_tmp:
|
||||
if type == 'PKCS12 (.p12)':
|
||||
create_pkcs12(body, output_tmp, key, alias, passphrase)
|
||||
create_pkcs12(body, chain, output_tmp, key, alias, passphrase)
|
||||
extension = "p12"
|
||||
else:
|
||||
raise Exception("Unable to export, unsupported type: {0}".format(type))
|
||||
|
5
lemur/plugins/lemur_slack/__init__.py
Normal file
5
lemur/plugins/lemur_slack/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
try:
|
||||
VERSION = __import__('pkg_resources') \
|
||||
.get_distribution(__name__).version
|
||||
except Exception as e:
|
||||
VERSION = 'unknown'
|
68
lemur/plugins/lemur_slack/plugin.py
Normal file
68
lemur/plugins/lemur_slack/plugin.py
Normal file
@ -0,0 +1,68 @@
|
||||
"""
|
||||
.. module: lemur.plugins.lemur_slack.slack
|
||||
:platform: Unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
|
||||
.. moduleauthor:: Harm Weites <harm@weites.com>
|
||||
"""
|
||||
from flask import current_app
|
||||
from lemur.plugins.bases import ExpirationNotificationPlugin
|
||||
from lemur.plugins import lemur_slack as slack
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
def find_value(name, options):
|
||||
for o in options:
|
||||
if o['name'] == name:
|
||||
return o['value']
|
||||
|
||||
|
||||
class SlackNotificationPlugin(ExpirationNotificationPlugin):
|
||||
title = 'Slack'
|
||||
slug = 'slack-notification'
|
||||
description = 'Sends notifications to Slack'
|
||||
version = slack.VERSION
|
||||
|
||||
author = 'Harm Weites'
|
||||
author_url = 'https://github.com/netflix/lemur'
|
||||
|
||||
additional_options = [
|
||||
{
|
||||
'name': 'webhook',
|
||||
'type': 'str',
|
||||
'required': True,
|
||||
'validation': '^https:\/\/hooks\.slack\.com\/services\/.+$',
|
||||
'helpMessage': 'The url Slack told you to use for this integration',
|
||||
}, {
|
||||
'name': 'username',
|
||||
'type': 'str',
|
||||
'required': True,
|
||||
'validation': '^.+$',
|
||||
'helpMessage': 'The great storyteller',
|
||||
}, {
|
||||
'name': 'recipients',
|
||||
'type': 'str',
|
||||
'required': True,
|
||||
'validation': '^(@|#).+$',
|
||||
'helpMessage': 'Where to send to, either @username or #channel',
|
||||
},
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def send(event_type, message, targets, options, **kwargs):
|
||||
"""
|
||||
A typical check can be performed using the notify command:
|
||||
`lemur notify`
|
||||
"""
|
||||
msg = 'Certificate expiry pending for certificate:\n*%s*\nCurrent state is: _%s_' % (message[0]['name'], event_type)
|
||||
body = '{"text": "%s", "channel": "%s", "username": "%s"}' % (msg, find_value('recipients', options), find_value('username', options))
|
||||
|
||||
current_app.logger.info("Sending message to Slack: %s" % body)
|
||||
current_app.logger.debug("Sending data to Slack endpoint at %s" % find_value('webhook', options))
|
||||
|
||||
r = requests.post(find_value('webhook', options), body)
|
||||
if r.status_code not in [200]:
|
||||
current_app.logger.error("Slack response: %s" % r.status_code)
|
||||
raise
|
@ -77,11 +77,17 @@ def process_options(options):
|
||||
'email': current_app.config.get("VERISIGN_EMAIL")
|
||||
}
|
||||
|
||||
if options.get('validityEnd'):
|
||||
if options.get('validity_end'):
|
||||
end_date, period = get_default_issuance(options)
|
||||
data['specificEndDate'] = end_date
|
||||
data['specificEndDate'] = str(end_date)
|
||||
data['validityPeriod'] = period
|
||||
|
||||
elif options.get('validity_years'):
|
||||
if options['validity_years'] in [1, 2]:
|
||||
data['validityPeriod'] = str(options['validity_years']) + 'Y'
|
||||
else:
|
||||
raise Exception("Verisign issued certificates cannot exceed two years in validity")
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@ -92,10 +98,10 @@ def get_default_issuance(options):
|
||||
:param options:
|
||||
:return:
|
||||
"""
|
||||
specific_end_date = arrow.get(options['validityEnd']).replace(days=-1).format("MM/DD/YYYY")
|
||||
specific_end_date = arrow.get(options['validity_end']).replace(days=-1).format("MM/DD/YYYY")
|
||||
|
||||
now = arrow.utcnow()
|
||||
then = arrow.get(options['validityEnd'])
|
||||
then = arrow.get(options['validity_end'])
|
||||
|
||||
if then < now.replace(years=+1):
|
||||
validity_period = '1Y'
|
||||
|
@ -7,36 +7,25 @@
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
from flask import Blueprint
|
||||
from flask.ext.restful import Api, reqparse, fields
|
||||
from flask.ext.restful import Api, reqparse
|
||||
from lemur.auth.service import AuthenticatedResource
|
||||
|
||||
from lemur.common.utils import marshal_items
|
||||
|
||||
from lemur.schemas import plugins_output_schema, plugin_output_schema
|
||||
from lemur.common.schema import validate_schema
|
||||
from lemur.plugins.base import plugins
|
||||
|
||||
mod = Blueprint('plugins', __name__)
|
||||
api = Api(mod)
|
||||
|
||||
|
||||
FIELDS = {
|
||||
'title': fields.String,
|
||||
'pluginOptions': fields.Raw(attribute='options'),
|
||||
'description': fields.String,
|
||||
'version': fields.String,
|
||||
'author': fields.String,
|
||||
'authorUrl': fields.String,
|
||||
'type': fields.String,
|
||||
'slug': fields.String,
|
||||
}
|
||||
|
||||
|
||||
class PluginsList(AuthenticatedResource):
|
||||
""" Defines the 'plugins' endpoint """
|
||||
def __init__(self):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(PluginsList, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
@validate_schema(None, plugins_output_schema)
|
||||
def get(self):
|
||||
"""
|
||||
.. http:get:: /plugins
|
||||
@ -94,7 +83,7 @@ class Plugins(AuthenticatedResource):
|
||||
def __init__(self):
|
||||
super(Plugins, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
@validate_schema(None, plugin_output_schema)
|
||||
def get(self, name):
|
||||
"""
|
||||
.. http:get:: /plugins/<name>
|
||||
|
@ -14,7 +14,7 @@ from sqlalchemy import Column, Integer, String, Text, ForeignKey
|
||||
|
||||
from lemur.database import db
|
||||
from lemur.utils import Vault
|
||||
from lemur.models import roles_users
|
||||
from lemur.models import roles_users, roles_authorities, roles_certificates
|
||||
|
||||
|
||||
class Role(db.Model):
|
||||
@ -25,5 +25,7 @@ class Role(db.Model):
|
||||
password = Column(Vault)
|
||||
description = Column(Text)
|
||||
authority_id = Column(Integer, ForeignKey('authorities.id'))
|
||||
authorities = relationship("Authority", secondary=roles_authorities, passive_deletes=True, backref="role", cascade='all,delete')
|
||||
user_id = Column(Integer, ForeignKey('users.id'))
|
||||
users = relationship("User", secondary=roles_users, passive_deletes=True, backref="role", cascade='all,delete')
|
||||
users = relationship("User", secondary=roles_users, viewonly=True, backref="role")
|
||||
certificates = relationship("Certificate", secondary=roles_certificates, backref="role")
|
||||
|
42
lemur/roles/schemas.py
Normal file
42
lemur/roles/schemas.py
Normal file
@ -0,0 +1,42 @@
|
||||
"""
|
||||
.. module: lemur.roles.schemas
|
||||
:platform: unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
from marshmallow import fields
|
||||
from lemur.users.schemas import UserNestedOutputSchema
|
||||
from lemur.authorities.schemas import AuthorityNestedOutputSchema
|
||||
from lemur.common.schema import LemurInputSchema, LemurOutputSchema
|
||||
from lemur.schemas import AssociatedUserSchema, AssociatedAuthoritySchema
|
||||
|
||||
|
||||
class RoleInputSchema(LemurInputSchema):
|
||||
id = fields.Integer()
|
||||
name = fields.String(required=True)
|
||||
username = fields.String()
|
||||
password = fields.String()
|
||||
description = fields.String()
|
||||
authorities = fields.Nested(AssociatedAuthoritySchema, many=True)
|
||||
users = fields.Nested(AssociatedUserSchema, many=True)
|
||||
|
||||
|
||||
class RoleOutputSchema(LemurOutputSchema):
|
||||
id = fields.Integer()
|
||||
name = fields.String()
|
||||
description = fields.String()
|
||||
authorities = fields.Nested(AuthorityNestedOutputSchema, many=True)
|
||||
users = fields.Nested(UserNestedOutputSchema, many=True)
|
||||
|
||||
|
||||
class RoleNestedOutputSchema(LemurOutputSchema):
|
||||
__envelope__ = False
|
||||
id = fields.Integer()
|
||||
name = fields.String()
|
||||
description = fields.String()
|
||||
|
||||
|
||||
role_input_schema = RoleInputSchema()
|
||||
role_output_schema = RoleOutputSchema()
|
||||
roles_output_schema = RoleOutputSchema(many=True)
|
@ -9,8 +9,6 @@
|
||||
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
from flask import g
|
||||
|
||||
from lemur import database
|
||||
from lemur.roles.models import Role
|
||||
from lemur.users.models import User
|
||||
@ -92,10 +90,6 @@ def render(args):
|
||||
:return:
|
||||
"""
|
||||
query = database.session_query(Role)
|
||||
sort_by = args.pop('sort_by')
|
||||
sort_dir = args.pop('sort_dir')
|
||||
page = args.pop('page')
|
||||
count = args.pop('count')
|
||||
filt = args.pop('filter')
|
||||
user_id = args.pop('user_id', None)
|
||||
authority_id = args.pop('authority_id', None)
|
||||
@ -106,20 +100,8 @@ def render(args):
|
||||
if authority_id:
|
||||
query = query.filter(Role.authority_id == authority_id)
|
||||
|
||||
# we make sure that user can see the role - admins can see all
|
||||
if not g.current_user.is_admin:
|
||||
ids = []
|
||||
for role in g.current_user.roles:
|
||||
ids.append(role.id)
|
||||
query = query.filter(Role.id.in_(ids))
|
||||
|
||||
if filt:
|
||||
terms = filt.split(';')
|
||||
query = database.filter(query, Role, terms)
|
||||
|
||||
query = database.find_all(query, Role, args)
|
||||
|
||||
if sort_by and sort_dir:
|
||||
query = database.sort(query, Role, sort_by, sort_dir)
|
||||
|
||||
return database.paginate(query, page, count)
|
||||
return database.sort_and_page(query, Role, args)
|
||||
|
@ -9,32 +9,28 @@
|
||||
"""
|
||||
from flask import Blueprint
|
||||
from flask import make_response, jsonify, abort, g
|
||||
from flask.ext.restful import reqparse, fields, Api
|
||||
from flask.ext.restful import reqparse, Api
|
||||
|
||||
from lemur.roles import service
|
||||
from lemur.auth.service import AuthenticatedResource
|
||||
from lemur.auth.permissions import ViewRoleCredentialsPermission, admin_permission
|
||||
from lemur.common.utils import marshal_items, paginated_parser
|
||||
from lemur.common.utils import paginated_parser
|
||||
|
||||
from lemur.common.schema import validate_schema
|
||||
from lemur.roles.schemas import role_input_schema, role_output_schema, roles_output_schema
|
||||
|
||||
|
||||
mod = Blueprint('roles', __name__)
|
||||
api = Api(mod)
|
||||
|
||||
|
||||
FIELDS = {
|
||||
'name': fields.String,
|
||||
'description': fields.String,
|
||||
'id': fields.Integer,
|
||||
}
|
||||
|
||||
|
||||
class RolesList(AuthenticatedResource):
|
||||
""" Defines the 'roles' endpoint """
|
||||
def __init__(self):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(RolesList, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
@validate_schema(None, roles_output_schema)
|
||||
def get(self):
|
||||
"""
|
||||
.. http:get:: /roles
|
||||
@ -75,9 +71,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 count: count number default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
@ -90,8 +86,8 @@ class RolesList(AuthenticatedResource):
|
||||
return service.render(args)
|
||||
|
||||
@admin_permission.require(http_exception=403)
|
||||
@marshal_items(FIELDS)
|
||||
def post(self):
|
||||
@validate_schema(role_input_schema, role_output_schema)
|
||||
def post(self, data=None):
|
||||
"""
|
||||
.. http:post:: /roles
|
||||
|
||||
@ -136,15 +132,8 @@ class RolesList(AuthenticatedResource):
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
"""
|
||||
self.reqparse.add_argument('name', type=str, location='json', required=True)
|
||||
self.reqparse.add_argument('description', type=str, location='json')
|
||||
self.reqparse.add_argument('username', type=str, location='json')
|
||||
self.reqparse.add_argument('password', type=str, location='json')
|
||||
self.reqparse.add_argument('users', type=list, location='json')
|
||||
|
||||
args = self.reqparse.parse_args()
|
||||
return service.create(args['name'], args.get('password'), args.get('description'), args.get('username'),
|
||||
args.get('users'))
|
||||
return service.create(data['name'], data.get('password'), data.get('description'), data.get('username'),
|
||||
data.get('users'))
|
||||
|
||||
|
||||
class RoleViewCredentials(AuthenticatedResource):
|
||||
@ -197,7 +186,7 @@ class Roles(AuthenticatedResource):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(Roles, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
@validate_schema(None, role_output_schema)
|
||||
def get(self, role_id):
|
||||
"""
|
||||
.. http:get:: /roles/1
|
||||
@ -234,12 +223,12 @@ class Roles(AuthenticatedResource):
|
||||
if not g.current_user.is_admin:
|
||||
user_role_ids = set([r.id for r in g.current_user.roles])
|
||||
if role_id not in user_role_ids:
|
||||
return dict(message="You are not allowed to view a role which you are not a member of"), 400
|
||||
return dict(message="You are not allowed to view a role which you are not a member of"), 403
|
||||
|
||||
return service.get(role_id)
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
def put(self, role_id):
|
||||
@validate_schema(role_input_schema, role_output_schema)
|
||||
def put(self, role_id, data=None):
|
||||
"""
|
||||
.. http:put:: /roles/1
|
||||
|
||||
@ -278,11 +267,7 @@ class Roles(AuthenticatedResource):
|
||||
"""
|
||||
permission = ViewRoleCredentialsPermission(role_id)
|
||||
if permission.can():
|
||||
self.reqparse.add_argument('name', type=str, location='json', required=True)
|
||||
self.reqparse.add_argument('description', type=str, location='json')
|
||||
self.reqparse.add_argument('users', type=list, location='json')
|
||||
args = self.reqparse.parse_args()
|
||||
return service.update(role_id, args['name'], args.get('description'), args.get('users'))
|
||||
return service.update(role_id, data['name'], data.get('description'), data.get('users'))
|
||||
abort(403)
|
||||
|
||||
@admin_permission.require(http_exception=403)
|
||||
@ -326,7 +311,7 @@ class UserRolesList(AuthenticatedResource):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(UserRolesList, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
@validate_schema(None, roles_output_schema)
|
||||
def get(self, user_id):
|
||||
"""
|
||||
.. http:get:: /users/1/roles
|
||||
@ -367,9 +352,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 count: count number default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
"""
|
||||
@ -385,7 +370,7 @@ class AuthorityRolesList(AuthenticatedResource):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(AuthorityRolesList, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
@validate_schema(None, roles_output_schema)
|
||||
def get(self, authority_id):
|
||||
"""
|
||||
.. http:get:: /authorities/1/roles
|
||||
@ -426,9 +411,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 count: count number default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
"""
|
||||
|
212
lemur/schemas.py
Normal file
212
lemur/schemas.py
Normal file
@ -0,0 +1,212 @@
|
||||
"""
|
||||
.. module: lemur.schemas
|
||||
:platform: unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
|
||||
"""
|
||||
from marshmallow import fields, post_load, pre_load, post_dump, validates_schema
|
||||
|
||||
from lemur.authorities.models import Authority
|
||||
from lemur.certificates.models import Certificate
|
||||
from lemur.common import validators
|
||||
from lemur.common.schema import LemurSchema, LemurInputSchema, LemurOutputSchema
|
||||
from lemur.destinations.models import Destination
|
||||
from lemur.notifications.models import Notification
|
||||
from lemur.plugins import plugins
|
||||
from lemur.roles.models import Role
|
||||
from lemur.users.models import User
|
||||
|
||||
|
||||
class AssociatedAuthoritySchema(LemurInputSchema):
|
||||
id = fields.Int()
|
||||
name = fields.String()
|
||||
|
||||
@post_load
|
||||
def get_object(self, data, many=False):
|
||||
if data.get('id'):
|
||||
return Authority.query.filter(Authority.id == data['id']).one()
|
||||
elif data.get('name'):
|
||||
return Authority.query.filter(Authority.name == data['name']).one()
|
||||
|
||||
|
||||
class AssociatedRoleSchema(LemurInputSchema):
|
||||
id = fields.Int(required=True)
|
||||
name = fields.String()
|
||||
|
||||
@post_load
|
||||
def get_object(self, data, many=False):
|
||||
if many:
|
||||
ids = [d['id'] for d in data]
|
||||
return Role.query.filter(Role.id.in_(ids)).all()
|
||||
else:
|
||||
return Role.query.filter(Role.id == data['id']).one()
|
||||
|
||||
|
||||
class AssociatedDestinationSchema(LemurInputSchema):
|
||||
id = fields.Int(required=True)
|
||||
name = fields.String()
|
||||
|
||||
@post_load
|
||||
def get_object(self, data, many=False):
|
||||
if many:
|
||||
ids = [d['id'] for d in data]
|
||||
return Destination.query.filter(Destination.id.in_(ids)).all()
|
||||
else:
|
||||
return Destination.query.filter(Destination.id == data['id']).one()
|
||||
|
||||
|
||||
class AssociatedNotificationSchema(LemurInputSchema):
|
||||
id = fields.Int(required=True)
|
||||
|
||||
@post_load
|
||||
def get_object(self, data, many=False):
|
||||
if many:
|
||||
ids = [d['id'] for d in data]
|
||||
return Notification.query.filter(Notification.id.in_(ids)).all()
|
||||
else:
|
||||
return Notification.query.filter(Notification.id == data['id']).one()
|
||||
|
||||
|
||||
class AssociatedCertificateSchema(LemurInputSchema):
|
||||
id = fields.Int(required=True)
|
||||
|
||||
@post_load
|
||||
def get_object(self, data, many=False):
|
||||
if many:
|
||||
ids = [d['id'] for d in data]
|
||||
return Certificate.query.filter(Certificate.id.in_(ids)).all()
|
||||
else:
|
||||
return Certificate.query.filter(Certificate.id == data['id']).one()
|
||||
|
||||
|
||||
class AssociatedUserSchema(LemurInputSchema):
|
||||
id = fields.Int(required=True)
|
||||
|
||||
@post_load
|
||||
def get_object(self, data, many=False):
|
||||
if many:
|
||||
ids = [d['id'] for d in data]
|
||||
return User.query.filter(User.id.in_(ids)).all()
|
||||
else:
|
||||
return User.query.filter(User.id == data['id']).one()
|
||||
|
||||
|
||||
class PluginInputSchema(LemurInputSchema):
|
||||
plugin_options = fields.List(fields.Dict())
|
||||
slug = fields.String(required=True)
|
||||
title = fields.String()
|
||||
description = fields.String()
|
||||
|
||||
@post_load
|
||||
def get_object(self, data, many=False):
|
||||
data['plugin_object'] = plugins.get(data['slug'])
|
||||
return data
|
||||
|
||||
|
||||
class PluginOutputSchema(LemurOutputSchema):
|
||||
id = fields.Integer()
|
||||
label = fields.String()
|
||||
description = fields.String()
|
||||
active = fields.Boolean()
|
||||
options = fields.List(fields.Dict(), dump_to='pluginOptions')
|
||||
slug = fields.String()
|
||||
title = fields.String()
|
||||
|
||||
|
||||
plugins_output_schema = PluginOutputSchema(many=True)
|
||||
plugin_output_schema = PluginOutputSchema
|
||||
|
||||
|
||||
class BaseExtensionSchema(LemurSchema):
|
||||
@pre_load(pass_many=True)
|
||||
def preprocess(self, data, many):
|
||||
return self.under(data, many=many)
|
||||
|
||||
@post_dump(pass_many=True)
|
||||
def post_process(self, data, many):
|
||||
if data:
|
||||
data = self.camel(data, many=many)
|
||||
return data
|
||||
|
||||
|
||||
class BasicConstraintsSchema(BaseExtensionSchema):
|
||||
pass
|
||||
|
||||
|
||||
class AuthorityIdentifierSchema(BaseExtensionSchema):
|
||||
use_authority_cert = fields.Boolean()
|
||||
|
||||
|
||||
class AuthorityKeyIdentifierSchema(BaseExtensionSchema):
|
||||
use_key_identifier = fields.Boolean()
|
||||
|
||||
|
||||
class CertificateInfoAccessSchema(BaseExtensionSchema):
|
||||
include_aia = fields.Boolean()
|
||||
|
||||
@post_dump
|
||||
def handle_keys(self, data):
|
||||
return {'includeAIA': data['include_aia']}
|
||||
|
||||
|
||||
class KeyUsageSchema(BaseExtensionSchema):
|
||||
use_crl_sign = fields.Boolean()
|
||||
use_data_encipherment = fields.Boolean()
|
||||
use_decipher_only = fields.Boolean()
|
||||
use_encipher_only = fields.Boolean()
|
||||
use_key_encipherment = fields.Boolean()
|
||||
use_digital_signature = fields.Boolean()
|
||||
use_non_repudiation = fields.Boolean()
|
||||
|
||||
|
||||
class ExtendedKeyUsageSchema(BaseExtensionSchema):
|
||||
use_server_authentication = fields.Boolean()
|
||||
use_client_authentication = fields.Boolean()
|
||||
use_eap_over_lan = fields.Boolean()
|
||||
use_eap_over_ppp = fields.Boolean()
|
||||
use_ocsp_signing = fields.Boolean()
|
||||
use_smart_card_authentication = fields.Boolean()
|
||||
use_timestamping = fields.Boolean()
|
||||
|
||||
|
||||
class SubjectKeyIdentifierSchema(BaseExtensionSchema):
|
||||
include_ski = fields.Boolean()
|
||||
|
||||
@post_dump
|
||||
def handle_keys(self, data):
|
||||
return {'includeSKI': data['include_ski']}
|
||||
|
||||
|
||||
class SubAltNameSchema(BaseExtensionSchema):
|
||||
name_type = fields.String(validate=validators.sub_alt_type)
|
||||
value = fields.String()
|
||||
|
||||
@validates_schema
|
||||
def check_sensitive(self, data):
|
||||
if data['name_type'] == 'DNSName':
|
||||
validators.sensitive_domain(data['value'])
|
||||
|
||||
|
||||
class SubAltNamesSchema(BaseExtensionSchema):
|
||||
names = fields.Nested(SubAltNameSchema, many=True)
|
||||
|
||||
|
||||
class CustomOIDSchema(BaseExtensionSchema):
|
||||
oid = fields.String()
|
||||
oid_type = fields.String(validate=validators.oid_type)
|
||||
value = fields.String()
|
||||
|
||||
|
||||
class ExtensionSchema(BaseExtensionSchema):
|
||||
basic_constraints = fields.Nested(BasicConstraintsSchema)
|
||||
key_usage = fields.Nested(KeyUsageSchema)
|
||||
extended_key_usage = fields.Nested(ExtendedKeyUsageSchema)
|
||||
subject_key_identifier = fields.Nested(SubjectKeyIdentifierSchema)
|
||||
sub_alt_names = fields.Nested(SubAltNamesSchema)
|
||||
authority_identifier = fields.Nested(AuthorityIdentifierSchema)
|
||||
authority_key_identifier = fields.Nested(AuthorityKeyIdentifierSchema)
|
||||
certificate_info_access = fields.Nested(CertificateInfoAccessSchema)
|
||||
custom = fields.List(fields.Nested(CustomOIDSchema))
|
37
lemur/sources/schemas.py
Normal file
37
lemur/sources/schemas.py
Normal file
@ -0,0 +1,37 @@
|
||||
"""
|
||||
.. module: lemur.sources.schemas
|
||||
:platform: unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
from marshmallow import fields, post_dump
|
||||
|
||||
from lemur.schemas import PluginInputSchema, PluginOutputSchema
|
||||
from lemur.common.schema import LemurInputSchema, LemurOutputSchema
|
||||
|
||||
|
||||
class SourceInputSchema(LemurInputSchema):
|
||||
id = fields.Integer()
|
||||
label = fields.String(required=True)
|
||||
description = fields.String()
|
||||
plugin = fields.Nested(PluginInputSchema)
|
||||
active = fields.Boolean()
|
||||
|
||||
|
||||
class SourceOutputSchema(LemurOutputSchema):
|
||||
id = fields.Integer()
|
||||
label = fields.String()
|
||||
description = fields.String()
|
||||
plugin = fields.Nested(PluginOutputSchema)
|
||||
options = fields.List(fields.Dict())
|
||||
fields.Boolean()
|
||||
|
||||
@post_dump
|
||||
def fill_object(self, data):
|
||||
data['plugin']['pluginOptions'] = data['options']
|
||||
return data
|
||||
|
||||
source_input_schema = SourceInputSchema()
|
||||
sources_output_schema = SourceOutputSchema(many=True)
|
||||
source_output_schema = SourceOutputSchema()
|
@ -20,7 +20,7 @@ def _disassociate_certs_from_source(current_certificates, found_certificates, so
|
||||
missing = []
|
||||
for cc in current_certificates:
|
||||
for fc in found_certificates:
|
||||
if fc['public_certificate'] == cc.body:
|
||||
if fc['body'] == cc.body:
|
||||
break
|
||||
else:
|
||||
missing.append(cc)
|
||||
@ -76,12 +76,12 @@ 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)
|
||||
|
||||
for certificate in certificates:
|
||||
exists = cert_service.find_duplicates(certificate['public_certificate'])
|
||||
exists = cert_service.find_duplicates(certificate['body'])
|
||||
|
||||
if not exists:
|
||||
sync_create(certificate, source)
|
||||
@ -174,10 +174,6 @@ def get_all():
|
||||
|
||||
|
||||
def render(args):
|
||||
sort_by = args.pop('sort_by')
|
||||
sort_dir = args.pop('sort_dir')
|
||||
page = args.pop('page')
|
||||
count = args.pop('count')
|
||||
filt = args.pop('filter')
|
||||
certificate_id = args.pop('certificate_id', None)
|
||||
|
||||
@ -191,9 +187,4 @@ def render(args):
|
||||
terms = filt.split(';')
|
||||
query = database.filter(query, Source, terms)
|
||||
|
||||
query = database.find_all(query, Source, args)
|
||||
|
||||
if sort_by and sort_dir:
|
||||
query = database.sort(query, Source, sort_by, sort_dir)
|
||||
|
||||
return database.paginate(query, page, count)
|
||||
return database.sort_and_page(query, Source, args)
|
||||
|
@ -7,35 +7,28 @@
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
from flask import Blueprint
|
||||
from flask.ext.restful import Api, reqparse, fields
|
||||
from flask.ext.restful import Api, reqparse
|
||||
from lemur.sources import service
|
||||
|
||||
from lemur.common.schema import validate_schema
|
||||
from lemur.sources.schemas import source_input_schema, source_output_schema, sources_output_schema
|
||||
|
||||
from lemur.auth.service import AuthenticatedResource
|
||||
from lemur.auth.permissions import admin_permission
|
||||
from lemur.common.utils import paginated_parser, marshal_items
|
||||
from lemur.common.utils import paginated_parser
|
||||
|
||||
|
||||
mod = Blueprint('sources', __name__)
|
||||
api = Api(mod)
|
||||
|
||||
|
||||
FIELDS = {
|
||||
'description': fields.String,
|
||||
'sourceOptions': fields.Raw(attribute='options'),
|
||||
'pluginName': fields.String(attribute='plugin_name'),
|
||||
'lastRun': fields.DateTime(attribute='last_run', dt_format='iso8061'),
|
||||
'label': fields.String,
|
||||
'id': fields.Integer,
|
||||
}
|
||||
|
||||
|
||||
class SourcesList(AuthenticatedResource):
|
||||
""" Defines the 'sources' endpoint """
|
||||
def __init__(self):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(SourcesList, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
@validate_schema(None, sources_output_schema)
|
||||
def get(self):
|
||||
"""
|
||||
.. http:get:: /sources
|
||||
@ -61,7 +54,7 @@ class SourcesList(AuthenticatedResource):
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"sourceOptions": [
|
||||
"options": [
|
||||
{
|
||||
"name": "accountNumber",
|
||||
"required": true,
|
||||
@ -83,9 +76,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 count: count number default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
"""
|
||||
@ -94,8 +87,8 @@ class SourcesList(AuthenticatedResource):
|
||||
return service.render(args)
|
||||
|
||||
@admin_permission.require(http_exception=403)
|
||||
@marshal_items(FIELDS)
|
||||
def post(self):
|
||||
@validate_schema(source_input_schema, source_output_schema)
|
||||
def post(self, data=None):
|
||||
"""
|
||||
.. http:post:: /sources
|
||||
|
||||
@ -110,7 +103,7 @@ class SourcesList(AuthenticatedResource):
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
{
|
||||
"sourceOptions": [
|
||||
"options": [
|
||||
{
|
||||
"name": "accountNumber",
|
||||
"required": true,
|
||||
@ -136,7 +129,7 @@ class SourcesList(AuthenticatedResource):
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"sourceOptions": [
|
||||
"options": [
|
||||
{
|
||||
"name": "accountNumber",
|
||||
"required": true,
|
||||
@ -158,12 +151,7 @@ class SourcesList(AuthenticatedResource):
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
"""
|
||||
self.reqparse.add_argument('label', type=str, location='json', required=True)
|
||||
self.reqparse.add_argument('plugin', type=dict, location='json', required=True)
|
||||
self.reqparse.add_argument('description', type=str, location='json')
|
||||
|
||||
args = self.reqparse.parse_args()
|
||||
return service.create(args['label'], args['plugin']['slug'], args['plugin']['pluginOptions'], args['description'])
|
||||
return service.create(data['label'], data['plugin']['slug'], data['plugin']['plugin_options'], data['description'])
|
||||
|
||||
|
||||
class Sources(AuthenticatedResource):
|
||||
@ -171,7 +159,7 @@ class Sources(AuthenticatedResource):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(Sources, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
@validate_schema(None, source_output_schema)
|
||||
def get(self, source_id):
|
||||
"""
|
||||
.. http:get:: /sources/1
|
||||
@ -195,7 +183,7 @@ class Sources(AuthenticatedResource):
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"sourceOptions": [
|
||||
"options": [
|
||||
{
|
||||
"name": "accountNumber",
|
||||
"required": true,
|
||||
@ -218,8 +206,8 @@ class Sources(AuthenticatedResource):
|
||||
return service.get(source_id)
|
||||
|
||||
@admin_permission.require(http_exception=403)
|
||||
@marshal_items(FIELDS)
|
||||
def put(self, source_id):
|
||||
@validate_schema(source_input_schema, source_output_schema)
|
||||
def put(self, source_id, data=None):
|
||||
"""
|
||||
.. http:put:: /sources/1
|
||||
|
||||
@ -234,7 +222,7 @@ class Sources(AuthenticatedResource):
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
{
|
||||
"sourceOptions": [
|
||||
"options": [
|
||||
{
|
||||
"name": "accountNumber",
|
||||
"required": true,
|
||||
@ -260,7 +248,7 @@ class Sources(AuthenticatedResource):
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"sourceOptions": [
|
||||
"options": [
|
||||
{
|
||||
"name": "accountNumber",
|
||||
"required": true,
|
||||
@ -283,12 +271,7 @@ class Sources(AuthenticatedResource):
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
"""
|
||||
self.reqparse.add_argument('label', type=str, location='json', required=True)
|
||||
self.reqparse.add_argument('plugin', type=dict, location='json', required=True)
|
||||
self.reqparse.add_argument('description', type=str, location='json')
|
||||
|
||||
args = self.reqparse.parse_args()
|
||||
return service.update(source_id, args['label'], args['plugin']['pluginOptions'], args['description'])
|
||||
return service.update(source_id, data['label'], data['plugin']['plugin_options'], data['description'])
|
||||
|
||||
@admin_permission.require(http_exception=403)
|
||||
def delete(self, source_id):
|
||||
@ -301,7 +284,7 @@ class CertificateSources(AuthenticatedResource):
|
||||
def __init__(self):
|
||||
super(CertificateSources, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
@validate_schema(None, sources_output_schema)
|
||||
def get(self, certificate_id):
|
||||
"""
|
||||
.. http:get:: /certificates/1/sources
|
||||
@ -327,7 +310,7 @@ class CertificateSources(AuthenticatedResource):
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"sourceOptions": [
|
||||
"options": [
|
||||
{
|
||||
"name": "accountNumber",
|
||||
"required": true,
|
||||
@ -349,9 +332,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 count: count number default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
"""
|
||||
|
27
lemur/static/app/angular/app.js
vendored
27
lemur/static/app/angular/app.js
vendored
@ -17,7 +17,9 @@
|
||||
'satellizer',
|
||||
'ngLetterAvatar',
|
||||
'angular-clipboard',
|
||||
'ngFileSaver'
|
||||
'ngFileSaver',
|
||||
'ngSanitize',
|
||||
'ui.select'
|
||||
]);
|
||||
|
||||
|
||||
@ -50,7 +52,7 @@
|
||||
|
||||
_.each(providers, function(provider) {
|
||||
if ($authProvider.hasOwnProperty(provider.name)) {
|
||||
$authProvider[provider.name] = provider;
|
||||
$authProvider[provider.name](provider);
|
||||
} else {
|
||||
$authProvider.oauth2(provider);
|
||||
}
|
||||
@ -89,6 +91,15 @@
|
||||
};
|
||||
});
|
||||
|
||||
lemur.directive('lemurBadRequest', [function () {
|
||||
return {
|
||||
template: '<h4>{{ directiveData.message }}</h4>' +
|
||||
'<div ng-repeat="(key, value) in directiveData.reasons">' +
|
||||
'<strong>{{ key | titleCase }}</strong> - {{ value }}</strong>' +
|
||||
'</div>'
|
||||
};
|
||||
}]);
|
||||
|
||||
lemur.factory('LemurRestangular', function (Restangular, $location, $auth) {
|
||||
return Restangular.withConfig(function (RestangularConfigurer) {
|
||||
RestangularConfigurer.setBaseUrl('http://localhost:8000/api/1');
|
||||
@ -109,18 +120,6 @@
|
||||
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()) {
|
||||
|
@ -2,9 +2,8 @@
|
||||
|
||||
angular.module('lemur')
|
||||
|
||||
.controller('AuthorityEditController', function ($scope, $modalInstance, AuthorityApi, AuthorityService, RoleService, toaster, editId){
|
||||
.controller('AuthorityEditController', function ($scope, $uibModalInstance, AuthorityApi, AuthorityService, RoleService, toaster, editId){
|
||||
AuthorityApi.get(editId).then(function (authority) {
|
||||
AuthorityService.getRoles(authority);
|
||||
$scope.authority = authority;
|
||||
});
|
||||
|
||||
@ -18,7 +17,7 @@ angular.module('lemur')
|
||||
title: authority.name,
|
||||
body: 'Successfully updated!'
|
||||
});
|
||||
$modalInstance.close();
|
||||
$uibModalInstance.close();
|
||||
},
|
||||
function (response) {
|
||||
toaster.pop({
|
||||
@ -31,16 +30,25 @@ angular.module('lemur')
|
||||
};
|
||||
|
||||
$scope.cancel = function () {
|
||||
$modalInstance.dismiss('cancel');
|
||||
$uibModalInstance.dismiss('cancel');
|
||||
};
|
||||
})
|
||||
|
||||
.controller('AuthorityCreateController', function ($scope, $modalInstance, AuthorityService, LemurRestangular, RoleService, PluginService, WizardHandler, toaster) {
|
||||
.controller('AuthorityCreateController', function ($scope, $uibModalInstance, AuthorityService, AuthorityApi, LemurRestangular, RoleService, PluginService, WizardHandler, toaster) {
|
||||
$scope.authority = LemurRestangular.restangularizeElement(null, {}, 'authorities');
|
||||
|
||||
// set the defaults
|
||||
AuthorityService.getDefaults($scope.authority);
|
||||
|
||||
$scope.getAuthoritiesByName = function (value) {
|
||||
return AuthorityService.findAuthorityByName(value).then(function (authorities) {
|
||||
$scope.authorities = authorities;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.cancel = function () {
|
||||
$uibModalInstance.dismiss('cancel');
|
||||
};
|
||||
|
||||
$scope.create = function (authority) {
|
||||
WizardHandler.wizard().context.loading = true;
|
||||
AuthorityService.create(authority).then(
|
||||
@ -50,7 +58,7 @@ angular.module('lemur')
|
||||
title: authority.name,
|
||||
body: 'Was created!'
|
||||
});
|
||||
$modalInstance.close();
|
||||
$uibModalInstance.close();
|
||||
},
|
||||
function (response) {
|
||||
toaster.pop({
|
||||
@ -65,23 +73,42 @@ angular.module('lemur')
|
||||
|
||||
PluginService.getByType('issuer').then(function (plugins) {
|
||||
$scope.plugins = plugins;
|
||||
$scope.authority.plugin = plugins[0];
|
||||
});
|
||||
|
||||
$scope.roleService = RoleService;
|
||||
|
||||
$scope.authorityService = AuthorityService;
|
||||
|
||||
$scope.open = function($event) {
|
||||
$event.preventDefault();
|
||||
$event.stopPropagation();
|
||||
|
||||
$scope.opened1 = true;
|
||||
$scope.dateOptions = {
|
||||
formatYear: 'yy',
|
||||
maxDate: new Date(2020, 5, 22),
|
||||
minDate: new Date(),
|
||||
startingDay: 1
|
||||
};
|
||||
|
||||
$scope.open2 = function($event) {
|
||||
$event.preventDefault();
|
||||
$event.stopPropagation();
|
||||
|
||||
$scope.opened2 = true;
|
||||
$scope.open1 = function() {
|
||||
$scope.popup1.opened = true;
|
||||
};
|
||||
|
||||
$scope.open2 = function() {
|
||||
$scope.popup2.opened = true;
|
||||
};
|
||||
|
||||
$scope.setDate = function(year, month, day) {
|
||||
$scope.dt = new Date(year, month, day);
|
||||
};
|
||||
|
||||
$scope.formats = ['dd-MMMM-yyyy', 'yyyy/MM/dd', 'dd.MM.yyyy', 'shortDate'];
|
||||
$scope.format = $scope.formats[0];
|
||||
$scope.altInputFormats = ['M!/d!/yyyy'];
|
||||
|
||||
$scope.popup1 = {
|
||||
opened: false
|
||||
};
|
||||
|
||||
$scope.popup2 = {
|
||||
opened: false
|
||||
};
|
||||
|
||||
});
|
||||
|
@ -1,4 +1,5 @@
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" ng-click="cancel()" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h3 class="modal-title"><span ng-show="!authority.id">Create</span><span ng-show="authority.id">Edit</span> Authority <span class="text-muted"><small>The nail that sticks out farthest gets hammered the hardest</small></span></h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
@ -6,7 +6,7 @@
|
||||
Country
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input name="country" ng-model="authority.caDN.country" placeholder="Country" class="form-control" ng-init="authority.caDN.country = 'US'" required/>
|
||||
<input name="country" ng-model="authority.country" placeholder="Country" class="form-control" required/>
|
||||
<p ng-show="dnForm.country.$invalid && !dnForm.country.$pristine" class="help-block">You must enter a country</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -16,7 +16,7 @@
|
||||
State
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input name="state" ng-model="authority.caDN.state" placeholder="State" class="form-control" ng-init="authority.caDN.state = 'CA'" required/>
|
||||
<input name="state" ng-model="authority.state" placeholder="State" class="form-control" required/>
|
||||
<p ng-show="dnForm.state.$invalid && !dnForm.state.$pristine" class="help-block">You must enter a state</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -26,7 +26,7 @@
|
||||
Location
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input name="location" ng-model="authority.caDN.location" placeholder="Location" class="form-control" ng-init="authority.caDN.location = 'Los Gatos'"required/>
|
||||
<input name="location" ng-model="authority.location" placeholder="Location" class="form-control" required/>
|
||||
<p ng-show="dnForm.location.$invalid && !dnForm.location.$pristine" class="help-block">You must enter a location</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -36,7 +36,7 @@
|
||||
Organization
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input name="organization" ng-model="authority.caDN.organization" placeholder="Organization" class="form-control" ng-init="authority.caDN.organization = 'Netflix'" required/>
|
||||
<input name="organization" ng-model="authority.organization" placeholder="Organization" class="form-control" ng-init="authority.organization = 'Netflix'" required/>
|
||||
<p ng-show="dnForm.organization.$invalid && !dnForm.organization.$pristine" class="help-block">You must enter a organization</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -46,7 +46,7 @@
|
||||
Organizational Unit
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input name="organizationalUnit" ng-model="authority.caDN.organizationalUnit" placeholder="Organizational Unit" class="form-control" ng-init="authority.caDN.organizationalUnit = 'Operations'"required/>
|
||||
<input name="organizationalUnit" ng-model="authority.organizationalUnit" placeholder="Organizational Unit" class="form-control" required/>
|
||||
<p ng-show="dnForm.organization.$invalid && !dnForm.organizationalUnit.$pristine" class="help-block">You must enter a organizational unit</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,64 +1,46 @@
|
||||
<div class="modal-header">
|
||||
<div class="modal-title">
|
||||
<h3 class="modal-header">Edit <span class="text-muted"><small>{{ authority.name }}</small></span></h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="createForm" class="form-horizontal" role="form" novalidate>
|
||||
<div class="form-group"
|
||||
ng-class="{'has-error': editForm.owner.$invalid, 'has-success': !editForm.owner.$invalid&&editForm.owner.$dirty}">
|
||||
<label class="control-label col-sm-2">
|
||||
Owner
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="email" name="owner" ng-model="authority.owner" placeholder="owner@example.com"
|
||||
class="form-control" required/>
|
||||
|
||||
<p ng-show="editForm.owner.$invalid && !editForm.owner.$pristine" class="help-block">Enter a valid
|
||||
email.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group"
|
||||
ng-class="{'has-error': editForm.description.$invalid, 'has-success': !editForm.$invalid&&editForm.description.$dirty}">
|
||||
<label class="control-label col-sm-2">
|
||||
Description
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea name="description" ng-model="authority.description" placeholder="Something elegant" class="form-control" required></textarea>
|
||||
<p ng-show="editForm.description.$invalid && !editForm.description.$pristine" class="help-block">You must give a short description about this authority will be used for, this description should only include alphanumeric characters</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-2">
|
||||
Roles
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<div class="input-group">
|
||||
<input type="text" ng-model="authority.selectedRole" placeholder="Role Name"
|
||||
typeahead="role.name for role in roleService.findRoleByName($viewValue)" typeahead-loading="loadingRoles"
|
||||
class="form-control input-md" typeahead-on-select="authority.attachRole($item)" typeahead-min-wait="50"
|
||||
tooltip="Roles control which authorities a user can issue certificates from"
|
||||
tooltip-trigger="focus" tooltip-placement="top">
|
||||
<span class="input-group-btn">
|
||||
<button ng-model="roles.show" class="btn btn-md btn-default" btn-checkbox btn-checkbox-true="1" btn-checkbox-false="0">
|
||||
<span class="badge">{{ authority.roles.length || 0 }}</span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<table ng-show="authority.roles" class="table">
|
||||
<tr ng-repeat="role in authority.roles track by $index">
|
||||
<td><a class="btn btn-sm btn-info" href="#/roles/{{ role.id }}/edit">{{ role.name }}</a></td>
|
||||
<td><span class="text-muted">{{ role.description }}</span></td>
|
||||
<td>
|
||||
<button type="button" ng-click="authority.removeRole($index)" class="btn btn-danger btn-sm pull-right">Remove</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button ng-click="save(authority)" type="submit" ng-disabled="createForm.$invalid" class="btn btn-primary">Save</button>
|
||||
<button ng-click="cancel()" class="btn btn-danger">Cancel</button>
|
||||
</div>
|
||||
<button type="button" class="close" ng-click="cancel()" aria-label="Close"><span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<h3>Edit <span class="text-muted"><small>{{ authority.name }}</small></span></h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="createForm" class="form-horizontal" role="form" novalidate>
|
||||
<div class="form-group"
|
||||
ng-class="{'has-error': editForm.owner.$invalid, 'has-success': !editForm.owner.$invalid&&editForm.owner.$dirty}">
|
||||
<label class="control-label col-sm-2">
|
||||
Owner
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="email" name="owner" ng-model="authority.owner" placeholder="owner@example.com"
|
||||
class="form-control" required/>
|
||||
|
||||
<p ng-show="editForm.owner.$invalid && !editForm.owner.$pristine" class="help-block">Enter a valid
|
||||
email.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group"
|
||||
ng-class="{'has-error': editForm.description.$invalid, 'has-success': !editForm.$invalid&&editForm.description.$dirty}">
|
||||
<label class="control-label col-sm-2">
|
||||
Description
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea name="description" ng-model="authority.description" placeholder="Something elegant"
|
||||
class="form-control" required></textarea>
|
||||
<p ng-show="editForm.description.$invalid && !editForm.description.$pristine" class="help-block">You
|
||||
must give a short description about this authority will be used for, this description should only
|
||||
include alphanumeric characters</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-2">
|
||||
Roles
|
||||
</label>
|
||||
<div class="col-sm-10" ng-model="authority" role-select></div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button ng-click="save(authority)" type="submit" ng-disabled="createForm.$invalid" class="btn btn-primary">Save
|
||||
</button>
|
||||
<button ng-click="cancel()" class="btn btn-danger">Cancel</button>
|
||||
</div>
|
||||
|
@ -8,7 +8,7 @@
|
||||
</div>
|
||||
<div class="col-sm-5">
|
||||
<div class="input-group">
|
||||
<input tooltip-trigger="focus" tooltip-placement="top" tooltip="String or Base64-encoded DER ASN.1 structure for the value" class="form-control" name="value" ng-model="authority.subAltValue" placeholder="Value" class="form-control" required/>
|
||||
<input tooltip-trigger="focus" tooltip-placement="top" uib-tooltip="String or Base64-encoded DER ASN.1 structure for the value" class="form-control" name="value" ng-model="authority.subAltValue" placeholder="Value" class="form-control" required/>
|
||||
<span class="input-group-btn">
|
||||
<button ng-click="authority.attachSubAltName()" class="btn btn-info">Add</button>
|
||||
</span>
|
||||
@ -132,12 +132,12 @@
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<div class="checkbox">
|
||||
<label tooltip-trigger="mouseenter" tooltip-placement="top" tooltip="Put Issuer's keyIdentifier in this extension" >
|
||||
<label tooltip-trigger="mouseenter" tooltip-placement="top" uib-tooltip="Put Issuer's keyIdentifier in this extension" >
|
||||
<input type="checkbox" ng-model="authority.extensions.authorityKeyIdentifier.useKeyIdentifier">Key Identifier
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label tooltip-trigger="mouseenter" tooltip-placement="top" tooltip="Put Issuer's Name and Serial number" >
|
||||
<label tooltip-trigger="mouseenter" tooltip-placement="top" uib-tooltip="Put Issuer's Name and Serial number" >
|
||||
<input type="checkbox" ng-model="authority.extensions.authorityKeyIdentifier.useAuthorityCert">Authority Certificate
|
||||
</label>
|
||||
</div>
|
||||
@ -149,7 +149,7 @@
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<div class="checkbox">
|
||||
<label tooltip-trigger="mouseenter" tooltip-placement="top" tooltip="Ask CA to include/not include AIA extension" >
|
||||
<label tooltip-trigger="mouseenter" tooltip-placement="top" uib-tooltip="Ask CA to include/not include AIA extension" >
|
||||
<input type="checkbox" ng-model="authority.extensions.authorityInfoAccess.includeAIA">Include AIA
|
||||
</label>
|
||||
</div>
|
||||
@ -161,7 +161,7 @@
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<div class="checkbox">
|
||||
<label tooltip-trigger="mouseenter" tooltip-placement="top" tooltip="Ask CA to include/not include Subject Key Identifier" >
|
||||
<label tooltip-trigger="mouseenter" tooltip-placement="top" uib-tooltip="Ask CA to include/not include Subject Key Identifier" >
|
||||
<input type="checkbox" ng-model="authority.extensions.subjectKeyIdentifier.includeSKI">Include SKI
|
||||
</label>
|
||||
</div>
|
||||
@ -180,14 +180,14 @@
|
||||
Custom
|
||||
</label>
|
||||
<div class="col-sm-2">
|
||||
<input tooltip-trigger="focus" tooltip-placement="top" tooltip="OID for the custom extension e.g. 1.12.123.12.10" class="form-control" name="oid" ng-model="authority.customOid" placeholder="Oid" class="form-control" required/>
|
||||
<input tooltip-trigger="focus" tooltip-placement="top" uib-tooltip="OID for the custom extension e.g. 1.12.123.12.10" class="form-control" name="oid" ng-model="authority.customOid" placeholder="Oid" class="form-control" required/>
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<select tooltip-trigger="focus" tooltip-placement="top" tooltip="Encoding for value" class="form-control col-sm-2" ng-model="authority.customEncoding" ng-options="item for item in ['b64asn1', 'string', 'ia5string']"></select>
|
||||
<select tooltip-trigger="focus" tooltip-placement="top" uib-tooltip="Encoding for value" class="form-control col-sm-2" ng-model="authority.customEncoding" ng-options="item for item in ['b64asn1', 'string', 'ia5string']"></select>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="input-group">
|
||||
<input tooltip-trigger="focus" tooltip-placement="top" tooltip="String or Base64-encoded DER ASN.1 structure for the value" class="form-control" name="value" ng-model="authority.customValue" placeholder="Value" class="form-control" required/>
|
||||
<input tooltip-trigger="focus" tooltip-placement="top" uib-tooltip="String or Base64-encoded DER ASN.1 structure for the value" class="form-control" name="value" ng-model="authority.customValue" placeholder="Value" class="form-control" required/>
|
||||
<span class="input-group-btn">
|
||||
<button ng-click="authority.attachCustom()" class="btn btn-info">Add</button>
|
||||
</span>
|
||||
|
@ -1,30 +1,10 @@
|
||||
<div class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-2">
|
||||
Type
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control" ng-model="authority.caType" ng-options="option for option in ['root', 'subca']" ng-init="authority.caType = 'root'"required></select>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-show="authority.caType == 'subca'" class="form-group">
|
||||
<label class="control-label col-sm-2">
|
||||
Parent Authority
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" ng-model="authority.caParent" placeholder="Parent Authority Name"
|
||||
typeahead="authority.name for authority in authorityService.findAuthorityByName($viewValue)" typeahead-loading="loadingAuthorities"
|
||||
class="form-control input-md" typeahead-min-wait="50"
|
||||
tooltip="When you specify a subordinate certificate authority you must specific the parent authority"
|
||||
tooltip-trigger="focus" tooltip-placement="top">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-2">
|
||||
Signing Algorithm
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control" ng-model="authority.caSigningAlgo" ng-options="option for option in ['sha1WithRSA', 'sha256WithRSA']" ng-init="authority.caSigningAlgo = 'sha256WithRSA'"></select>
|
||||
<select class="form-control" ng-model="authority.signingAlgorithm" ng-options="option for option in ['sha1WithRSA', 'sha256WithRSA']" ng-init="authority.signingAlgorithm = 'sha256WithRSA'"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@ -32,7 +12,7 @@
|
||||
Sensitivity
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control" ng-model="authority.caSensitivity" ng-options="option for option in ['medium', 'high']" ng-init="authority.caSensitivity = 'medium'"></select>
|
||||
<select class="form-control" ng-model="authority.sensitivity" ng-options="option for option in ['medium', 'high']" ng-init="authority.sensitivity = 'medium'"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@ -43,7 +23,7 @@
|
||||
<select class="form-control" ng-model="authority.keyType" ng-options="option for option in ['RSA2048', 'RSA4096']" ng-init="authority.keyType = 'RSA2048'"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-show="authority.caSensitivity == 'high'" class="form-group">
|
||||
<div ng-show="authority.sensitivity == 'high'" class="form-group">
|
||||
<label class="control-label col-sm-2">
|
||||
Key Name
|
||||
</label>
|
||||
@ -56,7 +36,7 @@
|
||||
Serial Number
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="number" name="serialNumber" ng-model="authority.caSerialNumber" placeholder="Serial Number" class="form-control"/>
|
||||
<input type="number" name="serialNumber" ng-model="authority.serialNumber" placeholder="Serial Number" class="form-control"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@ -64,7 +44,7 @@
|
||||
First Serial Number
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="number" name="firstSerialNumber" ng-model="authority.caFirstSerial" placeholder="First Serial Number" class="form-control" ng-init="1000" />
|
||||
<input type="number" name="firstSerialNumber" ng-model="authority.firstSerial" placeholder="First Serial Number" class="form-control" ng-init="1000" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@ -72,7 +52,7 @@
|
||||
Plugin
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control" ng-model="authority.pluginName" ng-options="plugin.slug as plugin.title for plugin in plugins" ng-init="authority.pluginName = 'cloudca-issuer'" required></select>
|
||||
<select class="form-control" ng-model="authority.plugin" ng-options="plugin as plugin.title for plugin in plugins" required></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -5,12 +5,12 @@
|
||||
<div class="col-sm-10">
|
||||
<div class="input-group">
|
||||
<input type="text" ng-model="authority.selectedRole" placeholder="Role Name"
|
||||
typeahead="role.name for role in roleService.findRoleByName($viewValue)" typeahead-loading="loadingAccounts"
|
||||
class="form-control input-md" typeahead-on-select="authority.attachRole($item)" typeahead-min-wait="50"
|
||||
tooltip="These are the User roles you wish to associated with your authority"
|
||||
uib-typeahead="role.name for role in roleService.findRoleByName($viewValue)" typeahead-loading="loadingAccounts"
|
||||
class="form-control input-md" typeahead-on-select="authority.attachRole($item)" typeahead-wait-ms="500"
|
||||
uib-tooltip="These are the User roles you wish to associated with your authority"
|
||||
tooltip-trigger="focus" tooltip-placement="top">
|
||||
<span class="input-group-btn">
|
||||
<button ng-model="roles.show" class="btn btn-md btn-default" btn-checkbox btn-checkbox-true="1" btn-checkbox-false="0">
|
||||
<button ng-model="roles.show" class="btn btn-md btn-default" uib-btn-checkbox btn-checkbox-true="1" btn-checkbox-false="0">
|
||||
<span class="badge">{{ authority.roles.length || 0 }}</span>
|
||||
</button>
|
||||
</span>
|
||||
|
@ -1,74 +1,140 @@
|
||||
<form name="trackingForm" novalidate>
|
||||
<div class="form-horizontal">
|
||||
<div class="form-group"
|
||||
ng-class="{'has-error': trackingForm.caName.$invalid, 'has-success': !trackingForm.caName.$invalid&&trackingForm.caName.$dirty}">
|
||||
<label class="control-label col-sm-2">
|
||||
Name
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input name="caName" ng-model="authority.caName" placeholder="Name" tooltip="This will be the name of your authority, it is the name you will reference when creating new certificates" class="form-control" ng-pattern="/^[A-Za-z0-9_-]+$/" required/>
|
||||
<p ng-show="trackingForm.caName.$invalid && !trackingForm.caName.$pristine" class="help-block">You must enter a valid authority name, spaces are not allowed</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group"
|
||||
ng-class="{'has-error': trackingForm.ownerEmail.$invalid, 'has-success': !trackingForm.$invalid&&trackingForm.ownerEmail.$dirty}">
|
||||
<label class="control-label col-sm-2">
|
||||
Owner
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="email" name="ownerEmail" ng-model="authority.ownerEmail" placeholder="TeamDL@example.com" tooltip="This is the authorities team distribution list or the main point of contact for this authority" class="form-control" required/>
|
||||
<p ng-show="trackingForm.ownerEmail.$invalid && !trackingForm.ownerEmail.$pristine" class="help-block">You must enter an Certificate Authority owner</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group"
|
||||
ng-class="{'has-error': trackingForm.caDescription.$invalid, 'has-success': !trackingForm.$invalid&&trackingForm.caDescription.$dirty}">
|
||||
<label class="control-label col-sm-2">
|
||||
Description
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea name="caDescription" ng-model="authority.caDescription" placeholder="Something elegant" class="form-control" ng-maxlength="250" required></textarea>
|
||||
<p ng-show="trackingForm.caDescription.$invalid && !trackingForm.caDescription.$pristine" class="help-block">You must give a short description about this authority will be used for</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group"
|
||||
ng-class="{'has-error': trackingForm.commonName.$invalid, 'has-success': !trackingForm.$invalid&&trackingForm.commonName.$dirty}">
|
||||
<label class="control-label col-sm-2">
|
||||
Common Name
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input name="commonName" ng-model="authority.caDN.commonName" placeholder="Common Name" class="form-control" ng-maxlength="64" required/>
|
||||
<p ng-show="trackingForm.commonName.$invalid && !trackingForm.commonName.$pristine" class="help-block">You must enter a common name and it must be less than 64 characters in length</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group"
|
||||
ng-class="{'has-error': trackingForm.validityEnd.$invalid || trackingForm.validityStart.$invalid, 'has-success': !trackingForm.$invalid&&trackingForm.validityEnd.$dirty&&trackingForm.validityStart.$dirty}">
|
||||
<label class="control-label col-sm-2">
|
||||
Validity Range
|
||||
</label>
|
||||
<div class="col-sm-4">
|
||||
<div>
|
||||
<div class="input-group">
|
||||
<input name="validityStart" tooltip="Starting Date" class="form-control" datepicker-popup="yyyy/MM/dd" is-open="opened1" ng-model="authority.validityStart" required/>
|
||||
<p ng-show="trackingForm.validityStart.$invalid && !trackingForm.validityStart.$pristine" class="help-block">A start date is required!</p>
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default" ng-click="open($event)"><i class="glyphicon glyphicon-calendar"></i></button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="form-horizontal">
|
||||
<div class="form-group"
|
||||
ng-class="{'has-error': trackingForm.name.$invalid, 'has-success': !trackingForm.name.$invalid&&trackingForm.name.$dirty}">
|
||||
<label class="control-label col-sm-2">
|
||||
Name
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input name="name" ng-model="authority.name" placeholder="Name"
|
||||
uib-tooltip="This will be the name of your authority, it is the name you will reference when creating new certificates"
|
||||
class="form-control" ng-pattern="/^[A-Za-z0-9_-]+$/" required/>
|
||||
<p ng-show="trackingForm.name.$invalid && !trackingForm.name.$pristine" class="help-block">You must
|
||||
enter a valid authority name, spaces are not allowed</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span style="padding-top: 15px" class="text-center col-sm-2"><label><span class="glyphicon glyphicon-resize-horizontal"></span></label></span>
|
||||
<div class="col-sm-4">
|
||||
<div>
|
||||
<div class="input-group">
|
||||
<input name="validityEnd" tooltip="Ending Date" class="form-control" datepicker-popup="yyyy/MM/dd" is-open="opened2" ng-model="authority.validityEnd" required/>
|
||||
<p ng-show="trackingForm.validityEnd.$invalid && !trackingForm.validityEnd.$pristine" class="help-block">A end date is required!</p>
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default" ng-click="open2($event)"><i class="glyphicon glyphicon-calendar"></i></button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="form-group"
|
||||
ng-class="{'has-error': trackingForm.owner.$invalid, 'has-success': !trackingForm.$invalid&&trackingForm.owner.$dirty}">
|
||||
<label class="control-label col-sm-2">
|
||||
Owner
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="email" name="owner" ng-model="authority.owner" placeholder="TeamDL@example.com"
|
||||
uib-tooltip="This is the authorities team distribution list or the main point of contact for this authority"
|
||||
class="form-control" required/>
|
||||
<p ng-show="trackingForm.owner.$invalid && !trackingForm.owner.$pristine" class="help-block">You must
|
||||
enter an Certificate Authority owner</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group"
|
||||
ng-class="{'has-error': trackingForm.description.$invalid, 'has-success': !trackingForm.$invalid&&trackingForm.description.$dirty}">
|
||||
<label class="control-label col-sm-2">
|
||||
Description
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea name="description" ng-model="authority.description" placeholder="Something elegant"
|
||||
class="form-control" ng-maxlength="250" required></textarea>
|
||||
<p ng-show="trackingForm.description.$invalid && !trackingForm.description.$pristine"
|
||||
class="help-block">You must give a short description about this authority will be used for</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group"
|
||||
ng-class="{'has-error': trackingForm.commonName.$invalid, 'has-success': !trackingForm.$invalid&&trackingForm.commonName.$dirty}">
|
||||
<label class="control-label col-sm-2">
|
||||
Common Name
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input name="commonName" ng-model="authority.commonName" placeholder="Common Name" class="form-control"
|
||||
ng-maxlength="64" required/>
|
||||
<p ng-show="trackingForm.commonName.$invalid && !trackingForm.commonName.$pristine" class="help-block">
|
||||
You must enter a common name and it must be less than 64 characters in length</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-2">
|
||||
Type
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control" ng-model="authority.type"
|
||||
ng-options="option for option in ['root', 'subca']" ng-init="authority.type = 'root'"
|
||||
required></select>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-show="authority.type == 'subca'" class="form-group">
|
||||
<label class="control-label col-sm-2">
|
||||
Parent Authority
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<ui-select class="input-md" ng-model="authority.parent" theme="bootstrap" title="choose an authority">
|
||||
<ui-select-match placeholder="select an authority...">{{$select.selected.name}}</ui-select-match>
|
||||
<ui-select-choices class="form-control" repeat="authority in authorities"
|
||||
refresh="getAuthoritiesByName($select.search)"
|
||||
refresh-delay="300">
|
||||
<div ng-bind-html="authority.name | highlight: $select.search"></div>
|
||||
<small>
|
||||
<span ng-bind-html="''+authority.description | highlight: $select.search"></span>
|
||||
</small>
|
||||
</ui-select-choices>
|
||||
</ui-select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group"
|
||||
ng-class="{'has-error': trackingForm.validityEnd.$invalid || trackingForm.validityStart.$invalid, 'has-success': !trackingForm.$invalid&&trackingForm.validityEnd.$dirty&&trackingForm.validityStart.$dirty}">
|
||||
<label class="control-label col-sm-2">
|
||||
Validity Range
|
||||
</label>
|
||||
<div class="col-sm-2">
|
||||
<select ng-model="authority.validityYears" class="form-control">
|
||||
<option value="">-</option>
|
||||
<option value="7">7 years</option>
|
||||
<option value="14">14 years</option>
|
||||
<option value="20">20 years</option>
|
||||
</select>
|
||||
</div>
|
||||
<span style="padding-top: 15px" class="text-center col-sm-1">
|
||||
<strong>- or -</strong>
|
||||
</span>
|
||||
<div class="col-sm-3">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control"
|
||||
uib-datepicker-popup="yyyy/MM/dd"
|
||||
ng-model="authority.validityStart"
|
||||
is-open="popup1.opened"
|
||||
datepicker-options="dateOptions"
|
||||
close-text="Close"
|
||||
max-date="authority.parent.authorityCertificate.notAfter"
|
||||
min-date="authority.parent.authorityCertificate.notBefore"
|
||||
alt-input-formats="altInputFormats" />
|
||||
<span class="input-group-btn">
|
||||
<button type="button" class="btn btn-default" ng-click="open1()"><i class="glyphicon glyphicon-calendar"></i></button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span style="padding-top: 15px" class="text-center col-sm-1"><label><span
|
||||
class="glyphicon glyphicon-resize-horizontal"></span></label></span>
|
||||
<div class="col-sm-3">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control"
|
||||
uib-datepicker-popup="yyyy/MM/dd"
|
||||
ng-model="authority.validityEnd"
|
||||
is-open="popup2.opened"
|
||||
datepicker-options="dateOptions"
|
||||
close-text="Close"
|
||||
max-date="authority.parent.authorityCertificate.notAfter"
|
||||
min-date="authority.parent.authorityCertificate.notBefore"
|
||||
alt-input-formats="altInputFormats" />
|
||||
<span class="input-group-btn">
|
||||
<button type="button" class="btn btn-default" ng-click="open2()"><i class="glyphicon glyphicon-calendar"></i></button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-2">
|
||||
Roles
|
||||
</label>
|
||||
<div class="col-sm-10" ng-model="authority" role-select></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
19
lemur/static/app/angular/authorities/services.js
vendored
19
lemur/static/app/angular/authorities/services.js
vendored
@ -29,8 +29,12 @@ angular.module('lemur')
|
||||
this.extensions.subAltNames.names.splice(index, 1);
|
||||
},
|
||||
attachCustom: function () {
|
||||
if (this.extensions === undefined || this.extensions.custom === undefined) {
|
||||
this.extensions = {'custom': []};
|
||||
if (this.extensions === undefined) {
|
||||
this.extensions = {};
|
||||
}
|
||||
|
||||
if (this.extensions.custom === undefined) {
|
||||
this.extensions.custom = [];
|
||||
}
|
||||
|
||||
if (angular.isString(this.customOid) && angular.isString(this.customEncoding) && angular.isString(this.customValue)) {
|
||||
@ -80,6 +84,7 @@ angular.module('lemur')
|
||||
|
||||
AuthorityService.create = function (authority) {
|
||||
authority.attachSubAltName();
|
||||
authority.attachCustom();
|
||||
return AuthorityApi.post(authority);
|
||||
};
|
||||
|
||||
@ -89,11 +94,11 @@ angular.module('lemur')
|
||||
|
||||
AuthorityService.getDefaults = function (authority) {
|
||||
return DefaultService.get().then(function (defaults) {
|
||||
authority.caDN.country = defaults.country;
|
||||
authority.caDN.state = defaults.state;
|
||||
authority.caDN.location = defaults.location;
|
||||
authority.caDN.organization = defaults.organization;
|
||||
authority.caDN.organizationalUnit = defaults.organizationalUnit;
|
||||
authority.country = defaults.country;
|
||||
authority.state = defaults.state;
|
||||
authority.location = defaults.location;
|
||||
authority.organization = defaults.organization;
|
||||
authority.organizationalUnit = defaults.organizationalUnit;
|
||||
});
|
||||
};
|
||||
|
||||
|
183
lemur/static/app/angular/authorities/view/view.js
vendored
183
lemur/static/app/angular/authorities/view/view.js
vendored
@ -16,7 +16,162 @@ angular.module('lemur')
|
||||
});
|
||||
})
|
||||
|
||||
.controller('AuthoritiesViewController', function ($scope, $q, $modal, $stateParams, AuthorityApi, AuthorityService, ngTableParams, toaster) {
|
||||
.directive('authorityVisualization', function () {
|
||||
// constants
|
||||
var margin = {top: 20, right: 120, bottom: 20, left: 120},
|
||||
width = 960 - margin.right - margin.left,
|
||||
height = 400 - margin.top - margin.bottom;
|
||||
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
val: '=',
|
||||
grouped: '='
|
||||
},
|
||||
link: function (scope, element) {
|
||||
function update(source) {
|
||||
|
||||
// Compute the new tree layout.
|
||||
var nodes = tree.nodes(root).reverse(),
|
||||
links = tree.links(nodes);
|
||||
|
||||
// Normalize for fixed-depth.
|
||||
nodes.forEach(function(d) { d.y = d.depth * 180; });
|
||||
|
||||
// Update the nodes…
|
||||
var node = svg.selectAll('g.node')
|
||||
.data(nodes, function(d) { return d.id || (d.id = ++i); });
|
||||
|
||||
// Enter any new nodes at the parent's previous position.
|
||||
var nodeEnter = node.enter().append('g')
|
||||
.attr('class', 'node')
|
||||
.attr('transform', function() { return 'translate(' + source.y0 + ',' + source.x0 + ')'; })
|
||||
.on('click', click);
|
||||
|
||||
nodeEnter.append('circle')
|
||||
.attr('r', 1e-6)
|
||||
.style('fill', function(d) { return d._children ? 'lightsteelblue' : '#fff'; });
|
||||
|
||||
nodeEnter.append('text')
|
||||
.attr('x', function(d) { return d.children || d._children ? -10 : 10; })
|
||||
.attr('dy', '.35em')
|
||||
.attr('text-anchor', function(d) { return d.children || d._children ? 'end' : 'start'; })
|
||||
.text(function(d) { return d.name; })
|
||||
.style('fill-opacity', 1e-6);
|
||||
|
||||
// Transition nodes to their new position.
|
||||
var nodeUpdate = node.transition()
|
||||
.duration(duration)
|
||||
.attr('transform', function(d) { return 'translate(' + d.y + ',' + d.x + ')'; });
|
||||
|
||||
nodeUpdate.select('circle')
|
||||
.attr('r', 4.5)
|
||||
.style('fill', function(d) { return d._children ? 'lightsteelblue' : '#fff'; });
|
||||
|
||||
nodeUpdate.select('text')
|
||||
.style('fill-opacity', 1);
|
||||
|
||||
// Transition exiting nodes to the parent's new position.
|
||||
var nodeExit = node.exit().transition()
|
||||
.duration(duration)
|
||||
.attr('transform', function() { return 'translate(' + source.y + ',' + source.x + ')'; })
|
||||
.remove();
|
||||
|
||||
nodeExit.select('circle')
|
||||
.attr('r', 1e-6);
|
||||
|
||||
nodeExit.select('text')
|
||||
.style('fill-opacity', 1e-6);
|
||||
|
||||
// Update the links…
|
||||
var link = svg.selectAll('path.link')
|
||||
.data(links, function(d) { return d.target.id; });
|
||||
|
||||
// Enter any new links at the parent's previous position.
|
||||
link.enter().insert('path', 'g')
|
||||
.attr('class', 'link')
|
||||
.attr('d', function() {
|
||||
var o = {x: source.x0, y: source.y0};
|
||||
return diagonal({source: o, target: o});
|
||||
});
|
||||
|
||||
// Transition links to their new position.
|
||||
link.transition()
|
||||
.duration(duration)
|
||||
.attr('d', diagonal);
|
||||
|
||||
// Transition exiting nodes to the parent's new position.
|
||||
link.exit().transition()
|
||||
.duration(duration)
|
||||
.attr('d', function() {
|
||||
var o = {x: source.x, y: source.y};
|
||||
return diagonal({source: o, target: o});
|
||||
})
|
||||
.remove();
|
||||
|
||||
// Stash the old positions for transition.
|
||||
nodes.forEach(function(d) {
|
||||
d.x0 = d.x;
|
||||
d.y0 = d.y;
|
||||
});
|
||||
}
|
||||
|
||||
// Toggle children on click.
|
||||
function click(d) {
|
||||
if (d.children) {
|
||||
d._children = d.children;
|
||||
d.children = null;
|
||||
} else {
|
||||
d.children = d._children;
|
||||
d._children = null;
|
||||
}
|
||||
update(d);
|
||||
}
|
||||
|
||||
var i = 0,
|
||||
duration = 750,
|
||||
root;
|
||||
|
||||
var tree = d3.layout.tree()
|
||||
.size([height, width]);
|
||||
|
||||
var diagonal = d3.svg.diagonal()
|
||||
.projection(function(d) { return [d.y, d.x]; });
|
||||
|
||||
var svg = d3.select(element[0]).append('svg')
|
||||
.attr('width', width + margin.right + margin.left)
|
||||
.attr('height', height + margin.top + margin.bottom)
|
||||
.call(d3.behavior.zoom().on('zoom', function () {
|
||||
svg.attr('transform', 'translate(' + d3.event.translate + ')' + ' scale(' + d3.event.scale + ')');
|
||||
}))
|
||||
.append('g')
|
||||
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
|
||||
|
||||
scope.val.customGET('visualize').then(function (result) {
|
||||
root = result;
|
||||
root.x0 = height / 2;
|
||||
root.y0 = 0;
|
||||
|
||||
function collapse(d) {
|
||||
if (d.children) {
|
||||
d._children = d.children;
|
||||
d._children.forEach(collapse);
|
||||
d.children = null;
|
||||
}
|
||||
}
|
||||
|
||||
root.children.forEach(collapse);
|
||||
update(root);
|
||||
|
||||
});
|
||||
|
||||
d3.select(self.frameElement).style('height', '800px');
|
||||
|
||||
}
|
||||
};
|
||||
})
|
||||
|
||||
.controller('AuthoritiesViewController', function ($scope, $q, $uibModal, $stateParams, AuthorityApi, AuthorityService, MomentService, ngTableParams, toaster) {
|
||||
$scope.filter = $stateParams;
|
||||
$scope.authoritiesTable = new ngTableParams({
|
||||
page: 1, // show first page
|
||||
@ -29,15 +184,14 @@ angular.module('lemur')
|
||||
total: 0, // length of data
|
||||
getData: function ($defer, params) {
|
||||
AuthorityApi.getList(params.url()).then(function (data) {
|
||||
_.each(data, function(authority) {
|
||||
AuthorityService.getRoles(authority);
|
||||
});
|
||||
params.total(data.total);
|
||||
$defer.resolve(data);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$scope.momentService = MomentService;
|
||||
|
||||
$scope.updateActive = function (authority) {
|
||||
AuthorityService.updateActive(authority).then(
|
||||
function () {
|
||||
@ -63,16 +217,13 @@ angular.module('lemur')
|
||||
return def;
|
||||
};
|
||||
|
||||
$scope.toggleFilter = function (params) {
|
||||
params.settings().$scope.show_filter = !params.settings().$scope.show_filter;
|
||||
};
|
||||
|
||||
$scope.edit = function (authorityId) {
|
||||
var modalInstance = $modal.open({
|
||||
var uibModalInstance = $uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: '/angular/authorities/authority/edit.tpl.html',
|
||||
controller: 'AuthorityEditController',
|
||||
size: 'lg',
|
||||
backdrop: 'static',
|
||||
resolve: {
|
||||
editId: function () {
|
||||
return authorityId;
|
||||
@ -80,18 +231,19 @@ angular.module('lemur')
|
||||
}
|
||||
});
|
||||
|
||||
modalInstance.result.then(function () {
|
||||
uibModalInstance.result.then(function () {
|
||||
$scope.authoritiesTable.reload();
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
$scope.editRole = function (roleId) {
|
||||
var modalInstance = $modal.open({
|
||||
var uibModalInstance = $uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: '/angular/roles/role/role.tpl.html',
|
||||
controller: 'RolesEditController',
|
||||
size: 'lg',
|
||||
backdrop: 'static',
|
||||
resolve: {
|
||||
editId: function () {
|
||||
return roleId;
|
||||
@ -99,21 +251,22 @@ angular.module('lemur')
|
||||
}
|
||||
});
|
||||
|
||||
modalInstance.result.then(function () {
|
||||
uibModalInstance.result.then(function () {
|
||||
$scope.authoritiesTable.reload();
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
$scope.create = function () {
|
||||
var modalInstance = $modal.open({
|
||||
var uibModalInstance = $uibModal.open({
|
||||
animation: true,
|
||||
controller: 'AuthorityCreateController',
|
||||
templateUrl: '/angular/authorities/authority/authorityWizard.tpl.html',
|
||||
size: 'lg'
|
||||
size: 'lg',
|
||||
backdrop: 'static',
|
||||
});
|
||||
|
||||
modalInstance.result.then(function () {
|
||||
uibModalInstance.result.then(function () {
|
||||
$scope.authoritiesTable.reload();
|
||||
});
|
||||
|
||||
|
@ -1,51 +1,130 @@
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h2 class="featurette-heading">Authorities
|
||||
<span class="text-muted"><small>The nail that sticks out farthest gets hammered the hardest</small></span></h2>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<div class="btn-group pull-right">
|
||||
<button class="btn btn-primary" ng-click="create()">Create</button>
|
||||
<div class="col-md-12">
|
||||
<h2 class="featurette-heading">Authorities
|
||||
<span class="text-muted"><small>The nail that sticks out farthest gets hammered the hardest</small></span>
|
||||
</h2>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<div class="btn-group pull-right">
|
||||
<button class="btn btn-primary" ng-click="create()">Create</button>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button ng-model="showFilter" class="btn btn-default" uib-btn-checkbox
|
||||
btn-checkbox-true="1"
|
||||
btn-checkbox-false="0">Filter</button>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table ng-table="authoritiesTable" class="table table-striped" template-pagination="angular/pager.html"
|
||||
show-filter="showFilter">
|
||||
<tbody>
|
||||
<tr ng-repeat-start="authority in $data track by $index">
|
||||
<td data-title="'Name'" sortable="'name'" filter="{ 'name': 'text' }">
|
||||
<ul class="list-unstyled">
|
||||
<li>{{ authority.name }}</li>
|
||||
<li><span class="text-muted">{{ authority.owner }}</span></li>
|
||||
</ul>
|
||||
</td>
|
||||
<td data-title="'Active'" filter="{ 'active': 'select' }" filter-data="getAuthorityStatus()">
|
||||
<form>
|
||||
<switch ng-change="updateActive(authority)" id="status" name="status"
|
||||
ng-model="authority.active" class="green small"></switch>
|
||||
</form>
|
||||
</td>
|
||||
<td data-title="'Common Name'" filter="{ 'cn': 'text'}">
|
||||
{{ authority.authorityCertificate.cn }}
|
||||
</td>
|
||||
<td data-title="''">
|
||||
<div class="btn-group pull-right">
|
||||
<a class="btn btn-sm btn-default"
|
||||
ui-sref="authority({name: authority.name})">Permalink</a>
|
||||
<button ng-model="authority.toggle" class="btn btn-sm btn-info" uib-btn-checkbox
|
||||
btn-checkbox-true="1"
|
||||
btn-checkbox-false="0">More
|
||||
</button>
|
||||
<button uib-tooltip="Edit Authority" ng-click="edit(authority.id)"
|
||||
class="btn btn-sm btn-warning">
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="warning" ng-if="authority.toggle" ng-repeat-end>
|
||||
<td colspan="12">
|
||||
<uib-tabset justified="true" class="col-md-6">
|
||||
<uib-tab>
|
||||
<uib-tab-heading>Basic Info</uib-tab-heading>
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item">
|
||||
<strong>Creator</strong>
|
||||
<span class="pull-right">
|
||||
{{ authority.authorityCertificate.user.email }}
|
||||
</span>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<strong>Not Before</strong>
|
||||
<span class="pull-right" uib-tooltip="{{ authority.authorityCertificate.notBefore }}">
|
||||
{{ momentService.createMoment(authority.authorityCertificate.notBefore) }}
|
||||
</span>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<strong>Not After</strong>
|
||||
<span class="pull-right" uib-tooltip="{{ authority.authorityCertificate.notAfter }}">
|
||||
{{ momentService.createMoment(authority.authorityCertificate.notAfter) }}
|
||||
</span>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<strong>Description</strong>
|
||||
<p>{{ authority.description }}</p>
|
||||
</li>
|
||||
</ul>
|
||||
</uib-tab>
|
||||
<uib-tab>
|
||||
<uib-tab-heading>Roles</uib-tab-heading>
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item" ng-repeat="role in authority.roles">
|
||||
<strong>{{ role.name }}</strong>
|
||||
<span class="pull-right">{{ role.description }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</uib-tab>
|
||||
</uib-tabset>
|
||||
<uib-tabset justified="true" class="col-md-6">
|
||||
<uib-tab>
|
||||
<uib-tab-heading>
|
||||
Chain
|
||||
<button class="btn btn-xs btn-default clipboard-btn glyphicon glyphicon-copy"
|
||||
uib-tooltip="Copy chain to clipboard" tooltip-trigger="mouseenter"
|
||||
clipboard
|
||||
text="authority.authorityCertificate.chain"></button>
|
||||
</uib-tab-heading>
|
||||
<pre style="width: 100%">{{ authority.authorityCertificate.chain }}</pre>
|
||||
</uib-tab>
|
||||
<uib-tab>
|
||||
<uib-tab-heading>
|
||||
Public Certificate
|
||||
<button class="btn btn-xs btn-default clipboard-btn glyphicon glyphicon-copy"
|
||||
uib-tooltip="Copy authority to clipboard" tooltip-trigger="mouseenter"
|
||||
clipboard
|
||||
text="authority.authorityCertificate.body"></button>
|
||||
</uib-tab-heading>
|
||||
<pre style="width: 100%">{{ authority.authorityCertificate.body }}</pre>
|
||||
</uib-tab>
|
||||
<uib-tab>
|
||||
<uib-tab-heading>
|
||||
Visualization
|
||||
</uib-tab-heading>
|
||||
<pre style="width: 100%">
|
||||
<authority-visualization val="authority"></authority-visualization>
|
||||
</pre>
|
||||
</uib-tab>
|
||||
</uib-tabset>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button ng-click="toggleFilter(authoritiesTable)" class="btn btn-default">Filter</button>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table ng-table="authoritiesTable" class="table table-striped" template-pagination="angular/pager.html" show-filter="false">
|
||||
<tbody>
|
||||
<tr ng-repeat="authority in $data track by $index">
|
||||
<td data-title="'Name'" sortable="'name'" filter="{ 'name': 'text' }">
|
||||
<ul class="list-unstyled">
|
||||
<li>{{ authority.name }}</li>
|
||||
<li><span class="text-muted">{{ authority.description }}</span></li>
|
||||
</ul>
|
||||
</td>
|
||||
<td data-title="'Active'" filter="{ 'active': 'select' }" filter-data="getAuthorityStatus()">
|
||||
<form>
|
||||
<switch ng-change="updateActive(authority)" id="status" name="status" ng-model="authority.active" class="green small"></switch>
|
||||
</form>
|
||||
</td>
|
||||
<td data-title="'Roles'"> <!--filter="{ 'select': 'role' }" filter-data="roleService.getRoleDropDown()">-->
|
||||
<div class="btn-group">
|
||||
<a ng-click="editRole(role.id)" ng-repeat="role in authority.roles" class="btn btn-sm btn-danger">
|
||||
{{ role.name }}
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
<td data-title="''">
|
||||
<div class="btn-group pull-right">
|
||||
<a class="btn btn-sm btn-default" ui-sref="authority({name: authority.name})">Permalink</a>
|
||||
<button tooltip="Edit Authority" ng-click="edit(authority.id)" class="btn btn-sm btn-info">
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('lemur')
|
||||
.controller('CertificateExportController', function ($scope, $modalInstance, CertificateApi, CertificateService, PluginService, FileSaver, Blob, toaster, editId) {
|
||||
.controller('CertificateExportController', function ($scope, $uibModalInstance, CertificateApi, CertificateService, PluginService, FileSaver, Blob, toaster, editId) {
|
||||
CertificateApi.get(editId).then(function (certificate) {
|
||||
$scope.certificate = certificate;
|
||||
});
|
||||
@ -11,7 +11,7 @@ angular.module('lemur')
|
||||
});
|
||||
|
||||
$scope.cancel = function () {
|
||||
$modalInstance.dismiss('cancel');
|
||||
$uibModalInstance.dismiss('cancel');
|
||||
};
|
||||
|
||||
$scope.save = function (certificate) {
|
||||
@ -41,22 +41,21 @@ angular.module('lemur')
|
||||
toaster.pop({
|
||||
type: 'error',
|
||||
title: certificate.name,
|
||||
body: 'Failed to export ' + response.data.message,
|
||||
body: 'lemur-bad-request',
|
||||
bodyOutputType: 'directive',
|
||||
directiveData: response.data,
|
||||
timeout: 100000
|
||||
});
|
||||
});
|
||||
};
|
||||
})
|
||||
.controller('CertificateEditController', function ($scope, $modalInstance, CertificateApi, CertificateService, DestinationService, NotificationService, toaster, editId) {
|
||||
.controller('CertificateEditController', function ($scope, $uibModalInstance, CertificateApi, CertificateService, DestinationService, NotificationService, toaster, editId) {
|
||||
CertificateApi.get(editId).then(function (certificate) {
|
||||
CertificateService.getNotifications(certificate);
|
||||
CertificateService.getDestinations(certificate);
|
||||
CertificateService.getReplacements(certificate);
|
||||
$scope.certificate = certificate;
|
||||
});
|
||||
|
||||
$scope.cancel = function () {
|
||||
$modalInstance.dismiss('cancel');
|
||||
$uibModalInstance.dismiss('cancel');
|
||||
};
|
||||
|
||||
$scope.save = function (certificate) {
|
||||
@ -67,13 +66,15 @@ angular.module('lemur')
|
||||
title: certificate.name,
|
||||
body: 'Successfully updated!'
|
||||
});
|
||||
$modalInstance.close();
|
||||
$uibModalInstance.close();
|
||||
},
|
||||
function (response) {
|
||||
toaster.pop({
|
||||
type: 'error',
|
||||
title: certificate.name,
|
||||
body: 'Failed to update ' + response.data.message,
|
||||
body: 'lemur-bad-request',
|
||||
bodyOutputType: 'directive',
|
||||
directiveData: response.data,
|
||||
timeout: 100000
|
||||
});
|
||||
});
|
||||
@ -84,12 +85,49 @@ angular.module('lemur')
|
||||
$scope.notificationService = NotificationService;
|
||||
})
|
||||
|
||||
.controller('CertificateCreateController', function ($scope, $modalInstance, CertificateApi, CertificateService, DestinationService, AuthorityService, PluginService, MomentService, WizardHandler, LemurRestangular, NotificationService, toaster) {
|
||||
.controller('CertificateCreateController', function ($scope, $uibModalInstance, CertificateApi, CertificateService, DestinationService, AuthorityService, AuthorityApi, PluginService, MomentService, WizardHandler, LemurRestangular, NotificationService, toaster) {
|
||||
$scope.certificate = LemurRestangular.restangularizeElement(null, {}, 'certificates');
|
||||
|
||||
// set the defaults
|
||||
CertificateService.getDefaults($scope.certificate);
|
||||
|
||||
$scope.cancel = function () {
|
||||
$uibModalInstance.dismiss('cancel');
|
||||
};
|
||||
|
||||
$scope.getAuthoritiesByName = function (value) {
|
||||
return AuthorityService.findAuthorityByName(value).then(function (authorities) {
|
||||
$scope.authorities = authorities;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.dateOptions = {
|
||||
formatYear: 'yy',
|
||||
maxDate: new Date(2020, 5, 22),
|
||||
minDate: new Date(),
|
||||
startingDay: 1
|
||||
};
|
||||
|
||||
|
||||
$scope.open1 = function() {
|
||||
$scope.popup1.opened = true;
|
||||
};
|
||||
|
||||
$scope.open2 = function() {
|
||||
$scope.popup2.opened = true;
|
||||
};
|
||||
|
||||
$scope.formats = ['dd-MMMM-yyyy', 'yyyy/MM/dd', 'dd.MM.yyyy', 'shortDate'];
|
||||
$scope.format = $scope.formats[0];
|
||||
$scope.altInputFormats = ['M!/d!/yyyy'];
|
||||
|
||||
$scope.popup1 = {
|
||||
opened: false
|
||||
};
|
||||
|
||||
$scope.popup2 = {
|
||||
opened: false
|
||||
};
|
||||
|
||||
$scope.create = function (certificate) {
|
||||
WizardHandler.wizard().context.loading = true;
|
||||
CertificateService.create(certificate).then(
|
||||
@ -99,15 +137,18 @@ angular.module('lemur')
|
||||
title: certificate.name,
|
||||
body: 'Successfully created!'
|
||||
});
|
||||
$modalInstance.close();
|
||||
$uibModalInstance.close();
|
||||
},
|
||||
function (response) {
|
||||
toaster.pop({
|
||||
type: 'error',
|
||||
title: certificate.name,
|
||||
body: 'Was not created! ' + response.data.message,
|
||||
body: 'lemur-bad-request',
|
||||
bodyOutputType: 'directive',
|
||||
directiveData: response.data,
|
||||
timeout: 100000
|
||||
});
|
||||
|
||||
WizardHandler.wizard().context.loading = false;
|
||||
});
|
||||
};
|
||||
@ -152,20 +193,6 @@ angular.module('lemur')
|
||||
}
|
||||
];
|
||||
|
||||
$scope.openNotBefore = function($event) {
|
||||
$event.preventDefault();
|
||||
$event.stopPropagation();
|
||||
|
||||
$scope.openNotBefore.isOpen = true;
|
||||
};
|
||||
|
||||
$scope.openNotAfter = function($event) {
|
||||
$event.preventDefault();
|
||||
$event.stopPropagation();
|
||||
|
||||
$scope.openNotAfter.isOpen = true;
|
||||
|
||||
};
|
||||
|
||||
PluginService.getByType('destination').then(function (plugins) {
|
||||
$scope.plugins = plugins;
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user