Compare commits

...

82 Commits
0.2.2 ... 0.3.0

Author SHA1 Message Date
d95b1a0a41 release bump (#348) 2016-06-06 09:01:19 -07:00
d9cc4980e8 Fixing destination upload. (#347)
* Fixing an issue where uploaded certificates would have a name of 'None'

* Clarifying comment.

* Improving order.
2016-06-03 18:45:58 -07:00
5e987fa8b6 Adding additional data migrations. (#346) 2016-06-03 17:56:32 -07:00
42001be9ec Fixing the way filters were toggled. (#345) 2016-06-03 09:24:17 -07:00
dc198fec8c Docs (#344)
* Adding release info.

* adding some fields

* Adding Source Plugin change.

* Updating docs
2016-06-03 08:28:09 -07:00
acd47d5ec9 Fixing an issue were authorities were not related to their roles (#342) 2016-06-02 09:07:17 -07:00
72e3fb5bfe Fixing several small issues. (#341)
* Fixing several small issues.

* Fixing tests.
2016-06-01 11:18:00 -07:00
b2539b843b Fixing and error causing duplicate roles to be created. (#339)
* Fixing and error causing duplicate roles to be created.

* Fixing python3

* Fixing python2 and python3
2016-05-31 15:44:54 -07:00
be5dff8472 Adding a visualization for authorities. (#338)
* Adding a visualization for authorities.

* Fixing some lint.

* Fixing some lint.
2016-05-30 21:52:34 -07:00
76037e8b3a Fixing certificate names. (#337) 2016-05-27 12:00:10 -07:00
11f4bd503b Fixes (#332)
* Ensuring domains are returned correctly.

* Ensuring certificates receive owner role
2016-05-24 17:10:19 -07:00
6688b279e7 Fixing some bad renaming. (#331) 2016-05-24 10:43:40 -07:00
1ca38015bc Fixes (#329)
* Modifying the way roles are assigned.

* Adding migration scripts.

* Adding endpoints field for future use.

* Fixing dropdowns.
2016-05-23 18:38:04 -07:00
656269ff17 Closes #147 (#328)
* Closes #147

* Fixing tests

* Ensuring we can validate max dates.
2016-05-23 11:28:25 -07:00
bd727b825d Making roles more apparent for certificates and authorities. (#327) 2016-05-20 12:48:12 -07:00
e04c1e7dc9 Fixing a few things, adding tests. (#326) 2016-05-20 09:03:34 -07:00
615df76dd5 Closes 262 (#324)
Moves the authority -> role relationship from a 1 -> many to a many -> many. This will allow one role to control and have access to many authorities.
2016-05-19 13:37:05 -07:00
112c6252d6 Adding password reset command to the cli. (#325) 2016-05-19 10:07:15 -07:00
b13370bf0d Making dropdowns look a bit better. (#322)
* Making dropdowns look a bit better.

* Pleasing Lint.
2016-05-19 09:04:50 -07:00
88aa5d3fdb Making nested notifications less verbose (#321) 2016-05-19 08:48:55 -07:00
b187d8f836 Adding a better comparison. (#320) 2016-05-16 19:03:10 -07:00
1763a1a717 254 duplication certificate name (#319) 2016-05-16 15:59:40 -07:00
62b61ed980 Fixing various issues. (#318)
* Fixing various issues.

* Fixing tests
2016-05-16 11:09:50 -07:00
c11034b9bc Fixes various issues. (#317) 2016-05-16 09:23:48 -07:00
58e8fe0bd0 Fixes various issues. (#316) 2016-05-13 14:35:38 -07:00
a0c8765588 Various bug fixes. (#314) 2016-05-12 12:38:44 -07:00
9022059dc6 Marshmallowing roles (#313) 2016-05-10 14:22:22 -07:00
7f790be1e4 Marsmallowing users (#312) 2016-05-10 14:19:24 -07:00
93791c999d Marsmallowing destinations (#311) 2016-05-10 13:43:26 -07:00
5e9f1437ad Marsmallowing sources (#310) 2016-05-10 13:16:33 -07:00
f9655213b3 Marshmallowing notifications. (#308) 2016-05-10 11:27:57 -07:00
008d608ec4 Fixing error in notifications. (#307) 2016-05-09 17:35:18 -07:00
78c8d12ad8 Cleaning up the way authorities are selected and upgrading uib dependencies. 2016-05-09 17:17:00 -07:00
df0ad4d875 Authorities marshmallow addition (#303) 2016-05-09 11:00:16 -07:00
776e0fcd11 Slack plugin for notifications (#305) 2016-05-08 09:07:16 -07:00
6ec3bad49a Closes #278 (#298)
* Closes #278
2016-05-05 15:28:17 -07:00
52f44c3ea6 Closes #278 and #199, Starting transition to marshmallow (#299)
* Closes #278  and #199, Starting transition to marshmallow
2016-05-05 12:52:08 -07:00
941d36ebfe Merge pull request #302 from kevgliss/301-p12-no-chain
Closes #301
2016-05-04 17:07:42 -07:00
db8243b4b4 Closes #301 2016-05-04 16:56:05 -07:00
f919b7360e Merge pull request #294 from kevgliss/regex
Regex
2016-04-25 17:20:52 -07:00
8e1b7c0036 Removing validation because regex is hard 2016-04-25 16:13:33 -07:00
9b0e0fa9c2 removing validtion from openssl 2016-04-25 16:11:37 -07:00
565d7afa92 Merge pull request #293 from kevgliss/devdocs
Fixes #291
2016-04-25 12:30:54 -07:00
c914ba946f Merge pull request #292 from kevgliss/docs
Fixes #285 Renames sync_sources function to sync to align documentation.
2016-04-25 12:16:47 -07:00
6f9280f64a Adding gulp path 2016-04-25 12:16:33 -07:00
8fe460e401 Fixes #291 2016-04-25 11:34:05 -07:00
b9fe359d23 Fixes #285 Renames sync_sources function to sync to align documentation. 2016-04-25 11:21:25 -07:00
2c6d494c32 Merge pull request #290 from kevgliss/289-java-export-intermediates
Fixes #289 and #275
2016-04-21 16:46:11 -07:00
dbd1279226 Fixes #289 and #275 2016-04-21 16:22:19 -07:00
b463fcf61b Merge pull request #280 from kevgliss/SAN-hotfix
Fixes an issue where custom OIDs would clear out san extensions
2016-04-11 12:04:24 -07:00
82b4f5125d Fixes an issue where custom OIDs would clear out san extensions 2016-04-11 11:17:18 -07:00
3f89d6d009 Merge pull request #271 from kevgliss/195
Closes #195
2016-04-08 12:01:10 -07:00
676f843c92 Merge pull request #276 from kevgliss/san-hotfix
Fixes an issue where custom OIDs would clear out san extensions
2016-04-07 10:30:12 -07:00
c2387dc120 Fixes an issue where custom OIDs would clear out san extensions 2016-04-07 10:29:08 -07:00
9a8e1534c0 Merge pull request #274 from kevgliss/metric_fix
Fixing an issue were metrics would not be sent
2016-04-05 10:50:46 -07:00
dbc4964e94 Fixing an issue were metrics would not be sent 2016-04-05 10:23:33 -07:00
00b263f345 Merge pull request #273 from kevgliss/216
Closes #216
2016-04-01 16:59:49 -07:00
62d03b0d41 Closes #216 2016-04-01 16:54:33 -07:00
b5a4b293a9 Merge pull request #270 from kevgliss/248
Closes #248
2016-04-01 14:28:52 -07:00
bfcfdb83a7 Closes #195 2016-04-01 14:27:57 -07:00
4ccbfa8164 Closes #248 2016-04-01 13:29:08 -07:00
675d10c8a6 Merge pull request #269 from kevgliss/263
Closes #263
2016-04-01 13:08:13 -07:00
2cde7336dc Closes #263 2016-04-01 13:01:56 -07:00
169490dbec Merge pull request #268 from kevgliss/252
Closes #252
2016-04-01 10:16:10 -07:00
3ceb297276 Merge pull request #267 from kevgliss/261
Closes #261
2016-04-01 10:12:10 -07:00
12633bfed6 Merge pull request #266 from kevgliss/tox
removing testing support for py33
2016-04-01 10:11:59 -07:00
5958bac2a2 Merge pull request #265 from kevgliss/257
Closes #257
2016-04-01 10:11:32 -07:00
37f2d5b8b0 Closes #252 2016-04-01 10:09:28 -07:00
47891d2953 Closes #261 2016-04-01 09:58:19 -07:00
af68571f4e removing testing support for py33 2016-04-01 09:52:19 -07:00
d0ec925ca3 Merge pull request #264 from kevgliss/246
Closes #246
2016-04-01 09:51:10 -07:00
939194158a Closes #257 2016-04-01 09:49:44 -07:00
576265e09c Closes #246 2016-04-01 09:19:36 -07:00
dfaf45344c Merge pull request #250 from lfaraone/patch-1
Remove duplicate `install` in Quickstart
2016-03-01 09:21:04 -08:00
6c378957e9 Remove duplicate install in Quickstart 2016-03-01 04:12:10 +00:00
e8f9bc80a0 Merge pull request #249 from kevgliss/master
Updating docs
2016-02-29 12:51:47 -08:00
a30b8b21e4 updating postgres login 2016-02-29 08:53:35 -08:00
12204852aa changeing the default port to 8000 2016-02-29 08:48:27 -08:00
edba980b56 Merge pull request #245 from mikegrima/issue243
Removed deprecated auth api endpoint.
2016-02-16 17:11:41 -08:00
ba666ddbfa Removed deprecated auth api endpoint. 2016-02-16 15:04:53 -08:00
35f9f59c57 Merge pull request #242 from kevgliss/version_bump
version bump
2016-02-05 13:13:01 -08:00
ac1f493338 version bump 2016-02-05 13:12:21 -08:00
148 changed files with 6584 additions and 4063 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
/.cache
.coverage
.tox
.DS_Store

View File

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

View File

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

View File

@ -1,21 +1,77 @@
Changelog
=========
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
~~~~~~~~~~~~~~~~~~
* Closed [#234](https://github.com/Netflix/lemur/issues/234) - Allows export plugins to define whether they need
* 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
* 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
* 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
* 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
* Closed `#218 <https://github.com/Netflix/lemur/issues/234>`_ - Fixed an issue where export passphrases would not validate
0.2.1 - 2015-12-14
@ -30,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
@ -46,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.

View File

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

View File

@ -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": [
"**/.*",

View File

@ -273,7 +273,7 @@ For more information about how to use social logins, see: `Satellizer <https://g
::
GOOGLE_CLIENT_ID = "client-id"
GOOGLE_CLIENT_ID = "client-id"
.. data:: GOOGLE_SECRET
:noindex:
@ -588,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

View File

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

View File

@ -211,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.
@ -223,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
@ -244,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

View File

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

View File

@ -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
------------------
@ -161,6 +152,7 @@ Additional notifications can be created through the UI or API. See :ref:`Creati
$ 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.
@ -178,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;
@ -251,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

View File

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

View File

@ -9,7 +9,7 @@ __title__ = "lemur"
__summary__ = ("Certificate management and orchestration service")
__uri__ = "https://github.com/Netflix/lemur"
__version__ = "0.2.2"
__version__ = "0.3.0"
__author__ = "The Lemur developers"
__email__ = "security@netflix.com"

View File

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

View File

@ -18,6 +18,9 @@ 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):
@ -36,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')

View File

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

View File

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

View File

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

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

View File

@ -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):
@ -31,8 +27,9 @@ def update(authority_id, description=None, owner=None, active=None, roles=None):
:return:
"""
authority = get(authority_id)
if roles:
authority = database.update_list(authority, 'roles', Role, roles)
authority.roles = roles
if active:
authority.active = active
@ -42,45 +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
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':
@ -88,25 +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)
# the owning dl or role should have this authority associated with it
owner_role = role_service.get_by_name(kwargs['ownerEmail'])
owner_role.authority = authority
g.current_user.authorities.append(authority)
metrics.send('authority_created', 'counter', 1, metric_tags=dict(owner=authority.owner))
return authority
@ -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)

View File

@ -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,20 +53,44 @@ 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
}
@ -87,7 +98,7 @@ class AuthoritiesList(AuthenticatedResource):
: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 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')

View File

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

View 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()

View File

@ -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,69 +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'))
if kwargs.get('replacements'):
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
@ -243,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
@ -346,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
@ -362,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']),
@ -376,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(
@ -435,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()
@ -445,7 +391,7 @@ def create_csr(csr_config):
encoding=serialization.Encoding.PEM
)
return csr, pem
return csr, private_key
def stats(**kwargs):
@ -476,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]

View File

@ -9,129 +9,24 @@ import base64
from builtins import str
from flask import Blueprint, make_response, jsonify
from flask.ext.restful import reqparse, Api, fields
from flask.ext.restful import reqparse, Api
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from lemur.plugins import plugins
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
from lemur.auth.permissions import AuthorityPermission
from lemur.auth.permissions import UpdateCertificatePermission
from lemur.auth.permissions import SensitiveDomainPermission
from lemur.auth.permissions import ViewKeyPermission, AuthorityPermission, CertificatePermission
from lemur.certificates import service
from lemur.authorities.models import Authority
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.domains import service as domain_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 get_domains_from_options(options):
"""
Retrive all domains from certificate options
:param options:
:return:
"""
domains = [options['commonName']]
if options.get('extensions'):
if options['extensions'].get('subAltNames'):
for k, v in options['extensions']['subAltNames']['names']:
if k == 'DNSName':
domains.append(v)
return domains
def check_sensitive_domains(domains):
"""
Determines if any certificates in the given certificate
are marked as sensitive
:param domains:
:return:
"""
for domain in domains:
domain_objs = domain_service.get_by_name(domain)
for d in domain_objs:
if d.sensitive:
raise ValueError("The domain {0} has been marked as sensitive. Contact an administrator to "
"issue this certificate".format(d.name))
def pem_str(value, name):
"""
Used to validate that the given string is a PEM formatted string
: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 """
@ -140,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
@ -164,26 +59,53 @@ 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
}
@ -191,10 +113,11 @@ class CertificatesList(AuthenticatedResource):
: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 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')
@ -208,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
@ -223,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**:
@ -318,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
@ -346,47 +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)
authority_permission = AuthorityPermission(authority.id, roles)
authority_permission = AuthorityPermission(data['authority'].id, roles)
if authority_permission.can():
# if we are not admins lets make sure we aren't issuing anything sensitive
if not SensitiveDomainPermission().can():
check_sensitive_domains(get_domains_from_options(args))
return service.create(**args)
return 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):
@ -396,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
@ -431,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
@ -458,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):
@ -553,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
@ -577,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
@ -634,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
@ -690,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
@ -714,27 +629,53 @@ 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
}
@ -742,10 +683,11 @@ class NotificationCertificatesList(AuthenticatedResource):
: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 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')
@ -766,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
@ -789,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
@ -821,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
@ -885,23 +860,22 @@ 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])
options = data['plugin']['plugin_options']
plugin = data['plugin']['plugin_object']
plugin = plugins.get(args['export']['plugin']['slug'])
if plugin.requires_key:
if permission.can():
extension, passphrase, data = plugin.export(cert.body, cert.chain, cert.private_key, args['export']['plugin']['pluginOptions'])
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, args['export']['plugin']['pluginOptions'])
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))

175
lemur/common/defaults.py Normal file
View 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
View 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
View 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))

View File

@ -239,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)
@ -287,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)

View 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()

View File

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

View File

@ -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,24 +52,32 @@ 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
}
@ -83,7 +85,7 @@ class DestinationsList(AuthenticatedResource):
: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 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,24 +362,32 @@ 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
}
@ -343,7 +395,7 @@ class CertificateDestinations(AuthenticatedResource):
: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 count: count number default is 10
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
"""

35
lemur/domains/schemas.py Normal file
View 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)

View File

@ -77,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)
@ -92,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)

View File

@ -8,19 +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,
'sensitive': fields.Boolean
}
from lemur.domains.schemas import domain_input_schema, domain_output_schema, domains_output_schema
mod = Blueprint('domains', __name__)
api = Api(mod)
@ -31,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
@ -74,7 +71,7 @@ class DomainsList(AuthenticatedResource):
: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 count: count number. default is 10
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
:statuscode 403: unauthenticated
@ -83,8 +80,8 @@ class DomainsList(AuthenticatedResource):
args = parser.parse_args()
return service.render(args)
@marshal_items(FIELDS)
def post(self):
@validate_schema(domain_input_schema, domain_output_schema)
def post(self, data=None):
"""
.. http:post:: /domains
@ -121,15 +118,12 @@ class DomainsList(AuthenticatedResource):
: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 count: count number default is 10
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
:statuscode 403: unauthenticated
"""
self.reqparse.add_argument('name', type=str, location='json')
self.reqparse.add_argument('sensitive', type=bool, default=False, location='json')
args = self.reqparse.parse_args()
return service.create(args['name'], args['sensitive'])
return service.create(data['name'], data['sensitive'])
class Domains(AuthenticatedResource):
@ -137,7 +131,7 @@ class Domains(AuthenticatedResource):
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
@ -172,8 +166,8 @@ class Domains(AuthenticatedResource):
"""
return service.get(domain_id)
@marshal_items(FIELDS)
def put(self, domain_id):
@validate_schema(domain_input_schema, domain_output_schema)
def put(self, domain_id, data=None):
"""
.. http:get:: /domains/1
@ -210,12 +204,8 @@ class Domains(AuthenticatedResource):
:statuscode 200: no error
:statuscode 403: unauthenticated
"""
self.reqparse.add_argument('name', type=str, location='json')
self.reqparse.add_argument('sensitive', type=bool, default=False, location='json')
args = self.reqparse.parse_args()
if SensitiveDomainPermission().can():
return service.update(domain_id, args['name'], args['sensitive'])
return service.update(domain_id, data['name'], data['sensitive'])
return dict(message='You are not authorized to modify this domain'), 403
@ -225,7 +215,7 @@ class CertificateDomains(AuthenticatedResource):
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
@ -268,7 +258,7 @@ class CertificateDomains(AuthenticatedResource):
: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 count: count number default is 10
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
:statuscode 403: unauthenticated

View File

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

View File

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

View File

@ -25,12 +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.models import get_name_from_arn
from lemur.certificates.service import get_name_from_arn
from lemur.certificates.verify import verify_string
from lemur.plugins.lemur_aws import elb
from lemur.sources.service import sync
from lemur.sources.service import sync as source_sync
from lemur import create_app
@ -189,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
@ -218,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(
@ -317,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),
@ -333,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):
@ -391,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
@ -841,6 +869,7 @@ 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())

32
lemur/metrics.py Normal file
View 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)

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

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

View File

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

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

View File

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

View File

@ -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,
@ -135,7 +98,7 @@ class NotificationsList(AuthenticatedResource):
: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 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,
@ -472,15 +422,11 @@ class CertificateNotifications(AuthenticatedResource):
: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 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')

View File

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

View 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

View File

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

View File

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

View File

@ -0,0 +1,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
)
)

View File

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

View File

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

View File

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

View 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,39 +87,36 @@ 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 JavaTruststoreExportPlugin(ExportPlugin):
@ -165,7 +164,7 @@ class JavaTruststoreExportPlugin(ExportPlugin):
if self.get_option('passphrase', options):
passphrase = self.get_option('passphrase', options)
else:
passphrase = get_psuedo_random_string()
passphrase = Fernet.generate_key()
with mktemppath() as jks_tmp:
create_truststore(body, chain, jks_tmp, alias, passphrase)
@ -215,7 +214,7 @@ class JavaKeystoreExportPlugin(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)
@ -226,8 +225,7 @@ class JavaKeystoreExportPlugin(ExportPlugin):
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)
create_keystore(body, chain, jks_tmp, key, alias, passphrase)
with open(jks_tmp, 'rb') as f:
raw = f.read()

View File

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

View File

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

View File

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

View File

@ -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'] = 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'

View File

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

View File

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

View File

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

View File

@ -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
@ -77,7 +73,7 @@ class RolesList(AuthenticatedResource):
: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 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
@ -369,7 +354,7 @@ class UserRolesList(AuthenticatedResource):
: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 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
@ -428,7 +413,7 @@ class AuthorityRolesList(AuthenticatedResource):
: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 count: count number default is 10
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
"""

212
lemur/schemas.py Normal file
View 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
View 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()

View File

@ -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)
@ -81,7 +81,7 @@ def sync(labels=None):
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)

View File

@ -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,
@ -85,7 +78,7 @@ class SourcesList(AuthenticatedResource):
: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 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,
@ -351,7 +334,7 @@ class CertificateSources(AuthenticatedResource):
: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 count: count number default is 10
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
"""

View File

@ -17,7 +17,9 @@
'satellizer',
'ngLetterAvatar',
'angular-clipboard',
'ngFileSaver'
'ngFileSaver',
'ngSanitize',
'ui.select'
]);
@ -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()) {

View File

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

View File

@ -1,4 +1,5 @@
<div class="modal-header">
<button type="button" class="close" ng-click="cancel()" aria-label="Close"><span aria-hidden="true">&times;</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">

View File

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

View File

@ -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">&times;</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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
<div class="modal-header">
<h3 class="modal-title"><span ng-show="!certificate.id">Create</span><span ng-show="certificate.id">Edit</span> Certificate <span class="text-muted"><small>encrypt all the things</small></h3>
<button type="button" class="close" ng-click="cancel()" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h3 class="modal-title"><span ng-show="!certificate.id">Create</span><span ng-show="certificate.id">Edit</span> Certificate <span class="text-muted"><small>encrypt all the things</small></span></h3>
</div>
<div class="modal-body">
<div>

View File

@ -5,12 +5,13 @@
<div class="col-sm-10">
<div class="input-group">
<input type="text" ng-model="certificate.selectedDestination" placeholder="AWS..."
typeahead="destination.label for destination in destinationService.findDestinationsByName($viewValue)" typeahead-loading="loadingDestinations"
class="form-control input-md" typeahead-on-select="certificate.attachDestination($item)" typeahead-min-wait="50"
tooltip="Lemur can upload certificates to any pre-defined destination"
tooltip-trigger="focus" tooltip-placement="top">
uib-typeahead="destination.label for destination in destinationService.findDestinationsByName($viewValue)" typeahead-loading="loadingDestinations"
class="form-control input-md" typeahead-on-select="certificate.attachDestination($item)"
uib-tooltip="Lemur can upload certificates to any pre-defined destination"
uib-tooltip-trigger="focus" uib-tooltip-placement="top"
typeahead-wait-ms="500">
<span class="input-group-btn">
<button ng-model="destinations.show" class="btn btn-md btn-default" btn-checkbox btn-checkbox-true="1" btn-checkbox-false="0">
<button ng-model="destinations.show" class="btn btn-md btn-default" uib-btn-checkbox btn-checkbox-true="1" btn-checkbox-false="0">
<span class="badge">{{ certificate.destinations.length || 0 }}</span>
</button>
</span>

View File

@ -1,39 +1,44 @@
<div class="modal-header">
<div class="modal-title">
<h3 class="modal-header">Edit <span class="text-muted"><small>{{ certificate.name }}</small></span></h3>
</div>
<div class="modal-body">
<form name="editForm" 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="certificate.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="certificate.description" placeholder="Something elegant" class="form-control" required></textarea>
<p ng-show="editForm.description.$invalid && !editForm.description.$pristine" class="help-block">You must give a short description about this authority will be used for, this description should only include alphanumeric characters</p>
</div>
</div>
<div ng-include="'angular/certificates/certificate/replaces.tpl.html'"></div>
<div ng-include="'angular/certificates/certificate/notifications.tpl.html'"></div>
<div ng-include="'angular/certificates/certificate/destinations.tpl.html'"></div>
</form>
</div>
<div class="modal-footer">
<button type="submit" ng-click="save(certificate)" ng-disabled="editForm.$invalid" class="btn btn-success">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">&times;</span></button>
<h3>Edit <span class="text-muted"><small>{{ certificate.name }}</small></span></h3>
</div>
<div class="modal-body">
<form name="editForm" 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="certificate.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="certificate.description" placeholder="Something elegant" class="form-control" required></textarea>
<p ng-show="editForm.description.$invalid && !editForm.description.$pristine" class="help-block">You must give a short description about this authority will be used for, this description should only include alphanumeric characters</p>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2">
Roles
</label>
<div class="col-sm-10" ng-model="certificate" role-select></div>
</div>
<div ng-include="'angular/certificates/certificate/replaces.tpl.html'"></div>
<div ng-include="'angular/certificates/certificate/notifications.tpl.html'"></div>
<div ng-include="'angular/certificates/certificate/destinations.tpl.html'"></div>
</form>
</div>
<div class="modal-footer">
<button type="submit" ng-click="save(certificate)" ng-disabled="editForm.$invalid" class="btn btn-success">Save</button>
<button ng-click="cancel()" class="btn btn-danger">Cancel</button>
</div>

View File

@ -1,5 +1,6 @@
<div class="modal-header">
<div class="modal-title">
<button type="button" class="close" ng-click="cancel()" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h3 class="modal-header">Export <span class="text-muted"><small>{{ certificate.name }}</small></span></h3>
</div>
<div class="modal-body">
@ -9,10 +10,10 @@
Plugin
</label>
<div class="col-sm-10">
<select class="form-control" ng-model="certificate.export.plugin" ng-options="plugin.title for plugin in plugins" required></select>
<select class="form-control" ng-model="certificate.plugin" ng-options="plugin.title for plugin in plugins" required></select>
</div>
</div>
<div class="form-group" ng-repeat="item in certificate.export.plugin.pluginOptions">
<div class="form-group" ng-repeat="item in certificate.plugin.pluginOptions">
<ng-form name="subForm" class="form-horizontal" role="form" novalidate>
<div ng-class="{'has-error': subForm.sub.$invalid, 'has-success': !subForm.sub.$invalid&&subForm.sub.$dirty}">
<label class="control-label col-sm-2">

View File

@ -5,12 +5,12 @@
<div class="col-sm-10">
<div class="input-group">
<input type="text" ng-model="certificate.selectedNotification" placeholder="Email"
typeahead="notification.label for notification in notificationService.findNotificationsByName($viewValue)" typeahead-loading="loadingDestinations"
class="form-control input-md" typeahead-on-select="certificate.attachNotification($item)" typeahead-min-wait="50"
tooltip="By default Lemur will always notify you about this certificate through Email notifications."
tooltip-trigger="focus" tooltip-placement="top">
uib-typeahead="notification.label for notification in notificationService.findNotificationsByName($viewValue)" typeahead-loading="loadingDestinations"
class="form-control input-md" typeahead-on-select="certificate.attachNotification($item)"
uib-tooltip="By default Lemur will always notify you about this certificate through Email notifications."
uib-tooltip-trigger="focus" tooltip-placement="top" typeahead-wait-ms="500">
<span class="input-group-btn">
<button ng-model="notifications.show" class="btn btn-md btn-default" btn-checkbox btn-checkbox-true="1" btn-checkbox-false="0">
<button ng-model="notifications.show" class="btn btn-md btn-default" uib-btn-checkbox btn-checkbox-true="1" btn-checkbox-false="0">
<span class="badge">{{ certificate.notifications.length || 0 }}</span>
</button>
</span>

View File

@ -10,7 +10,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="certificate.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="certificate.subAltValue" placeholder="Value" class="form-control" required/>
<span class="input-group-btn">
<button ng-click="certificate.attachSubAltName()" class="btn btn-info">Add</button>
</span>
@ -139,12 +139,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="certificate.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="certificate.extensions.authorityIdentifier.useAuthorityCert">Authority Certificate
</label>
</div>
@ -156,7 +156,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="certificate.extensions.certificateInfoAccess.includeAIA">Include AIA
</label>
</div>
@ -168,7 +168,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="certificate.extensions.subjectKeyIdentifier.includeSKI">Include SKI
</label>
</div>
@ -187,14 +187,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="certificate.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="certificate.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="certificate.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="certificate.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="certificate.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="certificate.customValue" placeholder="Value" class="form-control" required/>
<span class="input-group-btn">
<button ng-click="certificate.attachCustom()" class="btn btn-info">Add</button>
</span>

View File

@ -5,12 +5,12 @@
<div class="col-sm-10">
<div class="input-group">
<input type="text" ng-model="certificate.selectedReplacement" placeholder="Certificate123..."
typeahead="certificate.name for certificate in certificateService.findCertificatesByName($viewValue)" typeahead-loading="loadingCertificates"
class="form-control input-md" typeahead-on-select="certificate.attachReplacement($item)" typeahead-min-wait="100"
tooltip="Lemur will mark any certificates being replaced as 'inactive'"
tooltip-trigger="focus" tooltip-placement="top">
uib-typeahead="certificate.name for certificate in certificateService.findCertificatesByName($viewValue)" typeahead-loading="loadingCertificates"
class="form-control input-md" typeahead-on-select="certificate.attachReplacement($item)"
uib-tooltip="Lemur will mark any certificates being replaced as 'inactive'"
uib-tooltip-trigger="focus" uib-tooltip-placement="top" typeahead-wait-ms="500">
<span class="input-group-btn">
<button ng-model="replacements.show" class="btn btn-md btn-default" btn-checkbox btn-checkbox-true="1" btn-checkbox-false="0">
<button ng-model="replacements.show" class="btn btn-md btn-default" uib-btn-checkbox btn-checkbox-true="1" btn-checkbox-false="0">
<span class="badge">{{ certificate.replacements.length || 0 }}</span>
</button>
</span>

View File

@ -1,132 +1,170 @@
<form name="trackingForm" novalidate>
<div class="form-horizontal">
<div class="form-group"
ng-class="{'has-error': trackingForm.ownerEmail.$invalid, 'has-success': !trackingForm.$invalid&&trackingForm.ownerEmail.$dirty}">
<label class="control-label col-sm-2">
Owner
</label>
<div class="form-horizontal">
<div class="form-group"
ng-class="{'has-error': trackingForm.ownerEmail.$invalid, 'has-success': !trackingForm.$invalid&&trackingForm.ownerEmail.$dirty}">
<label class="control-label col-sm-2">
Owner
</label>
<div class="col-sm-10">
<input type="email" name="ownerEmail" ng-model="certificate.owner" placeholder="TeamDL@example.com"
tooltip="This is the certificates team distribution list or main point of contact" class="form-control"
required/>
<div class="col-sm-10">
<input type="email" name="ownerEmail" ng-model="certificate.owner" placeholder="TeamDL@example.com"
uib-tooltip="This is the certificates team distribution list or main point of contact"
class="form-control"
required/>
<p ng-show="trackingForm.ownerEmail.$invalid && !trackingForm.ownerEmail.$pristine" class="help-block">You must
enter an Certificate owner</p>
</div>
</div>
<div class="form-group"
ng-class="{'has-error': trackingForm.description.$invalid, 'has-success': !trackingForm.$invalid&&trackingForm.description.$dirty}">
<label class="control-label col-sm-2">
Description
</label>
<p ng-show="trackingForm.ownerEmail.$invalid && !trackingForm.ownerEmail.$pristine" class="help-block">
You must enter an Certificate owner</p>
</div>
</div>
<div class="form-group"
ng-class="{'has-error': trackingForm.name.$invalid, 'has-success': !trackingForm.name.$invalid&&trackingForm.name.$dirty}">
<label class="control-label col-sm-2" uib-tooltip="If no name is provided, Lemur will generate a name for you">
Custom Name <span class="glyphicon glyphicon-question-sign"></span>
</label>
<div class="col-sm-10">
<input name="name" ng-model="certificate.name"
placeholder="the.example.net-SymantecCorporation-20150828-20160830" class="form-control"/>
</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">
<div class="col-sm-10">
<textarea name="description" ng-model="certificate.description" placeholder="Something elegant"
class="form-control" required></textarea>
<p ng-show="trackingForm.description.$invalid && !trackingForm.description.$pristine" class="help-block">You
must give a short description about this authority will be used for.</p>
</div>
</div>
<div class="form-group"
ng-class="{'has-error': trackingForm.selectedAuthority.$invalid, 'has-success': !trackingForm.$invalid&&trackingForm.selectedAuthority.$dirty}">
<label class="control-label col-sm-2">
Certificate Authority
</label>
<div class="col-sm-10">
<div class="input-group col-sm-12">
<input name="selectedAuthority"
tooltip="If you are unsure which authority you need; you most likely want to use 'verisign'"
type="text" ng-model="certificate.selectedAuthority" placeholder="Authority Name"
typeahead-on-select="certificate.attachAuthority($item)"
typeahead="authority.name for authority in authorityService.findActiveAuthorityByName($viewValue)"
typeahead-loading="loadingAuthorities"
class="form-control" typeahead-wait-ms="1000"
typeahead-template-url="angular/authorities/authority/select.tpl.html" required>
<p ng-show="trackingForm.description.$invalid && !trackingForm.description.$pristine"
class="help-block">You
must give a short description about this certificate will be used for.</p>
</div>
</div>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2">
Certificate Template
</label>
<div class="col-sm-10">
<select class="form-control" ng-change="certificate.useTemplate()" name="certificateTemplate"
ng-model="certificate.template" ng-options="template.name for template in templates"></select>
</div>
</div>
<div class="form-group"
ng-class="{'has-error': trackingForm.commonName.$invalid, 'has-success': !trackingForm.$invalid&&trackingForm.commonName.$dirty}">
<label class="control-label col-sm-2">
Common Name
</label>
<div class="col-sm-10">
<input name="commonName"
tooltip="If you need a certificate with multiple domains enter your primary domain here and the rest under 'Subject Alternate Names' in the next few panels"
ng-model="certificate.commonName" placeholder="Common Name" class="form-control" ng-maxlength="64"
required/>
<p ng-show="trackingForm.commonName.$invalid && !trackingForm.commonName.$pristine" class="help-block">You must
enter a common name and it must be less than 64 characters</p>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2"
tooltip="If no date is selected Lemur attempts to issue a 2 year certificate">
Validity Range <span class="glyphicon glyphicon-question-sign"></span>
</label>
<div class="col-sm-4">
<div>
<div class="input-group">
<input tooltip="Starting Date (yyyy/MM/dd)" class="form-control" datepicker-popup="yyyy/MM/dd"
is-open="$parent.openNotBefore.isOpen" min-date="certificate.authority.notBefore"
max-date="certificate.authority.maxDate" ng-model="certificate.validityStart"/>
<span class="input-group-btn">
<button class="btn btn-default" ng-click="openNotBefore($event)"><i
class="glyphicon glyphicon-calendar"></i></button>
</span>
</div>
<div class="form-group"
ng-class="{'has-error': trackingForm.selectedAuthority.$invalid, 'has-success': !trackingForm.$invalid&&trackingForm.selectedAuthority.$dirty}">
<label class="control-label col-sm-2">
Certificate Authority
</label>
<div class="col-sm-10">
<ui-select class="input-md" ng-model="certificate.authority" 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>
<span style="padding-top: 15px" class="text-center col-sm-2"><label><span
class="glyphicon glyphicon-resize-horizontal"></span></label></span>
<div class="form-group">
<label class="control-label col-sm-2">
Certificate Template
</label>
<div class="col-sm-4">
<div>
<div class="input-group">
<input tooltip="Ending Date (yyyy/MM/dd)" class="form-control" datepicker-popup="yyyy/MM/dd"
is-open="$parent.openNotAfter.isOpen" min-date="certificate.authority.notBefore"
max-date="certificate.authority.maxDate" ng-model="certificate.validityEnd"/>
<span class="input-group-btn">
<button class="btn btn-default" ng-click="openNotAfter($event)"><i
class="glyphicon glyphicon-calendar"></i></button>
<div class="col-sm-10">
<select class="form-control" ng-change="certificate.useTemplate()" name="certificateTemplate"
ng-model="certificate.template" ng-options="template.name for template in templates"></select>
</div>
</div>
<div class="form-group"
ng-class="{'has-error': trackingForm.commonName.$invalid, 'has-success': !trackingForm.$invalid&&trackingForm.commonName.$dirty}">
<label class="control-label col-sm-2">
Common Name
</label>
<div class="col-sm-10">
<input name="commonName"
uib-tooltip="If you need a certificate with multiple domains enter your primary domain here and the rest under 'Subject Alternate Names' in the next few panels"
ng-model="certificate.commonName" placeholder="Common Name" class="form-control"
ng-maxlength="64"
required/>
<p ng-show="trackingForm.commonName.$invalid && !trackingForm.commonName.$pristine" class="help-block">
You must
enter a common name and it must be less than 64 characters</p>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2"
uib-tooltip="If no date is selected Lemur attempts to issue a 2 year certificate">
Validity Range <span class="glyphicon glyphicon-question-sign"></span>
</label>
<div class="col-sm-2">
<select ng-model="certificate.validityYears" class="form-control">
<option value="">-</option>
<option value="1">1 year</option>
<option value="2">2 years</option>
<option value="3">3 years</option>
<option value="4">4 years</option>
</select>
</div>
<span style="padding-top: 15px" class="text-center col-sm-1">
<strong>- or -</strong>
</span>
</div>
<div class="col-sm-3">
<div class="input-group">
<input type="text" class="form-control"
uib-datepicker-popup="yyyy/MM/dd"
ng-model="certificate.validityStart"
is-open="popup1.opened"
datepicker-options="dateOptions"
close-text="Close"
max-date="certificate.authority.authorityCertificate.notAfter"
min-date="certificate.authority.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="certificate.validityEnd"
is-open="popup2.opened"
datepicker-options="dateOptions"
close-text="Close"
max-date="certificate.authority.authorityCertificate.notAfter"
min-date="certificate.authority.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>
</div>
<div class="form-group"
ng-class="{'has-error': trackingForm.csr.$invalid&&trackingForm.csr.$dirty, 'has-success': !trackingForm.csr.$invalid&&trackingForm.csr.$dirty}">
<label class="control-label col-sm-2">
Certificate Signing Request (CSR)
</label>
<div class="col-sm-10">
<textarea tooltip="Values defined in the CSR will take precedence" name="certificate signing request" ng-model="certificate.csr"
<div class="form-group"
ng-class="{'has-error': trackingForm.csr.$invalid&&trackingForm.csr.$dirty, 'has-success': !trackingForm.csr.$invalid&&trackingForm.csr.$dirty}">
<label class="control-label col-sm-2">
Certificate Signing Request (CSR)
</label>
<div class="col-sm-10">
<textarea uib-tooltip="Values defined in the CSR will take precedence" name="certificate signing request"
ng-model="certificate.csr"
placeholder="PEM encoded string..." class="form-control"
ng-pattern="/^-----BEGIN CERTIFICATE REQUEST-----/"></textarea>
<p ng-show="trackingForm.csr.$invalid && !trackingForm.csr.$pristine"
class="help-block">Enter a valid certificate signing request.</p>
</div>
<p ng-show="trackingForm.csr.$invalid && !trackingForm.csr.$pristine"
class="help-block">Enter a valid certificate signing request.</p>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2">
Roles
</label>
<div class="col-sm-10" ng-model="certificate" role-select></div>
</div>
<div ng-include="'angular/certificates/certificate/replaces.tpl.html'"></div>
<div ng-include="'angular/certificates/certificate/notifications.tpl.html'"></div>
<div ng-include="'angular/certificates/certificate/destinations.tpl.html'"></div>
</div>
<div ng-include="'angular/certificates/certificate/replaces.tpl.html'"></div>
<div ng-include="'angular/certificates/certificate/notifications.tpl.html'"></div>
<div ng-include="'angular/certificates/certificate/destinations.tpl.html'"></div>
</div>
</form>

View File

@ -2,7 +2,7 @@
angular.module('lemur')
.controller('CertificateUploadController', function ($scope, $modalInstance, CertificateService, LemurRestangular, DestinationService, NotificationService, PluginService, toaster) {
.controller('CertificateUploadController', function ($scope, $uibModalInstance, CertificateService, LemurRestangular, DestinationService, NotificationService, PluginService, toaster) {
$scope.certificate = LemurRestangular.restangularizeElement(null, {}, 'certificates');
$scope.upload = CertificateService.upload;
@ -22,7 +22,7 @@ angular.module('lemur')
title: certificate.name,
body: 'Successfully uploaded!'
});
$modalInstance.close();
$uibModalInstance.close();
},
function (response) {
toaster.pop({
@ -35,7 +35,7 @@ angular.module('lemur')
};
$scope.cancel = function () {
$modalInstance.dismiss('cancel');
$uibModalInstance.dismiss('cancel');
};
});

View File

@ -1,5 +1,6 @@
<div class="modal-header">
<div class="modal-title">
<button type="button" class="close" ng-click="cancel()" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h3 class="modal-header">Upload a certificate <span class="text-muted"><small>encrypt all the things</small></span></h3>
</div>
<div class="modal-body">
@ -20,7 +21,7 @@
</div>
<div class="form-group"
ng-class="{'has-error': uploadForm.name.$invalid, 'has-success': !uploadForm.name.$invalid&&uploadForm.name.$dirty}">
<label class="control-label col-sm-2" tooltip="If no name is provided, Lemur will generate a name for you">
<label class="control-label col-sm-2" uib-tooltip="If no name is provided, Lemur will generate a name for you">
Custom Name <span class="glyphicon glyphicon-question-sign"></span>
</label>
<div class="col-sm-10">
@ -44,7 +45,7 @@
</label>
<div class="col-sm-10">
<textarea name="publicCert" ng-model="certificate.publicCert" placeholder="PEM encoded string..."
<textarea name="publicCert" ng-model="certificate.body" placeholder="PEM encoded string..."
class="form-control" ng-pattern="/^-----BEGIN CERTIFICATE-----/" required></textarea>
<p ng-show="uploadForm.publicCert.$invalid && !uploadForm.publicCert.$pristine" class="help-block">Enter
@ -72,7 +73,7 @@
</label>
<div class="col-sm-10">
<textarea name="intermediateCert" ng-model="certificate.intermediateCert"
<textarea name="intermediateCert" ng-model="certificate.chain"
placeholder="PEM encoded string..." class="form-control"
ng-pattern="/^-----BEGIN CERTIFICATE-----/"></textarea>
@ -80,6 +81,12 @@
class="help-block">Enter a valid certificate.</p>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2">
Roles
</label>
<div class="col-sm-10" ng-model="certificate" role-select></div>
</div>
<div ng-include="'angular/certificates/certificate/replaces.tpl.html'"></div>
<div ng-include="'angular/certificates/certificate/notifications.tpl.html'"></div>
<div ng-include="'angular/certificates/certificate/destinations.tpl.html'"></div>

View File

@ -4,38 +4,52 @@ angular.module('lemur')
.service('CertificateApi', function (LemurRestangular, DomainService) {
LemurRestangular.extendModel('certificates', function (obj) {
return angular.extend(obj, {
attachRole: function (role) {
this.selectedRole = null;
if (this.roles === undefined) {
this.roles = [];
}
this.roles.push(role);
},
removeRole: function (index) {
this.roles.splice(index, 1);
},
attachAuthority: function (authority) {
this.authority = authority;
this.authority.maxDate = moment(this.authority.notAfter).subtract(1, 'days').format('YYYY/MM/DD');
},
attachSubAltName: function () {
if (this.extensions === undefined) {
this.extensions = {};
}
attachSubAltName: function () {
if (this.extensions === undefined) {
this.extensions = {};
}
if (this.extensions.subAltNames === undefined) {
this.extensions.subAltNames = {'names': []};
}
if (this.extensions.subAltNames === undefined) {
this.extensions.subAltNames = {'names': []};
}
if (!angular.isString(this.subAltType)) {
this.subAltType = 'CNAME';
}
if (!angular.isString(this.subAltType)) {
this.subAltType = 'CNAME';
}
if (angular.isString(this.subAltValue) && angular.isString(this.subAltType)) {
this.extensions.subAltNames.names.push({'nameType': this.subAltType, 'value': this.subAltValue});
this.findDuplicates();
}
if (angular.isString(this.subAltValue) && angular.isString(this.subAltType)) {
this.extensions.subAltNames.names.push({'nameType': this.subAltType, 'value': this.subAltValue});
//this.findDuplicates();
}
this.subAltType = null;
this.subAltValue = null;
},
this.subAltType = null;
this.subAltValue = null;
},
removeSubAltName: function (index) {
this.extensions.subAltNames.names.splice(index, 1);
this.findDuplicates();
//this.findDuplicates();
},
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)) {
@ -110,6 +124,7 @@ angular.module('lemur')
CertificateService.create = function (certificate) {
certificate.attachSubAltName();
certificate.attachCustom();
// Help users who may have just typed in their authority
if (!certificate.authority) {
AuthorityService.findActiveAuthorityByName(certificate.selectedAuthority).then(function (authorities) {

View File

@ -17,7 +17,7 @@ angular.module('lemur')
});
})
.controller('CertificatesViewController', function ($q, $scope, $modal, $stateParams, CertificateApi, CertificateService, MomentService, ngTableParams, toaster) {
.controller('CertificatesViewController', function ($q, $scope, $uibModal, $stateParams, CertificateApi, CertificateService, MomentService, ngTableParams, toaster) {
$scope.filter = $stateParams;
$scope.certificateTable = new ngTableParams({
page: 1, // show first page
@ -31,15 +31,6 @@ angular.module('lemur')
getData: function ($defer, params) {
CertificateApi.getList(params.url())
.then(function (data) {
// TODO we should attempt to resolve all of these in parallel
_.each(data, function (certificate) {
CertificateService.getDomains(certificate);
CertificateService.getDestinations(certificate);
CertificateService.getNotifications(certificate);
CertificateService.getReplacements(certificate);
CertificateService.getAuthority(certificate);
CertificateService.getCreator(certificate);
});
params.total(data.total);
$defer.resolve(data);
});
@ -115,30 +106,27 @@ angular.module('lemur')
$scope.fields = [{title: 'Current User', value: 'currentUser'}, {title: 'All', value: 'all'}];
$scope.toggleFilter = function (params) {
params.settings().$scope.show_filter = !params.settings().$scope.show_filter;
};
$scope.create = function () {
var modalInstance = $modal.open({
var uibModalInstance = $uibModal.open({
animation: true,
controller: 'CertificateCreateController',
templateUrl: '/angular/certificates/certificate/certificateWizard.tpl.html',
size: 'lg'
size: 'lg',
backdrop: 'static'
});
modalInstance.result.then(function () {
uibModalInstance.result.then(function () {
$scope.certificateTable.reload();
});
};
$scope.edit = function (certificateId) {
var modalInstance = $modal.open({
var uibModalInstance = $uibModal.open({
animation: true,
controller: 'CertificateEditController',
templateUrl: '/angular/certificates/certificate/edit.tpl.html',
size: 'lg',
backdrop: 'static',
resolve: {
editId: function () {
return certificateId;
@ -146,30 +134,32 @@ angular.module('lemur')
}
});
modalInstance.result.then(function () {
uibModalInstance.result.then(function () {
$scope.certificateTable.reload();
});
};
$scope.import = function () {
var modalInstance = $modal.open({
var uibModalInstance = $uibModal.open({
animation: true,
controller: 'CertificateUploadController',
templateUrl: '/angular/certificates/certificate/upload.tpl.html',
size: 'lg'
size: 'lg',
backdrop: 'static'
});
modalInstance.result.then(function () {
uibModalInstance.result.then(function () {
$scope.certificateTable.reload();
});
};
$scope.export = function (certificateId) {
$modal.open({
$uibModal.open({
animation: true,
controller: 'CertificateExportController',
templateUrl: '/angular/certificates/certificate/export.tpl.html',
size: 'lg',
backdrop: 'static',
resolve: {
editId: function () {
return certificateId;

View File

@ -13,13 +13,15 @@
</button>
</div>
<div class="btn-group">
<button ng-click="toggleFilter(certificateTable)" class="btn btn-default">Filter</button>
<button ng-model="showFilter" class="btn btn-default" uib-btn-checkbox
btn-checkbox-true="1"
btn-checkbox-false="0">Filter</button>
</div>
<!--<select class="form-control" ng-model="show" ng-options="item.value as item.title for item in fields"></select>-->
<div class="clearfix"></div>
</div>
<div class="table-responsive">
<table ng-table="certificateTable" class="table" show-filter="false" template-pagination="angular/pager.html">
<table ng-table="certificateTable" class="table" show-filter="showFilter" template-pagination="angular/pager.html">
<tbody>
<tr ng-class="{'even-row': $even }" ng-repeat-start="certificate in $data track by $index">
<td data-title="'Name'" sortable="'name'" filter="{ 'name': 'text' }">
@ -37,14 +39,14 @@
<td data-title="'Issuer'" sortable="'issuer'" filter="{ 'issuer': 'text' }">
{{ certificate.authority.name || certificate.issuer }}
</td>
<td data-title="'Domains'" filter="{ 'cn': 'text'}">
<td data-title="'Common Name'" filter="{ 'cn': 'text'}">
{{ certificate.cn }}
</td>
<td class="col-md-2" data-title="''">
<div class="btn-group pull-right">
<a class="btn btn-sm btn-default" ui-sref="certificate({name: certificate.name})">Permalink</a>
<button ng-model="certificate.toggle" class="btn btn-sm btn-info" btn-checkbox btn-checkbox-true="1"
butn-checkbox-false="0">More
<button ng-model="certificate.toggle" class="btn btn-sm btn-info" uib-btn-checkbox btn-checkbox-true="1"
btn-checkbox-false="0">More
</button>
<button ng-click="export(certificate.id)" class="btn btn-sm btn-success">
Export
@ -53,35 +55,35 @@
</div>
</td>
</tr>
<tr class="warning" ng-show="certificate.toggle" ng-repeat-end>
<td colspan="6">
<tabset justified="true" class="col-md-6">
<tab>
<tab-heading>Basic Info</tab-heading>
<tr class="warning" ng-if="certificate.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">
{{ certificate.creator.email }}
{{ certificate.user.email }}
</span>
</li>
<li class="list-group-item">
<strong>Not Before</strong>
<span class="pull-right" tooltip="{{ certificate.notBefore }}">
<span class="pull-right" uib-tooltip="{{ certificate.notBefore }}">
{{ momentService.createMoment(certificate.notBefore) }}
</span>
</li>
<li class="list-group-item">
<strong>Not After</strong>
<span class="pull-right" tooltip="{{ certificate.notAfter }}">
<span class="pull-right" uib-tooltip="{{ certificate.notAfter }}">
{{ momentService.createMoment(certificate.notAfter) }}
</span>
</li>
<li class="list-group-item">
<strong>San</strong>
<span class="pull-right">
<i class="glyphicon glyphicon-ok" ng-show="certificate.san"></i>
<i class="glyphicon glyphicon-remove" ng-show="!certificate.san"></i>
<i class="glyphicon glyphicon-ok" ng-if="certificate.san"></i>
<i class="glyphicon glyphicon-remove" ng-if="!certificate.san"></i>
</span>
</li>
<li class="list-group-item">
@ -97,13 +99,13 @@
<span class="pull-right">{{ certificate.serial }}</span>
</li>
<li
tooltip="Lemur will attempt to check a certificates validity, this is used to track whether a certificate as been revoked"
uib-tooltip="Lemur will attempt to check a certificates validity, this is used to track whether a certificate as been revoked"
class="list-group-item">
<strong>Validity</strong>
<span class="pull-right">
<span ng-show="!certificate.status" class="label label-warning">Unknown</span>
<span ng-show="certificate.status == 'revoked'" class="label label-danger">Revoked</span>
<span ng-show="certificate.status == 'valid'" class="label label-success">Valid</span>
<span ng-if="!certificate.status" class="label label-warning">Unknown</span>
<span ng-if="certificate.status == 'revoked'" class="label label-danger">Revoked</span>
<span ng-if="certificate.status == 'valid'" class="label label-success">Valid</span>
</span>
</li>
<li class="list-group-item">
@ -111,71 +113,80 @@
<p>{{ certificate.description }}</p>
</li>
</ul>
</tab>
<tab>
<tab-heading>Notifications</tab-heading>
</uib-tab>
<uib-tab>
<uib-tab-heading>Notifications</uib-tab-heading>
<ul class="list-group">
<li class="list-group-item" ng-repeat="notification in certificate.notifications">
<strong>{{ notification.label }}</strong>
<span class="pull-right">{{ notification.description}}</span>
</li>
</ul>
</tab>
<tab>
<tab-heading>Destinations</tab-heading>
</uib-tab>
<uib-tab>
<uib-tab-heading>Roles</uib-tab-heading>
<ul class="list-group">
<li class="list-group-item" ng-repeat="role in certificate.roles">
<strong>{{ role.name }}</strong>
<span class="pull-right">{{ role.description}}</span>
</li>
</ul>
</uib-tab>
<uib-tab>
<uib-tab-heading>Destinations</uib-tab-heading>
<ul class="list-group">
<li class="list-group-item" ng-repeat="destination in certificate.destinations">
<strong>{{ destination.label }}</strong>
<span class="pull-right">{{ destination.description }}</span>
</li>
</ul>
</tab>
<tab>
<tab-heading>Domains</tab-heading>
</uib-tab>
<uib-tab>
<uib-tab-heading>Domains</uib-tab-heading>
<div class="list-group">
<a href="#/domains/{{ domain.id }}" class="list-group-item"
ng-repeat="domain in certificate.domains">{{ domain.name }}</a>
</div>
</tab>
<tab>
<tab-heading>Replaces</tab-heading>
</uib-tab>
<uib-tab>
<uib-tab-heading>Replaces</uib-tab-heading>
<ul class="list-group">
<li class="list-group-item" ng-repeat="replacement in certificate.replacements">
<li class="list-group-item" ng-repeat="replacement in certificate.replaces">
<strong>{{ replacement.name }}</strong>
<p>{{ replacement.description}}</p>
</li>
</ul>
</tab>
</tabset>
<tabset justified="true" class="col-md-6">
<tab>
<tab-heading>
</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"
tooltip="Copy chain to clipboard" tooltip-trigger="mouseenter" clipboard
uib-tooltip="Copy chain to clipboard" tooltip-trigger="mouseenter" clipboard
text="certificate.chain"></button>
</tab-heading>
</uib-tab-heading>
<pre style="width: 100%">{{ certificate.chain }}</pre>
</tab>
<tab>
<tab-heading>
</uib-tab>
<uib-tab>
<uib-tab-heading>
Public Certificate
<button class="btn btn-xs btn-default clipboard-btn glyphicon glyphicon-copy"
tooltip="Copy certificate to clipboard" tooltip-trigger="mouseenter" clipboard
uib-tooltip="Copy certificate to clipboard" tooltip-trigger="mouseenter" clipboard
text="certificate.body"></button>
</tab-heading>
</uib-tab-heading>
<pre style="width: 100%">{{ certificate.body }}</pre>
</tab>
<tab ng-click="loadPrivateKey(certificate)">
<tab-heading>
</uib-tab>
<uib-tab ng-click="loadPrivateKey(certificate)">
<uib-tab-heading>
Private Key
<button class="btn btn-xs btn-default clipboard-btn glyphicon glyphicon-copy"
tooltip="Copy key to clipboard" tooltip-trigger="mouseenter" clipboard
uib-tooltip="Copy key to clipboard" tooltip-trigger="mouseenter" clipboard
text="certificate.privateKey"></button>
</tab-heading>
</uib-tab-heading>
<pre style="width: 100%">{{ certificate.privateKey }}</pre>
</tab>
</tabset>
</uib-tab>
</uib-tabset>
</td>
</tr>
</tbody>

View File

@ -2,7 +2,7 @@
angular.module('lemur')
.controller('DestinationsCreateController', function ($scope, $modalInstance, PluginService, DestinationService, LemurRestangular){
.controller('DestinationsCreateController', function ($scope, $uibModalInstance, PluginService, DestinationService, LemurRestangular, toaster){
$scope.destination = LemurRestangular.restangularizeElement(null, {}, 'destinations');
PluginService.getByType('destination').then(function (plugins) {
@ -10,24 +10,38 @@ angular.module('lemur')
});
$scope.save = function (destination) {
DestinationService.create(destination).then(function () {
$modalInstance.close();
DestinationService.create(destination).then(
function () {
toaster.pop({
type: 'success',
title: destination.label,
body: 'Successfully Created!'
});
$uibModalInstance.close();
}, function (response) {
toaster.pop({
type: 'error',
title: destination.label,
body: 'lemur-bad-request',
bodyOutputType: 'directive',
directiveData: response.data,
timeout: 100000
});
});
};
$scope.cancel = function () {
$modalInstance.dismiss('cancel');
$uibModalInstance.dismiss('cancel');
};
})
.controller('DestinationsEditController', function ($scope, $modalInstance, DestinationService, DestinationApi, PluginService, editId) {
.controller('DestinationsEditController', function ($scope, $uibModalInstance, DestinationService, DestinationApi, PluginService, toaster, editId) {
DestinationApi.get(editId).then(function (destination) {
$scope.destination = destination;
PluginService.getByType('destination').then(function (plugins) {
$scope.plugins = plugins;
_.each($scope.plugins, function (plugin) {
if (plugin.slug === $scope.destination.pluginName) {
plugin.pluginOptions = $scope.destination.destinationOptions;
$scope.destination.plugin = plugin;
}
});
@ -35,12 +49,27 @@ angular.module('lemur')
});
$scope.save = function (destination) {
DestinationService.update(destination).then(function () {
$modalInstance.close();
});
DestinationService.update(destination).then(
function () {
toaster.pop({
type: 'success',
title: destination.label,
body: 'Successfully Updated!'
});
$uibModalInstance.close();
}, function (response) {
toaster.pop({
type: 'error',
title: destination.label,
body: 'lemur-bad-request',
bodyOutputType: 'directive',
directiveData: response.data,
timeout: 100000
});
});
};
$scope.cancel = function () {
$modalInstance.dismiss('cancel');
$uibModalInstance.dismiss('cancel');
};
});

View File

@ -1,56 +1,55 @@
<div class="modal-header">
<div class="modal-title">
<h3 class="modal-header"><span ng-show="!destination.fromServer">Create</span><span ng-show="destination.fromServer">Edit</span> Destination <span class="text-muted"><small>oh the places you will go!</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': createForm.label.$invalid, 'has-success': !createForm.label.$invalid&&createForm.label.$dirty}">
<label class="control-label col-sm-2">
Label
</label>
<div class="col-sm-10">
<input name="label" ng-model="destination.label" placeholder="Label" class="form-control" required/>
<p ng-show="createForm.label.$invalid && !createForm.label.$pristine" class="help-block">You must enter an destination label</p>
</div>
<button type="button" class="close" ng-click="cancel()" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h3><span ng-show="!destination.fromServer">Create</span><span ng-show="destination.fromServer">Edit</span> Destination <span class="text-muted"><small>oh the places you will go!</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': createForm.label.$invalid, 'has-success': !createForm.label.$invalid&&createForm.label.$dirty}">
<label class="control-label col-sm-2">
Label
</label>
<div class="col-sm-10">
<input name="label" ng-model="destination.label" placeholder="Label" class="form-control" required/>
<p ng-show="createForm.label.$invalid && !createForm.label.$pristine" class="help-block">You must enter an destination label</p>
</div>
<div class="form-group">
<label class="control-label col-sm-2">
Description
</label>
<div class="col-sm-10">
<textarea name="comments" ng-model="destination.description" placeholder="Something elegant" class="form-control" ></textarea>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2">
Description
</label>
<div class="col-sm-10">
<textarea name="comments" ng-model="destination.description" placeholder="Something elegant" class="form-control" ></textarea>
</div>
<div class="form-group">
<label class="control-label col-sm-2">
Plugin
</label>
<div class="col-sm-10">
<select class="form-control" ng-model="destination.plugin" ng-options="plugin.title for plugin in plugins" required></select>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2">
Plugin
</label>
<div class="col-sm-10">
<select class="form-control" ng-model="destination.plugin" ng-options="plugin.title for plugin in plugins" required></select>
</div>
<div class="form-group" ng-repeat="item in destination.plugin.pluginOptions">
<ng-form name="subForm" class="form-horizontal" role="form" novalidate>
<div ng-class="{'has-error': subForm.sub.$invalid, 'has-success': !subForm.sub.$invalid&&subForm.sub.$dirty}">
<label class="control-label col-sm-2">
{{ item.name | titleCase }}
</label>
<div class="col-sm-10">
<input name="sub" ng-if="item.type == 'int'" type="number" ng-pattern="/^[0-9]{12,12}$/" class="form-control" ng-model="item.value"/>
<select name="sub" ng-if="item.type == 'select'" class="form-control" ng-options="i for i in item.available" ng-model="item.value"></select>
<input name="sub" ng-if="item.type == 'bool'" class="form-control" type="checkbox" ng-model="item.value">
<input name="sub" ng-if="item.type == 'str'" type="text" class="form-control" ng-model="item.value"/>
<p ng-show="subForm.sub.$invalid && !subForm.sub.$pristine" class="help-block">{{ item.helpMessage }}</p>
</div>
</div>
<div class="form-group" ng-repeat="item in destination.plugin.pluginOptions">
<ng-form name="subForm" class="form-horizontal" role="form" novalidate>
<div ng-class="{'has-error': subForm.sub.$invalid, 'has-success': !subForm.sub.$invalid&&subForm.sub.$dirty}">
<label class="control-label col-sm-2">
{{ item.name | titleCase }}
</label>
<div class="col-sm-10">
<input name="sub" ng-if="item.type == 'int'" type="number" ng-pattern="/^[0-9]{12,12}$/" class="form-control" ng-model="item.value"/>
<select name="sub" ng-if="item.type == 'select'" class="form-control" ng-options="i for i in item.available" ng-model="item.value"></select>
<input name="sub" ng-if="item.type == 'bool'" class="form-control" type="checkbox" ng-model="item.value">
<input name="sub" ng-if="item.type == 'str'" type="text" class="form-control" ng-model="item.value"/>
<p ng-show="subForm.sub.$invalid && !subForm.sub.$pristine" class="help-block">{{ item.helpMessage }}</p>
</div>
</ng-form>
</div>
</form>
</div>
<div class="modal-footer">
<button ng-click="save(destination)" type="submit" ng-disabled="createForm.$invalid" class="btn btn-primary">Save</button>
<button ng-click="cancel()" class="btn btn-danger">Cancel</button>
</div>
</div>
</ng-form>
</div>
</form>
</div>
<div class="modal-footer">
<button ng-click="save(destination)" type="submit" ng-disabled="createForm.$invalid" class="btn btn-primary">Save</button>
<button ng-click="cancel()" class="btn btn-danger">Cancel</button>
</div>

View File

@ -3,7 +3,7 @@ angular.module('lemur')
.service('DestinationApi', function (LemurRestangular) {
return LemurRestangular.all('destinations');
})
.service('DestinationService', function ($location, DestinationApi, PluginService, toaster) {
.service('DestinationService', function ($location, DestinationApi, PluginService) {
var DestinationService = this;
DestinationService.findDestinationsByName = function (filterValue) {
return DestinationApi.getList({'filter[label]': filterValue})
@ -13,41 +13,11 @@ angular.module('lemur')
};
DestinationService.create = function (destination) {
return DestinationApi.post(destination).then(
function () {
toaster.pop({
type: 'success',
title: destination.label,
body: 'Successfully created!'
});
$location.path('destinations');
},
function (response) {
toaster.pop({
type: 'error',
title: destination.label,
body: 'Was not created! ' + response.data.message
});
});
return DestinationApi.post(destination);
};
DestinationService.update = function (destination) {
return destination.put().then(
function () {
toaster.pop({
type: 'success',
title: destination.label,
body: 'Successfully updated!'
});
$location.path('destinations');
},
function (response) {
toaster.pop({
type: 'error',
title: destination.label,
body: 'Was not updated! ' + response.data.message
});
});
return destination.put();
};
DestinationService.getPlugin = function (destination) {

View File

@ -10,7 +10,7 @@ angular.module('lemur')
});
})
.controller('DestinationsViewController', function ($scope, $modal, DestinationApi, DestinationService, ngTableParams, toaster) {
.controller('DestinationsViewController', function ($scope, $uibModal, DestinationApi, DestinationService, ngTableParams, toaster) {
$scope.filter = {};
$scope.destinationsTable = new ngTableParams({
page: 1, // show first page
@ -24,9 +24,6 @@ angular.module('lemur')
getData: function ($defer, params) {
DestinationApi.getList(params.url()).then(
function (data) {
_.each(data, function (destination) {
DestinationService.getPlugin(destination);
});
params.total(data.total);
$defer.resolve(data);
}
@ -43,18 +40,19 @@ angular.module('lemur')
toaster.pop({
type: 'error',
title: 'Opps',
body: 'I see what you did there' + response.data.message
body: 'I see what you did there: ' + response.data.message
});
}
);
};
$scope.edit = function (destinationId) {
var modalInstance = $modal.open({
var uibModalInstance = $uibModal.open({
animation: true,
templateUrl: '/angular/destinations/destination/destination.tpl.html',
controller: 'DestinationsEditController',
size: 'lg',
backdrop: 'static',
resolve: {
editId: function () {
return destinationId;
@ -62,28 +60,25 @@ angular.module('lemur')
}
});
modalInstance.result.then(function () {
uibModalInstance.result.then(function () {
$scope.destinationsTable.reload();
});
};
$scope.create = function () {
var modalInstance = $modal.open({
var uibModalInstance = $uibModal.open({
animation: true,
controller: 'DestinationsCreateController',
templateUrl: '/angular/destinations/destination/destination.tpl.html',
size: 'lg'
size: 'lg',
backdrop: 'static'
});
modalInstance.result.then(function () {
uibModalInstance.result.then(function () {
$scope.destinationsTable.reload();
});
};
$scope.toggleFilter = function (params) {
params.settings().$scope.show_filter = !params.settings().$scope.show_filter;
};
});

View File

@ -8,12 +8,14 @@
<button ng-click="create()" class="btn btn-primary">Create</button>
</div>
<div class="btn-group">
<button ng-click="toggleFilter(destinationsTable)" class="btn btn-default">Filter</button>
<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="destinationsTable" class="table table-striped" show-filter="false" template-pagination="angular/pager.html" >
<table ng-table="destinationsTable" class="table table-striped" show-filter="showFilter" template-pagination="angular/pager.html" >
<tbody>
<tr ng-repeat="destination in $data track by $index">
<td data-title="'Label'" sortable="'label'" filter="{ 'label': 'text' }">
@ -30,10 +32,10 @@
</td>
<td data-title="''">
<div class="btn-group-vertical pull-right">
<button tooltip="Edit Destination" ng-click="edit(destination.id)" class="btn btn-sm btn-info">
<button uib-tooltip="Edit Destination" ng-click="edit(destination.id)" class="btn btn-sm btn-info">
Edit
</button>
<button tooltip="Delete Destination" ng-click="remove(destination)" type="button" class="btn btn-sm btn-danger pull-left">
<button uib-tooltip="Delete Destination" ng-click="remove(destination)" type="button" class="btn btn-sm btn-danger pull-left">
Remove
</button>
</div>

View File

@ -0,0 +1,63 @@
'use strict';
angular.module('lemur')
.controller('DomainsCreateController', function ($scope, $uibModalInstance, PluginService, DomainService, LemurRestangular, toaster){
$scope.domain = LemurRestangular.restangularizeElement(null, {}, 'domains');
$scope.save = function (domain) {
DomainService.create(domain).then(
function () {
toaster.pop({
type: 'success',
title: domain,
body: 'Successfully Created!'
});
$uibModalInstance.close();
}, function (response) {
toaster.pop({
type: 'error',
title: domain,
body: 'lemur-bad-request',
bodyOutputType: 'directive',
directiveData: response.data,
timeout: 100000
});
});
};
$scope.cancel = function () {
$uibModalInstance.dismiss('cancel');
};
})
.controller('DomainsEditController', function ($scope, $uibModalInstance, DomainService, DomainApi, toaster, editId) {
DomainApi.get(editId).then(function (domain) {
$scope.domain = domain;
});
$scope.save = function (domain) {
DomainService.update(domain).then(
function () {
toaster.pop({
type: 'success',
title: domain,
body: 'Successfully Created!'
});
$uibModalInstance.close();
}, function (response) {
toaster.pop({
type: 'error',
title: domain,
body: 'lemur-bad-request',
bodyOutputType: 'directive',
directiveData: response.data,
timeout: 100000
});
});
};
$scope.cancel = function () {
$uibModalInstance.dismiss('cancel');
};
});

Some files were not shown because too many files have changed in this diff Show More